リクエストの二重送信防止に 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();
}

参考)

ふりかえり

転職して3ヶ月。この期間にやったことなどをふりかえってみたり。

やったこと

  • オリエンテーション (人事制度説明とか)
  • 各種アカウント準備
  • PCセットアップ
  • 開発環境構築
  • アラート解析
  • デバッグ (軽微/小規模なもの)
  • エンハンス
  • 問い合わせ対応

最初は、業務やシステム全体の理解を兼ねてアラートの解析などをやりつつ、ある程度原因が分かるものはデバッグしてリリース。その後は少しずつエンハンスのタスクを担当するように。1月からはマーケ寄りの施策をリリース。現在は諸事情があって一時的に別チームに合流して開発タスクをサポートしている。

問い合わせ対応は、いわゆる社内の割り込みタスクみたいなもので、基本的に開発メンバー全体宛に問い合わせが来る。比較的簡単なものは自分から拾うようにした。拾ったあとに前提知識が足りなくてハマったりしたけど、チームメンバーにサポートしてもらいつつなんとか対応。少なくとも自分から行動できたのはよかったと思う。

オンボーディング&コミュニケーション

社内の対面のコミュニケーションはこんな感じ。

  • デイリー
  • スプリントレビュー
  • 1 on 1
  • 社内勉強会
  • 部内ミーティング
  • など...

現在はリモートで作業しており、デイリー以外は基本的に Slack でやり取りしている。ある程度、自走できるひとであれば困ることはあまりないと思う。というか、困ったときにどうすればいいかを知っているのである程度のことは解決できそう。ただ、リモートメインのオンボーディングは、経験の浅いひとにはやはり厳しいように思う今日この頃...。受け入れる側は、明確な指示が出せるような体制とある程度ブレイクダウンしたタスクを準備してサポートする必要がありそう。

あと、どこかのテックブログで「入社初日にコードを書いてリリースする文化がある」という記事を読んだ記憶がある。初日にリリースまでやるには、短時間で開発環境をセットアップできる体制と手頃なタスクを前もって準備する必要がある。なかなか大変な気はするけど、こういう文化はよさそう。初日とは言わずともできる限り早いタイミングで自分が書いたコードをリリースするのはいい試みかもしれない。今後、自分が新しいメンバーを迎える側になったときにやってみようかな。

ミーティングなど、特にあまり交流のないひとたちの中で自分の意見を言うのはなかなか勇気がいるものだし、どちらかというと自分も苦手な方ではある。ただ、前職の頃からわりと意識して行動していたことだったので、今となってはあまり苦手というわけでもないのかもしれない。というか、年齢を重ねるにつれてこういう部分がいい意味で鈍感になってる気がする。肩の力を抜いて仕事に取り組めるのはいいかもしれないけど。

その他

あまり根性論みたいになるのはいやだけど、この3ヶ月は休まないようにした。別に無理する必要はないけど、やっぱりできる限り早くキャッチアップしたいみたいな焦りや不安がある。まぁ、焦りや不安をモチベーションにして小さなことからコツコツと積み重ねるしかない。

これまで、直接業務で使う機会がなかった技術なども、気になるものはできる限りキャッチアップすることを心掛けていた。最近、このあたりが実際の業務に生かせている気がする。今すぐ使うかどうか分からないものでも情報収集して引き出しを増やしておくのは大事。業務で必要になってから準備をするのではなく、必要になった段階ではある程度の素振りまでは済ませておきたいところ。

ところで、最近はこんな本を読んでます。

Ingress のルーティングを nginx-hello で試してみる

Ingress のルーティングを nginxinc/NGINX-Demosnginx-hello を使って試してみる。

まずは Ingress Controller を有効にする。

$ minikube addons enable ingress
🔎  Verifying ingress addon...
🌟  The 'ingress' addon is enabled

$ kubectl get pods -n kube-system
NAME                                        READY   STATUS      RESTARTS   AGE
coredns-f9fd979d6-5mfmx                     1/1     Running     0          5m7s
etcd-minikube                               1/1     Running     0          5m12s
ingress-nginx-admission-create-x6d2h        0/1     Completed   0          3m25s
ingress-nginx-admission-patch-kkr7b         0/1     Completed   2          3m25s
ingress-nginx-controller-558664778f-k8gjr   1/1     Running     0          3m25s
kube-apiserver-minikube                     1/1     Running     0          5m12s
kube-controller-manager-minikube            1/1     Running     0          5m12s
kube-proxy-zjvcb                            1/1     Running     0          5m7s
kube-scheduler-minikube                     1/1     Running     0          5m12s
storage-provisioner                         1/1     Running     0          5m12s

バックエンドの Service と Deployment を作成。イメージは nginxdemos/nginx-hello:plain-text を使う。

