ユニコーン企業のひみつ

先日、『ユニコーン企業のひみつ』を購入して読んでみました。

「チームに権限を与える」「チームを信頼する」ことで 自律性のあるチームにする というのが全体を通して書かれており、そのために Spotify が取り組んできたことが紹介されています。

このあたりはなかなか興味深い。

この本の中で「エンタープライズ企業」とか「従来型企業」というのが頻繁に登場してユニコーン企業と比較されてるのですが、これらの前提が極端であまり納得感がない感じがします。

「計画や予算がゴールになりがちでプロダクトにフォーカスできていない」というのは確かにありそうだけど、いわゆる「従来型企業」でも準委任契約のプロジェクトでアジャイルスクラムのような形を取ることは今となっては特に珍しい話ではないと思われます。

ただ、これに関して個人的に悩ましいのは、こういうプロジェクトだとなかなか規模 (売上) がスケールできないところが課題として捉えられてしまうところ。「うまみのないプロジェクト」みたいに言われてしまうと、そのプロジェクトのメンバーは仕事に対する意義を見出しにくくなるような気がします。その点で、リソースに投資して自律性のある小さいチームにするというのは、わりと合理的なのかなって思う今日この頃です。

読みやすくてなかなかおもしろい内容だったし、こういう本を通して自分たちのチームの課題に向き合うきっかけになったらいいのかなって。

Vegeta attack

以前から名前は知ってたけど、パフォーマンステストをするのに Vegeta が手軽に使えて便利そう。README にだいたい書いてあるけど取り急ぎ使いそうなところだけ備忘録。

github.com

$ brew update && brew install vegeta
$ echo "GET http://localhost:8080/" | \
    vegeta attack -rate=10 -duration=60s | \
    tee results.bin | \
    vegeta report

-rate は単位時間あたりのリクエスト数。デフォルトは秒間。分間で指定する場合は -rate=10/m のように書けばよさそう。-duration はテストの実行期間。

リクエストヘッダは -header で指定する。例えば、Authorization ヘッダを付ける場合はこんな感じ。

$ echo "GET http://localhost:8080/" | \
    vegeta attack -header "authorization: Basic $(echo -n '{username}:{password}' | base64)" -rate=10 -duration=60s | \
    ...

実行結果を vegeta report で出力するとこんな感じ。

Requests      [total, rate, throughput]         600, 10.02, 10.01
Duration      [total, attack, wait]             59.956s, 59.902s, 54.164ms
Latencies     [min, mean, 50, 90, 95, 99, max]  38.694ms, 56.504ms, 49.225ms, 70.715ms, 85.368ms, 175.937ms, 576.39ms
Bytes In      [total, mean]                     1059600, 1766.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:600  
Error Set:
  • Requests
  • Duration
    • total : テスト実行時間の合計 (attack + wait)
    • attack : すべてのリクエスト送信にかかった時間 (total - wait)
    • wait : レスポンスが返るまでの時間 (total - attack)
  • Latencies
    • min, mean, max : レイテンシの最小値, 平均値, 最大値
    • 50, 90, 95, 99 : パーセンタイル
  • Bytes In, Bytes Out
    • total, mean : リクエストボディ, レスポンスボディのバイト数の合計, 平均値
  • Success
  • Status Codes
  • Error Set : エラーレスポンスのステータスコードが列挙される

vegeta plot で実行結果をグラフ表示できる。

$ cat results.bin | vegeta plot > plot.html

f:id:knt_mr:20210507000758p:plain

現場からは以上です。

HIGH OUTPUT MANAGEMENT

以前、あるラジオで紹介されていた HIGH OUTPUT MANAGEMENT を購入して読んでみました。とは言っても購入したのは年初で、途中ちょっと積読しつつようやく読み終えた...。著者の Andrew Grove 氏はインテル社の CEO を務めていた方らしい。

「自身が率いる組織のアウトプットを最大化」をテーマにマネージャーがやるべきことが細かく書かれています。書かれてることをざっくり分類するとこんな感じ?

  • プロセスや品質のモニタリング&改善
  • ミーティング, 1on1
  • チーム運営
  • タスクコントロール
  • 評価, フィードバック
  • 面接

もともとこの本が書かれたのは30年以上前らしい。もちろん加筆や修正はあると思うけど、今では当たり前のように使われている 1on1 やスクラムなんかも、この本の内容がもとになっていたりするんだろうか。いわゆるバイブル的な本なのかもしれないです。

読書メモをこのツイートのリプライにまとめてみました。内容をふりかえりたいときに読み返そう。スレッド全部を貼り付けたかったけどやり方がわからない...。

リクエストの二重送信防止に UIEvent.detail を利用する

更新系のボタンクリックでローディングを表示して二重クリックを防止する実装をよく見かけるが、次のような操作をするとリクエストが二重送信できることがある。

  1. ボタンクリック (ローディング表示)
  2. キーボードの Enter or Space 押下