---
apiVersion: v1
kind: Service
metadata:
  name: sandbox-nginx-service-1
spec:
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: sandbox-nginx-1
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sandbox-nginx-deployment-1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sandbox-nginx-1
  template:
    metadata:
      labels:
        app: sandbox-nginx-1
    spec:
      containers:
      - image: nginxdemos/nginx-hello:plain-text
        name: nginx
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: sandbox-nginx-service-2
spec:
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: sandbox-nginx-2
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sandbox-nginx-deployment-2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sandbox-nginx-2
  template:
    metadata:
      labels:
        app: sandbox-nginx-2
    spec:
      containers:
      - image: nginxdemos/nginx-hello:plain-text
        name: nginx
        ports:
        - containerPort: 80
$ kubectl apply -f backends.yml 
service/sandbox-nginx-service-1 created
deployment.apps/sandbox-nginx-deployment-1 created
service/sandbox-nginx-service-2 created
deployment.apps/sandbox-nginx-deployment-2 created

Ingress を作成。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sandbox-ingress
spec:
  rules:
  - http:
      paths:
      - path: /nginx1
        pathType: Prefix
        backend:
          service:
            name: sandbox-nginx-service-1
            port:
              number: 80
      - path: /nginx2
        pathType: Prefix
        backend:
          service:
            name: sandbox-nginx-service-2
            port:
              number: 80
$ kubectl apply -f ingress.yml 
ingress.networking.k8s.io/sandbox-ingress created

$ kubectl get ingress
NAME              CLASS    HOSTS   ADDRESS         PORTS   AGE
sandbox-ingress   <none>   *       192.168.64.10   80      110s

それぞれにリクエストしてみる。

$ kubectl get pods
NAME                                          READY   STATUS    RESTARTS   AGE
sandbox-nginx-deployment-1-677d4f9887-dq5d7   1/1     Running   0          5m2s
sandbox-nginx-deployment-2-694b844ff6-fnxzh   1/1     Running   0          5m2s

# /nginx1 にリクエスト
$ curl http://`minikube ip`/nginx1
Server address: 172.17.0.3:8080
Server name: sandbox-nginx-deployment-1-677d4f9887-dq5d7
Date: 10/Jan/2021:03:06:03 +0000
URI: /nginx1
Request ID: 3cc25e80736bfcc2bc13b16c95884e54

# /nginx2 にリクエスト
$ curl http://`minikube ip`/nginx2
Server address: 172.17.0.4:8080
Server name: sandbox-nginx-deployment-2-694b844ff6-fnxzh
Date: 10/Jan/2021:03:06:07 +0000
URI: /nginx2
Request ID: 7429d04e9bf93c4e462545f62f780c50

/nginx1, /nginx2 のレスポンスがそれぞれの Service の nginx から返る。nginx-hello を使うとルーティングされていることが分かりやすい。

参考)

Kubernetes on Minikube/HyperKit

備忘録。

KubernetesMinikube で試してみる。(ローカルでマルチノードクラスタを構築する場合は kind を使う)

$ minikube version
minikube version: v1.15.0

今回は HyperKit 上に Kubernetes を起動する。デフォルトは Docker が使われるっぽい。他には VirtualBox など。

$ minikube start --driver=hyperkit
$ minikube start # クラスタ作成済の場合はこっち

$ minikube status
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured

$ kubectl get nodes
NAME       STATUS   ROLES    AGE   VERSION
minikube   Ready    master   42s   v1.19.4

$ kubectl version --short
Client Version: v1.19.3
Server Version: v1.19.4

Minikube クラスタ停止/削除。

$ minikube stop
$ minikube delete

リソース作成/更新。

$ kubectl apply -f <FILENAME>

Pod の詳細情報を表示する。

$ kubectl describe pod/<POD_NAME>

コンテナを起動して shell を使う。--rm オプションを付けるとコンテナ終了時に Deployment が削除される。

$ kubectl run alpine -it --rm --image alpine -- ash

ConfigMap, Secret は設定や Credential をコンテナイメージから分離する。値は Volume や環境変数を通して Pod に渡される。ConfigMap で UTF-8 に含まれないバイトシーケンスがある場合は binaryData フィールドを使う。値は Base64エンコードされる。

Secret で stringData フィールドを使う場合は Base64 エンコードは不要。適用の際にエンコードして data に渡される。

コンテナのヘルスチェックには LivenessProbe, ReadinessProbe を使う。LivenessProbe はコンテナの存否をチェックする。LivenessProbe が通らない場合はコンテナが再作成される。ReadinessProbe はコンテナが Ready かどうかをチェックする。ReadinessProbe が通らない場合は Service のルーティングから除外される。


今回はここまで。