ボタンクリックでボタンにフォーカスが当たるが、ローディングを表示してもボタンのフォーカスは外れない。なので、キーボードの Enter や Space 押下でボタンの click イベントが走ってリクエストが二重送信される。

たぶん、ボタンを disabled にするのが簡単かもしれないが、以降は UIEvent.detail プロパティを使って回避する方法。

developer.mozilla.org

サンプルコード

サンプルコードでは、ボタンクリックで overlay が表示されてボタンが二重クリックできなくなるが、キーボードの Enter や Space を押下するとコンソールに submit! が何度も表示される。

これに対して、ボタンの click イベントでキャンセルイベントを呼び出すようにすると事象が発生しなくなる。(L49 のコメントアウトを外す)

window.addEventListener('click', cancelKeyEvent, true)

event.detail プロパティには現在のクリック数 (> 0) が設定される。一方、キーボードの Enter や Space 押下のときは値は常に 0 になる。あとは、stopPropagation() でイベントをキャンセルする。

ちなみに event.pointerType === 'mouse'IE 用の判定。

現場からは以上です。

git worktree を試してみる

備忘録。モノリポで、あるブランチで作業中に別ブランチで開発した他のアプリケーションを立ち上げてローカルで結合したい。そんなときに git worktree が使えそう。

git-scm.com

$ git worktree list
/Users/kntmr/workspace/repo                         e562a08b4f7 [feature/service-a]

# 指定したパスにそれぞれのブランチが checkout される
# checkout したブランチは独立しているため、作業中の worktree に影響を与えない
$ git worktree add ~/workspace/worktrees/feature/service-b feature/service-b
$ git worktree add ~/workspace/worktrees/feature/service-c feature/service-c

$ git worktree list
/Users/kntmr/workspace/repo                         e562a08b4f7 [feature/service-a]
/Users/kntmr/workspace/worktrees/feature/service-b  f3ce5c04f3c [feature/service-b]
/Users/kntmr/workspace/worktrees/feature/service-c  d017566d700 [feature/service-c]

# worktree を削除する
$ git worktree remove ~/workspace/worktrees/feature/service-b
$ git worktree remove ~/workspace/worktrees/feature/service-c

ディレクトリを作成して丸ごと clone, checkout する感じ。ローカルリポジトリからコピーしているっぽいのでそこまで時間はかからない。ちょっと別ブランチを作成して hotfix を commit したいみたいなときは stash で十分かもだけど、複数のブランチで同時に作業したいときには git worktree が便利そう。

前提

$ git --version
git version 2.29.2

IntelliJ IDEA で Thymeleaf の templates を hot swapping したい

備忘録。IntelliJ IDEA で Thymeleaf の templates の hot swapping が動かないと思ったら、IntelliJ 側の設定が必要なのか。Eclipse 歴が長いもので...。

  • Preferences... > Build, Execution, Deployment > Compiler
    • Build project automatically をチェックする
  • Cmd+Shift+A > Registry...
    • compiler.automake.allow.when.app.running をチェックする
  • application.properties
    • spring.thymeleaf.cache=false

クラスファイルの変更を検知して hot reload とかしたいときは spring-boot-devtools が必要だった気がする。

前提

  • IntelliJ IDEA Ultimate 2020.2
  • Spring Boot 2.4.2
  • Thymeleaf 3.0.12

Spring Security + RBAC (Role, Authority)

簡易的ですが、Spring Security + RBAC のサンプルを作りました。

kntmr/playground/spring-security-rbac - GitHub

f:id:knt_mr:20210203230209p:plain:w420

今回、User と Role は 1 対 1 にしています。あと、Thymeleaf 側では、Permission (hasAuthority) ではなく、あえて Role (hasRole) を使っているところがあります。RBAC ならすべて Permission に寄せるべきだろうとは思いますが、今回はサンプルなので...。

というわけで、UserDetailsService#loadUserByUsername では、権限と Prefix 付き (ROLE_*) の Role を UserDetails に渡しています。これで hasAuthorityhasRole を使い分ける。

@Transactional
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByName(username);
    return new org.springframework.security.core.userdetails.User(
            user.getName(),
            user.getPassword(),
            AuthorityUtils.createAuthorityList(
                    Stream.concat(
                            Stream.of(user.getRole().nameWithPrefix()), // Prefix を付けて返す
                            user.getRole().getPermissions().stream().map(Permission::getName)).toArray(String[]::new)));
}

WebSecurityConfigurerAdapter#configure はふつう。and() でメソッドチェーンするより分割して書いた方が見やすい気がする。なるほど。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
            .loginPage("/login")
            .usernameParameter("username")
            .passwordParameter("password")
            .defaultSuccessUrl("/", true)
            .permitAll();
    http.logout()
            .logoutUrl("/logout")
            .logoutSuccessUrl("/")
            .permitAll()
            .invalidateHttpSession(true);
    http.authorizeRequests()
            .mvcMatchers("/admin/**").hasAuthority("WRITE")
            .anyRequest().authenticated();
}

参考)