久々に arch linux を使おうかな、と思いまして。なんかもうプライベートで使うマシン、mac じゃなくていいなーって気分が高まりすぎてる。
archlinux の利点は、pacman で管理できて、だいたいのものが AUR 経由でインストールできるので便利という感じなのと、ubuntu と違って、必要なものしか入らない感じにできるってことかな。
(その結果として、必要なものが入ってなくて変な状況になることもあるけど)
昔は頑張って windows のディスクをパーティションきって設定したりしてたけど、めんどくさいので SSD を USB で増設してやる。最近は USB 接続でも十分な速度が出るので良い時代だ。
https://wiki.archlinux.jp/index.php/USB_%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2
rufus で usb memory に書いた。
あとは、公式のガイド通りに設定していけばいい。fdisk はちょっとだるいので、TUI でできる cfdisk でやった。
https://wiki.archlinux.org/title/installation_guide
細かいところでよくわかんなかったところは https://zenn.dev/ytjvdcm/articles/0efb9112468de3 を参考にしたのだが、何を参考にしたのか忘れた。たぶん EFI 周り。
そして、i3wm を startx するような構成にした。
具体的に、入れてるのは以下のようなものです。
#!/bin/bash
set -ex
# install: pacman -S pkgname
# search: pacman -Ss keyword
# update list
sudo pacman -Syu
# core
sudo pacman -S --needed --noconfirm openssh vim git tmux sudo git-lfs gcc base-devel go the_silver_searcher zsh w3m curl
# nvidia driver(if needed)
sudo pacman -S --needed --noconfirm nvidia
# manage mirror site
sudo pacman -S reflector
sudo reflector --country 'Japan' --age 24 --protocol https --sort rate --save /etc/pacman.d/mirrorlist
# network
# network-manager-applet=nm-applet
sudo pacman -S --needed --noconfirm networkmanager network-manager-applet
sudo systemctl enable NetworkManager
# yay
# yay is the wrapper for AUR and pacman.
if [[ -e /usr/bin/yay ]]; then
echo "yay exists"
else
git clone https://aur.archlinux.org/yay.git /tmp/yay && cd /tmp/yay && makepkg -si
fi
# X
sudo pacman -S --needed --noconfirm wezterm firefox i3lock xdg-utils
yay -S --needed --noconfirm google-chrome
yay -S --needed --noconfirm obsidian
# startx
yay -S --needed --noconfirm xorg-xinit
# i3
sudo pacman -S --needed --noconfirm rofi i3-wm polybar xss-lock dunst
# Japanese
sudo pacman -S --needed --noconfirm adobe-source-han-sans-jp-fonts adobe-source-han-serif-jp-fonts otf-ipafont noto-fonts noto-fonts-cjk noto-fonts-emoji
yay -S --needed --noconfirm ttf-mona ttf-monapo ttf-ipa-mona ttf-vlgothic ttf-mplus ttf-koruri ttf-mplus ttf-sazanami ttf-hanazono
sudo pacman -S --needed --noconfirm fcitx5-mozc fcitx5-configtool fitx5-gtk fcitx5-qt fcitx5-im
# JetBrains
yay -S --noconfirm intellij-idea-ultimate-edition intellij-idea-ultimate-edition-jre
yay -S --noconfirm jetbrains-fleet
# Ultimate hacking keyboard
yay -S --noconfirm uhk-agent-appimage
./setup-vimplug.sh
# I need to enable multilib for installing steam app
yay -S --noconfirm steam
# Chatting
yay -S --noconfirm slack-electron
分離型のキーボードとして MD550 を使い始めた。分離型は方が自然な位置になるのでなかなか気に入りまして、Kinesis の freestyle2 などいくつか試しました(Kinesis は微妙に手になじまずすぐに諦めてしまった)。
そんな中、Ultimate hacking keyboard を知って購入したのが数ヶ月前。長らく立って、やっと届いたのが先週ぐらい、という感じ。
特徴としては
分離型でも一体型でも使える
ポインティングデバイスを親指の位置につけられる
完全にカスタマイズできるキーレイアウトを複数保持できる
といったところで、ポインティングデバイスがキーボードから手を離さなくてもできるというのがとても良いので買ってみた。
もともと値段が高いデバイスであり、さらに円安もあるのとハンガリーという遠い場所で作られてるということもあり、まぁ高い。
ポインティングデバイスがトラックボール、トラックポイント、タッチパッドから選べるのでそれを全種類うっかり頼んだのでそれもあってまぁ高くなった。
買ったもの
サイレントの茶軸。サイレントとはいえまぁそこそこ音はします。打鍵感は最高に良いです。
キーマッピング
キーマッピングはさすがに Ultimate というだけあって、複数をキーボード側に記憶させることができる。
なので、PC用とMac用で完全にワケたキーマッピングを設定している。
Windows 用のキーマッピング
Windows を操作するときに問題になるのが、Mac では Cmd と Ctrl という2つのモディファイアを使うことによって、編集系の操作とメニュー系の操作を行うことができるわけだが、これを行う機能が Windows にはないということだ。これは非常に面倒で、擬似的に Mac 風な操作を AutoHotKey で実現していた。
しかし、AutoHotKey の設定ファイルは記号まみれでいじるのがしんどい感じのファイルなので、あまりメンテナンスする気が起きないものなのである。
Ultimate hacking keyboard なら、そういった悩みはなく、GUIでポチポチ設定すればOK。簡単だった。
マクロも設定できるんで、案外複雑なマッピングもできる。
ベースマップ
ベースのキーマップはそこまでいじらず。
Aの隣はコントロール(Fn3)に。
エスケープキーがこのキーボードにはないので、Ctrl単押しの場合は ESC 相当とする。
左Cmdだと無変換、右Cmdでかな入力、という設定に Karabiner 由来でなれているのでそういう設定を入れる。こういうのがキーボード側で設定できるの便利。Karabiner ってたまにOSアップデートに追従できないときあるけどこれなら安心。
左のスペースが Mod になっていたのでスペースに。割と右も左もスペースとして使っていて、、普段。主には左を使っているので。
Cmd の位置に Fn2 レイヤーへの切り替えボタンを配置。
Mod マップ
デフォルトだとカーソルの移動が独特なので、HJKLスタイルに変更。音量の切り替えもここに入れた。音量の切り替え、どうしてもゲームやってるときとかによく使うので、割と触りやすい位置においておきたかった。
Mouse マップ
マウスキー機能がある。が、実際にはアタッチメントをつけてるのであんまいらないかな。と思いつつ一応設定。
HJKL 風にしている。
Fn マップ
基本的にはキーマップの切り替え機能だけを設定している。
Fn2 マップ(Cmd相当)
Cmd キーを押したときをエミュレートしている。
Fn3 マップ(Ctrl 相当)
Ctrl+A を押したときに Home キーを出す、みたいなのを設定している。
Ctrl+K で kill する部分は、マクロで設定している。
Mac 用のキーマップ
基本的には Windows 用と一緒。
ただし、いくつかのカスタマイズを入れている。
option+shift+↓ を拡張キーに割当ている。これは、slack, LINE で未読をさっさか処理できるように。
ケンジントンの trackball で割り当てていたものをここに移植した形。
進歩するハードウェア
Ultimate hacking keyboard の良いところは、V2 になったことでかなり進化していて、かつファームウェア的にも進歩しているようだ。
初期の頃に Ultimate hacking keyboard を買った人のブログ記事を一通り読んだのだが、初期の頃に買った人が書いている改善点がかなり改善されてきていて、かなり良くなっているように思う(ありがとう先人の人たち)
特に大きいのがスマートマクロ機能で、起動時に設定を変えるとかそういうのができるようになっている。
個人的にはOSごとに違うキーマップを設定しているので、OSの切り替え時に自動的にキーマップを変えられる機能がほしい。今は UHK Agent を自前でビルドすればできるぽいけどまだ試していない。
まとめ
結論、とても満足している。ポインティングデバイスが一体化していることのメリットがとにかくでかいし、キーマッピングをめっちゃこまかく設定できるんで快適。
今までだったらできなかったような設定ができてる(多分似たようなことができるものは存在しているのかもしれないが、僕は他に知らない。)
ファームウェアのアップデートなどでできることが増えているようだし、今後使い続けていくのも楽しみでならない。
kotlin 1.7.10 から 1.7.21 にアップグレードしようとしたら以下の謎エラーが出る怪奇現象が発生していた。
e: java.lang.NoSuchMethodError: 'void kotlin.script.experimental.api.KotlinType.<init>(kotlin.reflect.KClass, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker)'
at org.jetbrains.kotlin.scripting.definitions.ScriptCompilationConfigurationFromDefinition$1.invoke(ScriptCompilationConfigurationFromDefinition.kt:32)
at org.jetbrains.kotlin.scripting.definitions.ScriptCompilationConfigurationFromDefinition$1.invoke(ScriptCompilationConfigurationFromDefinition.kt:28)
at kotlin.script.experimental.api.ScriptCompilationConfiguration.<init>(scriptCompilation.kt:23)
at kotlin.script.experimental.api.ScriptCompilationConfiguration.<init>(scriptCompilation.kt:25)
at org.jetbrains.kotlin.scripting.definitions.ScriptCompilationConfigurationFromDefinition.<init>(ScriptCompilationConfigurationFromDefinition.kt:27)
at org.jetbrains.kotlin.scripting.definitions.ScriptDefinition$Companion.getDefault(ScriptDefinition.kt:221)
at org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCompilerConfigurationExtension.updateConfiguration(ScriptingCompilerConfigurationExtension.kt:67)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment$Companion.configureProjectEnvironment(KotlinCoreEnvironment.kt:578)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.<init>(KotlinCoreEnvironment.kt:199)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.<init>(KotlinCoreEnvironment.kt:108)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment$Companion.createForProduction(KotlinCoreEnvironment.kt:445)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.createCoreEnvironment(K2JVMCompiler.kt:192)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:143)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:53)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:99)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:47)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:475)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:125)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally(IncrementalCompilerRunner.kt:373)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileIncrementally$default(IncrementalCompilerRunner.kt:318)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.rebuild(IncrementalCompilerRunner.kt:114)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:207)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:79)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:625)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:101)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1746)
at jdk.internal.reflect.GeneratedMethodAccessor26.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
色々試した結果、spring boot bom の中で kotlin のバージョンを固定しているのが問題なようだった。以下のように dependency management plugin でバージョン指定しているため、spring boot が 2.7.5 だったので、org.jetbrains.kotlin:*
のバージョンが 1.6.21 になってしまって、結果として API が齟齬ったようだ。
dependencyManagement {
imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
}
}
というわけで、以下のようにして解決。
dependencyManagement {
imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
mavenBom("org.jetbrains.kotlin:kotlin-bom:1.7.21")
}
}
20221129 追記
ext["kotlin.version"] = Versions.KOTLIN
とかするのがスマートかも
kweb provides the document to integrate spring-boot.
https://docs.kweb.io/book/integrations.html#spring-boot
But in my opinion, it's bit complex and tricky way.
In my opinion, spring-boot users want to use the spring-boot's configuration loading feature and the bean management feature . But the sample code demonstrates the kweb integration with the spring-webmvc's servlet container.
Then, I wrote a sample code to use kweb on spring boot framework. https://github.com/tokuhirom/ktor-spring-boot-demo/blob/kweb-spring-boot-demo/src/main/kotlin/com/example/springbootdemo/SpringbootdemoApplication.kt
This code demonstrates, the code starts kweb server and it runs with spring-boot's beans and configurations. While the shutdown process, spring will call the "close" method on the Kweb instance!
https://prismjs.com/
prism.js がナウいのかどうかはよくわからないけれど、とりあえず入れた。markdown のレンダリングに使っている flexmark が prism.js で使うように language-kotlin などの class をふってくれるので、そのまま読み込むだけ。
https://github.com/tokuhirom/blog3/pull/20/files
println("Hello")
s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s qq q and s s s chr length q q each die local chown rmdir tie chr q semop and s s s chr length q qr our xor getc dump order goto log join eof write time crypt dump exit vec index open ord tied q semop and s s s chr length q q each die local chown rmdir tie chr dump fcntl warn ord eval q semop and s s s chr length q qr our break chop given given pack q semop and s s s chr length q qr our break chop given given pack q semop and s s s chr length q qr our break chop given given pack exit ord goto read undef q semop and s s s chr length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind tell each given q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind tell our log q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr untie pack exp cmp given ref q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr link tie q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind fcntl q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack exit ord goto read undef exp recv tell xor wait exit tell time bind fcntl q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q q each die local chown rmdir tie flock hex alarm undef cmp each ord glob ioctl untie die chr untie pack exp cmp q semop and s s s chr length q qr our xor untie chomp lock chdir vec cmp pipe q semop and s s s length q qr our break chop given given pack q semop and eval eval
https://github.com/tokuhirom/docker-minil-release
CPAN module をリリースするには、色々と前準備が必要というか、、色々な CPAN module をインストールしなきゃいけなくてちょっと手間だった。最近は年に数回しかしないのだが、、リリースのたびに準備の時間がかかるので、ちょっと時間がかかって面倒なのだった。
面倒なので、ある程度設定した状態の VPS を維持していたのだが、それもなぁ、という。
このブログなども k8s に移行したので、VPS を維持する必要がなくなったのでその VPS を廃止したい。
本来なら github actions などからリリースできるようにしたいのだが、その手間もまぁまぁデカイということで、コスパないしタイパが良い範囲でやろうかなと。。
ということで、ある程度設定された状態の docker image を作っておこうかなと言う感じ。
実装の解説
FROM perl:latest
とすることにより、perl の docker image を利用する。正直、常に最新版を使ってリリースすりゃいいので、latest image を利用。
RUN apt update
RUN apt upgrade -y
RUN apt install -y vim
vim がはいってないと minil の機能である Changes の編集機能がつかえないので、vim を入れる。
ENV PERL_CPANM_OPT="--notest --no-man-pages --with-recommends --with-suggests --with-all-features --with-configure --with-develop"
ENV を設定して、とにかく依存っぽいやつをガンガン入れるように設定する。こうしないとテストコケることが多くなる。
続いて肝心の minilla を入れる。
RUN cpanm Minilla
あとは、インストールに時間かかる感じの依存をガンガン入れていく。
RUN cpanm Amon2
RUN cpanm DBD::SQLite LWP::Protocol::https DBD::mysql
RUN cpanm Catalyst::Runtime
RUN cpanm DBIx::Class
RUN cpanm Dist::Milla
RUN cpanm Tiffany
RUN cpanm Task::BeLike::TOKUHIROM
RUN cpanm Test::Perl::Critic Perl::Critic
RUN cpanm HTTP::Server::Simple::CGI Spiffy WWW::MobileCarrierJP
minilla はリリースプロセスの中で git commit するので、user.email, user.name を設定しておく。
RUN git config --global user.email "[email protected] "
RUN git config --global user.name "Tokuhiro Matsuno"
起動時に、/target/
にマウントされたソースコードを対象に、cpanm を使って依存を入れて、minil release が実行されるようにする。
WORKDIR /target/
CMD [ "/bin/bash", "-c", "cpanm --installdeps . && minil release" ]
利用方法
docker pull してイメージを取得する。
docker pull ghcr.io/tokuhirom/docker-minil-release:latest
あとは適当に volume を mount しながら実行するだけ。簡単。
docker run -it --rm -v $PWD:/target/ -v $HOME/.pause:/root/.pause -v $HOME/.ssh:/root/.ssh --name minil-release-app ghcr.io/tokuhirom/docker-minil-release:latest
モニター3枚ほど並べていたのだが配線がぐちゃぐちゃすぎてうんざりしたので、ウルトラワイドモニター一枚にしてみた。
かなりポートがたくさんついているので、これに接続するだけでほとんどの接続が集約されていて、快適です。USB ハブ機能があるのが便利。デスクが広く使える。
色々つないでたんですが、いまは↑のあたりをつないでいます
昇降式デスクというと、Flexispot が有名なのだが、Amazon で買えて一人でも古いデスクの回収もやってくれて、新しいデスクの組み立ても代行してくれるものという条件で探したらこれになった。
そこまで天板にこだわりないので、板を自分で選ぶとかめんどくさいなっていう感じ。
yabai という mac で動く tiling window manager を活用している。これと、同じ作者の skhd というショートカットマネージャーを組み合わせることで、ウィンドウの操作をほぼ全てキーボードで実施できるので鬼便利。
僕は ratpoison とかを長年使ってた経験あるので、この手の window manager は結構好き。
.yabairc を以下のように設定している。
# global settings
yabai -m config mouse_follows_focus off
yabai -m config focus_follows_mouse off
yabai -m config window_origin_display default
yabai -m config window_placement second_child
yabai -m config window_topmost off
yabai -m config window_shadow on
yabai -m config window_opacity off
yabai -m config window_opacity_duration 0.0
yabai -m config active_window_opacity 1.0
yabai -m config normal_window_opacity 0.60
yabai -m config window_border on
yabai -m config window_border_width 6
yabai -m config active_window_border_color 0xff775759
yabai -m config normal_window_border_color 0xff555555
yabai -m config insert_feedback_color 0xffd75f5f
yabai -m config split_ratio 0.50
yabai -m config auto_balance off
yabai -m config mouse_modifier fn
yabai -m config mouse_action1 move
yabai -m config mouse_action2 resize
yabai -m config mouse_drop_action swap
# general space settings
yabai -m config layout bsp
yabai -m config top_padding 0
yabai -m config bottom_padding 0
yabai -m config left_padding 0
yabai -m config right_padding 0
yabai -m config window_gap 0
yabai -m rule --add app="^System Preferences$" manage=off
yabai -m rule --add app="^Skitch$" manage=off
yabai -m rule --add app="^Zoom.*$" manage=off
echo "yabai configuration loaded.."
.skhdrc を以下のようにしている。
# focus window
alt - j : ~/dotfiles/bin/yabai-focus.pl
alt - k : ~/dotfiles/bin/yabai-focus.pl -r
# 画面を大きく3つのfloating window で占めて表示させる。
# 一番左に LINE アプリを表示。LINE アプリで本文が表示されるギリギリの横幅がこれ。
alt - 1: yabai -m window --toggle float && yabai -m window --grid 1:10:0:0:3:1
alt - 2: yabai -m window --toggle float && yabai -m window --grid 1:10:3:0:4:1
alt - 3: yabai -m window --toggle float && yabai -m window --grid 1:10:7:0:3:1
# swap window(よく使う)
shift + alt - h : yabai -m window --swap west
shift + alt - j : yabai -m window --swap south
shift + alt - k : yabai -m window --swap north
shift + alt - l : yabai -m window --swap east
# move window
shift + cmd - h : yabai -m window --warp west
shift + cmd - j : yabai -m window --warp south
shift + cmd - k : yabai -m window --warp north
shift + cmd - l : yabai -m window --warp east
# move window
shift + ctrl - a : yabai -m window --move rel:-20:0
shift + ctrl - s : yabai -m window --move rel:0:20
shift + ctrl - w : yabai -m window --move rel:0:-20
shift + ctrl - d : yabai -m window --move rel:20:0
# increase window size(よく使う)
shift + alt - a : yabai -m window --resize left:-20:0
shift + alt - s : yabai -m window --resize bottom:0:20
shift + alt - w : yabai -m window --resize top:0:-20
shift + alt - d : yabai -m window --resize right:20:0
# decrease window size
shift + cmd - a : yabai -m window --resize left:20:0
shift + cmd - s : yabai -m window --resize bottom:0:-20
shift + cmd - w : yabai -m window --resize top:0:20
shift + cmd - d : yabai -m window --resize right:-20:0
# rotate tree
alt - r : yabai -m space --rotate 90
# mirror tree y-axis
alt - y : yabai -m space --mirror y-axis
# mirror tree x-axis
alt - x : yabai -m space --mirror x-axis
# toggle desktop offset
alt - a : yabai -m space --toggle padding && yabai -m space --toggle gap
# toggle window fullscreen zoom
alt - f : yabai -m window --toggle zoom-fullscreen
# toggle window native fullscreen
shift + alt - f : yabai -m window --toggle float
# toggle window split type
alt - e : yabai -m window --toggle split
alt - o : yabai -m space --focus 2
alt - i : yabai -m space --move 2
# https://iter01.com/667321.html
で、ウィンドウの切り替えに以下のスクリプトを利用。
#!/usr/bin/perl
use strict;
use warnings;
use JSON::PP qw/decode_json/;
my $reversed = 0;
if (@ARGV == 1 && $ARGV[0] eq '-r') {
$reversed = 1;
}
my $json = `yabai -m query --windows --space`;
my @dat = @{decode_json($json)};
my @sorted = sort {
$a->{'is-floating'} <=> $b->{'is-floating'}
|| $a->{'frame'}{x} <=> $b->{'frame'}{x}
|| $a->{'frame'}{y} <=> $b->{'frame'}{y}
|| $a->{'frame'}{w} <=> $b->{'frame'}{w}
|| $a->{'frame'}{h} <=> $b->{'frame'}{h}
|| $a->{'id'} <=> $b->{'id'}
} @dat;
my $prev_is_focused = 0;
my @targets = 0..@sorted-1;
if ($reversed) {
@targets = reverse @targets;
}
for my $i (@targets) {
if ($prev_is_focused != 0) {
focus($sorted[$i]->{id});
exit 0;
}
if ($sorted[$i]->{'has-focus'}) {
$prev_is_focused++;
}
}
focus($sorted[$reversed ? -1 : 0]->{id});
exit 0;
sub focus {
my $id = shift;
system("yabai -m window --focus $id");
}
僕は、一般的なタイリングウィンドウマネージャー的な使い方ではなく、ちょっと変わった使い方をしている。
つかっているのがウルトラワイドモニターなので、フツウに bsp にすると画面を有効に使えてない気がするので、ちょいとした工夫をしている。
alt+1,alt+2,alt+3 で、ウィンドウをほぼ3分割して表示するようにしている。中央はやや広め。で、この3つはfloating windowとする。
alt+j, alt+k でのウィンドウの切り替えをするのだが、floating window を左から順番に切り替えて、その後 bsp を左から順番に切り替えるという設定にすることにより、ウィンドウの切り替えが速くなった。
ExponentialBackoffTest > test() FAILED
org.mockito.exceptions.base.MockitoException:
Mockito cannot mock this class: class java.util.Random.
Mockito can only mock non-private & non-final classes.
If you're not sure why you're getting this error, please report to the mailing list.
Java : 17
JVM vendor name : Eclipse Adoptium
JVM vendor version : 17.0.2+8
JVM name : OpenJDK 64-Bit Server VM
JVM version : 17.0.2+8
JVM info : mixed mode
OS name : Mac OS X
OS version : 12.3
こういうエラーがでていた。
ネットを探すと --add-opens
をつけろという記事が多いのだが、実はmock(Random.class, withSettings().withoutAnnotations())
とすれば良いようだ。
https://stackoverflow.com/questions/70993863/mockito-can-not-mock-random-in-java-17/70994194#70994194
問題が起きていた pyroscope-java については、PR を出して修正済み。
https://github.com/pyroscope-io/pyroscope-java/pull/26
spring-session-data-redis のデフォルト実装である RedisIndexedSessionRepository を利用すると以下の問題があります。
spring-security と併用した場合に TTL を設定していないキーが発生して、メモリがあふれる
spring-security を利用していなくてもメモリ仕様量が多い。
毎分0秒ごとにすべてのアプリケーション・サーバーからセッションデータ削除処理が走って、セルフ DDoS 状態になる(サーバー台数が多いときに顕著化する)。
これに対して、大規模サービス用のよりシンプルな実装として RedisSessionRepository が提供されている。これを用いれば、既存実装の問題点から開放されることができる。
検証
RedisIndexedSessionRepository
↓RedisIndexedSessionRepository で、ログインするまでに実行される redis のコードは以下のようになっている。一人がログインするだけなのに、めちゃくちゃ実行コマンドが多い。
1646916911.858574 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc2\xe7" "maxInactiveInterval" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b" "creationTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc2\xe7" "sessionAttr:SPRING_SECURITY_SAVED_REQUEST" "\xac\xed\x00\x05sr\x00Aorg.springframework.security.web.savedrequest.DefaultSavedRequest\x1e@HD\xf96d\x94\x02\x00\x0eI\x00\nserverPortL\x00\x0bcontextPatht\x00\x12Ljava/lang/String;L\x00\acookiest\x00\x15Ljava/util/ArrayList;L\x00\aheaderst\x00\x0fLjava/util/Map;L\x00\alocalesq\x00~\x00\x02L\x00\x06methodq\x00~\x00\x01L\x00\nparametersq\x00~\x00\x03L\x00\bpathInfoq\x00~\x00\x01L\x00\x0bqueryStringq\x00~\x00\x01L\x00\nrequestURIq\x00~\x00\x01L\x00\nrequestURLq\x00~\x00\x01L\x00\x06schemeq\x00~\x00\x01L\x00\nserverNameq\x00~\x00\x01L\x00\x0bservletPathq\x00~\x00\x01xp\x00\x00\x1f\x90t\x00\x00sr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xsr\x00\x11java.util.TreeMap\x0c\xc1\xf6>-%j\xe6\x03\x00\x01L\x00\ncomparatort\x00\x16Ljava/util/Comparator;xpsr\x00*java.lang.String$CaseInsensitiveComparatorw\x03\\}\\P\xe5\xce\x02\x00\x00xpw\x04\x00\x00\x00\x10t\x00\x06acceptsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x87text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9xt\x00\x0faccept-encodingsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x11gzip, deflate, brxt\x00\x0faccept-languagesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00#ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7xt\x00\rcache-controlsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\tmax-age=0xt\x00\nconnectionsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\nkeep-alivext\x00\x04hostsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0elocalhost:8080xt\x00\areferersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x1bhttp://localhost:8080/loginxt\x00\tsec-ch-uasq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00@\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"98\", \"Google Chrome\";v=\"98\"xt\x00\x10sec-ch-ua-mobilesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?0xt\x00\x12sec-ch-ua-platformsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\a\"macOS\"xt\x00\x0esec-fetch-destsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bdocumentxt\x00\x0esec-fetch-modesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bnavigatext\x00\x0esec-fetch-sitesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0bsame-originxt\x00\x0esec-fetch-usersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?1xt\x00\x19upgrade-insecure-requestssq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x011xt\x00\nuser-agentsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00yMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36xxsq\x00~\x00\x06\x00\x00\x00\x04w\x04\x00\x00\x00\x04sr\x00\x10java.util.Locale~\xf8\x11`\x9c0\xf9\xec\x03\x00\x06I\x00\bhashcodeL\x00\acountryq\x00~\x00\x01L\x00\nextensionsq\x00~\x00\x01L\x00\blanguageq\x00~\x00\x01L\x00\x06scriptq\x00~\x00\x01L\x00\avariantq\x00~\x00\x01xp\xff\xff\xff\xfft\x00\x02JPq\x00~\x00\x05t\x00\x02jaq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Aq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xfft\x00\x02USq\x00~\x00\x05t\x00\x02enq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Eq\x00~\x00\x05q\x00~\x00\x05xxt\x00\x03GETsq\x00~\x00\bpw\x04\x00\x00\x00\x00xppt\x00\x01/t\x00\x16http://localhost:8080/t\x00\x04httpt\x00\tlocalhostt\x00\x01/"
1646916911.860858 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.862692 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916911.864704 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" ""
1646916911.865652 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "1800000"
1646916911.866575 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "2100000"
1646916911.868053 [0 127.0.0.1:59916] "PUBLISH" "spring:session:event:0:created:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "\xac\xed\x00\x05sr\x00\x11java.util.HashMap\x05\a\xda\xc1\xc3\x16`\xd1\x03\x00\x02F\x00\nloadFactorI\x00\tthresholdxp?@\x00\x00\x00\x00\x00\x04w\b\x00\x00\x00\x04\x00\x00\x00\x00x"
1646916911.882439 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.887132 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc3\x0c" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" "\xac\xed\x00\x05sr\x006org.springframework.security.web.csrf.DefaultCsrfTokenZ\xef\xb7\xc8/\xa2\xfb\xd5\x02\x00\x03L\x00\nheaderNamet\x00\x12Ljava/lang/String;L\x00\rparameterNameq\x00~\x00\x01L\x00\x05tokenq\x00~\x00\x01xpt\x00\x0cX-CSRF-TOKENt\x00\x05_csrft\x00$31641e92-2d3d-486b-82e4-efcad96ee8fe"
1646916911.887987 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.888732 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916911.889398 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" ""
1646916911.890051 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "1800000"
1646916911.890638 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "2100000"
1646916911.891258 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916911.892703 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.624004 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.825021 [0 127.0.0.1:59916] "RENAME" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.825777 [0 127.0.0.1:59916] "RENAME" "spring:session:sessions:expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.829586 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xcd\xc4" "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION" "" "sessionAttr:SPRING_SECURITY_CONTEXT" "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$c332c966-2d8b-4cb9-a940-fcced7ba25d2psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" ""
1646916914.831427 [0 127.0.0.1:59916] "SADD" "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user" "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.832225 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.832900 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916914.833575 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" ""
1646916914.834209 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "1800000"
1646916914.834850 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "2100000"
1646916914.835507 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.836309 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "sessionAttr:SPRING_SECURITY_CONTEXT" "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$c332c966-2d8b-4cb9-a940-fcced7ba25d2psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user"
1646916914.837401 [0 127.0.0.1:59916] "SREM" "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user" "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.838195 [0 127.0.0.1:59916] "SADD" "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user" "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.838894 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.839500 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916914.840189 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" ""
1646916914.840861 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "1800000"
1646916914.841486 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "2100000"
1646916914.842081 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
1646916914.847443 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.856865 [0 127.0.0.1:59916] "HMSET" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xce\xa0" "sessionAttr:SPRING_SECURITY_SAVED_REQUEST" ""
1646916914.860546 [0 127.0.0.1:59916] "SADD" "spring:session:expirations:1646918760000" "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.861292 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:expirations:1646918760000" "2100000"
1646916914.861985 [0 127.0.0.1:59916] "APPEND" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" ""
1646916914.862693 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "1800000"
1646916914.864418 [0 127.0.0.1:59916] "PEXPIRE" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48" "2100000"
1646916914.864982 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.866710 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
1646916914.887045 [0 127.0.0.1:59916] "HGETALL" "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
↓ご存知のとおり、 TTL が設定されていないキーが一つある し、無駄が多そう。
このキーは、spring-security を有効化していると、設定されるようになる ようだ。
127.0.0.1:6379> eval "local t = {} local i = 1 for _,v in ipairs(redis.call('KEYS', '*')) do if redis.call('TTL',v) == -1 then t[i] = v i = i + 1 end end return t" 0
1) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user"
127.0.0.1:6379> keys *
1) "spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
2) "spring:session:expirations:1646918760000"
3) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user"
4) "spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
127.0.0.1:6379> hgetall spring:session:sessions:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48
1) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN"
2) ""
3) "sessionAttr:SPRING_SECURITY_CONTEXT"
4) "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$c332c966-2d8b-4cb9-a940-fcced7ba25d2psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user"
5) "sessionAttr:SPRING_SECURITY_SAVED_REQUEST"
6) ""
7) "creationTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xc2\xe7"
9) "maxInactiveInterval"
10) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
11) "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION"
12) ""
13) "lastAccessedTime"
14) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe5\xce\xa0"
127.0.0.1:6379> smembers spring:session:expirations:1646918760000
1) "\xac\xed\x00\x05t\x00,expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
2) "\xac\xed\x00\x05t\x00,expires:c332c966-2d8b-4cb9-a940-fcced7ba25d2"
127.0.0.1:6379> smembers spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user
1) "\xac\xed\x00\x05t\x00$44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48"
127.0.0.1:6379> get spring:session:sessions:expires:44b8a9c4-db68-4bbf-be7e-6f9dd5a01d48
""
ここから、1分間ごとに spring:session:expirations:1646918760000 をなめて消す処理が走る。しかし、ここには以下のような問題がある。
ロック処理などはないので、spring-session をロードしているすべてのサーバーで同じ処理が走って無駄であって、エコではない。
該当時間に全部のサーバーが落ちていれば、削除処理は動かない。
たとえば再起動しているタイミングがあると、タイミングによってリークするかもしれない。
ほとんどのデータは TTL が入っているので大丈夫。だが、TTL が入ってないキーがあるのでそこが問題だ。
中途半端に TTL 設定してるのがよくわからない。謎。
特定ユーザーのログインセッションを一括で消す機能があって便利だが、これもまぁ別に RedisSessionRepository をベースに自前で用意したほうが良いかもしれない。
RedisSessionRepository
RedisSessionRepository はよりシンプルな新しいクラスである。https://github.com/spring-projects/spring-session/issues/1278 の PR で導入されていて、複雑すぎる RedisIndexedSessionRepository に対する代替案として提示されているものである。今後はこっちをデフォルトにする計画もあるっぽいです。 https://github.com/spring-projects/spring-session/issues/1711
(SimpleRedisOperationsSessionRepositoryという名前から変更された)
↓RedisSessionRepository での実行結果。そこそこ多いが、だいぶマシである。
1646916589.099128 [0 127.0.0.1:59338] "HMSET" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6!" "maxInactiveInterval" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b" "sessionAttr:SPRING_SECURITY_SAVED_REQUEST" "\xac\xed\x00\x05sr\x00Aorg.springframework.security.web.savedrequest.DefaultSavedRequest\x1e@HD\xf96d\x94\x02\x00\x0eI\x00\nserverPortL\x00\x0bcontextPatht\x00\x12Ljava/lang/String;L\x00\acookiest\x00\x15Ljava/util/ArrayList;L\x00\aheaderst\x00\x0fLjava/util/Map;L\x00\alocalesq\x00~\x00\x02L\x00\x06methodq\x00~\x00\x01L\x00\nparametersq\x00~\x00\x03L\x00\bpathInfoq\x00~\x00\x01L\x00\x0bqueryStringq\x00~\x00\x01L\x00\nrequestURIq\x00~\x00\x01L\x00\nrequestURLq\x00~\x00\x01L\x00\x06schemeq\x00~\x00\x01L\x00\nserverNameq\x00~\x00\x01L\x00\x0bservletPathq\x00~\x00\x01xp\x00\x00\x1f\x90t\x00\x00sr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xsr\x00\x11java.util.TreeMap\x0c\xc1\xf6>-%j\xe6\x03\x00\x01L\x00\ncomparatort\x00\x16Ljava/util/Comparator;xpsr\x00*java.lang.String$CaseInsensitiveComparatorw\x03\\}\\P\xe5\xce\x02\x00\x00xpw\x04\x00\x00\x00\x10t\x00\x06acceptsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x87text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9xt\x00\x0faccept-encodingsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x11gzip, deflate, brxt\x00\x0faccept-languagesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00#ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7xt\x00\rcache-controlsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\tmax-age=0xt\x00\nconnectionsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\nkeep-alivext\x00\x04hostsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0elocalhost:8080xt\x00\areferersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x1bhttp://localhost:8080/loginxt\x00\tsec-ch-uasq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00@\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"98\", \"Google Chrome\";v=\"98\"xt\x00\x10sec-ch-ua-mobilesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?0xt\x00\x12sec-ch-ua-platformsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\a\"macOS\"xt\x00\x0esec-fetch-destsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bdocumentxt\x00\x0esec-fetch-modesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\bnavigatext\x00\x0esec-fetch-sitesq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x0bsame-originxt\x00\x0esec-fetch-usersq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x02?1xt\x00\x19upgrade-insecure-requestssq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00\x011xt\x00\nuser-agentsq\x00~\x00\x06\x00\x00\x00\x01w\x04\x00\x00\x00\x01t\x00yMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.109 Safari/537.36xxsq\x00~\x00\x06\x00\x00\x00\x04w\x04\x00\x00\x00\x04sr\x00\x10java.util.Locale~\xf8\x11`\x9c0\xf9\xec\x03\x00\x06I\x00\bhashcodeL\x00\acountryq\x00~\x00\x01L\x00\nextensionsq\x00~\x00\x01L\x00\blanguageq\x00~\x00\x01L\x00\x06scriptq\x00~\x00\x01L\x00\avariantq\x00~\x00\x01xp\xff\xff\xff\xfft\x00\x02JPq\x00~\x00\x05t\x00\x02jaq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Aq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xfft\x00\x02USq\x00~\x00\x05t\x00\x02enq\x00~\x00\x05q\x00~\x00\x05xsq\x00~\x00>\xff\xff\xff\xffq\x00~\x00\x05q\x00~\x00\x05q\x00~\x00Eq\x00~\x00\x05q\x00~\x00\x05xxt\x00\x03GETsq\x00~\x00\bpw\x04\x00\x00\x00\x00xppt\x00\x01/t\x00\x16http://localhost:8080/t\x00\x04httpt\x00\tlocalhostt\x00\x01/" "creationTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6!"
1646916589.102109 [0 127.0.0.1:59338] "PEXPIREAT" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "1646918389089"
1646916589.105447 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.116953 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.122376 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.125151 [0 127.0.0.1:59338] "HMSET" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6@" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" "\xac\xed\x00\x05sr\x006org.springframework.security.web.csrf.DefaultCsrfTokenZ\xef\xb7\xc8/\xa2\xfb\xd5\x02\x00\x03L\x00\nheaderNamet\x00\x12Ljava/lang/String;L\x00\rparameterNameq\x00~\x00\x01L\x00\x05tokenq\x00~\x00\x01xpt\x00\x0cX-CSRF-TOKENt\x00\x05_csrft\x00$49938ae3-2c5a-4343-a983-3b941ec5fe34"
1646916589.127423 [0 127.0.0.1:59338] "PEXPIREAT" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "1646918389120"
1646916589.129688 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.133727 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916589.135493 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916592.188724 [0 127.0.0.1:59338] "HGETALL" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916592.312255 [0 127.0.0.1:59338] "EXISTS" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90"
1646916592.313174 [0 127.0.0.1:59338] "RENAME" "spring:session:sessions:82b6e568-278f-41bf-8ac5-ca7861460d90" "spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0"
1646916592.314158 [0 127.0.0.1:59338] "HMSET" "spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0" "lastAccessedTime" "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xe2>" "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION" "" "sessionAttr:SPRING_SECURITY_CONTEXT" "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$82b6e568-278f-41bf-8ac5-ca7861460d90psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user" "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" ""
↓ ** TTL が設定されていないキーは存在しないし、1セッションにつき1つのハッシュのみがセットされていて、大きくデータ量を削減することができる。**
(デフォルトで30分の TTL っぽい)
127.0.0.1:6379> eval "local t = {} local i = 1 for _,v in ipairs(redis.call('KEYS', '*')) do if redis.call('TTL',v) == -1 then t[i] = v i = i + 1 end end return t" 0
(empty array)
127.0.0.1:6379> keys *
1) "spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0"
127.0.0.1:6379> type spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0
hash
127.0.0.1:6379> hgetall spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0
1) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN"
2) ""
3) "sessionAttr:SPRING_SECURITY_CONTEXT"
4) "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x020\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Oorg.springframework.security.authentication.UsernamePasswordAuthenticationToken\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\x0bcredentialst\x00\x12Ljava/lang/Object;L\x00\tprincipalq\x00~\x00\x04xr\x00Gorg.springframework.security.authentication.AbstractAuthenticationToken\xd3\xaa(~nGd\x0e\x02\x00\x03Z\x00\rauthenticatedL\x00\x0bauthoritiest\x00\x16Ljava/util/Collection;L\x00\adetailsq\x00~\x00\x04xp\x01sr\x00&java.util.Collections$UnmodifiableList\xfc\x0f%1\xb5\xec\x8e\x10\x02\x00\x01L\x00\x04listt\x00\x10Ljava/util/List;xr\x00,java.util.Collections$UnmodifiableCollection\x19B\x00\x80\xcb^\xf7\x1e\x02\x00\x01L\x00\x01cq\x00~\x00\x06xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x00w\x04\x00\x00\x00\x00xq\x00~\x00\rsr\x00Horg.springframework.security.web.authentication.WebAuthenticationDetails\x00\x00\x00\x00\x00\x00\x020\x02\x00\x02L\x00\rremoteAddresst\x00\x12Ljava/lang/String;L\x00\tsessionIdq\x00~\x00\x0fxpt\x00\x0f0:0:0:0:0:0:0:1t\x00$82b6e568-278f-41bf-8ac5-ca7861460d90psr\x002org.springframework.security.core.userdetails.User\x00\x00\x00\x00\x00\x00\x020\x02\x00\aZ\x00\x11accountNonExpiredZ\x00\x10accountNonLockedZ\x00\x15credentialsNonExpiredZ\x00\aenabledL\x00\x0bauthoritiest\x00\x0fLjava/util/Set;L\x00\bpasswordq\x00~\x00\x0fL\x00\busernameq\x00~\x00\x0fxp\x01\x01\x01\x01sr\x00%java.util.Collections$UnmodifiableSet\x80\x1d\x92\xd1\x8f\x9b\x80U\x02\x00\x00xq\x00~\x00\nsr\x00\x11java.util.TreeSet\xdd\x98P\x93\x95\xed\x87[\x03\x00\x00xpsr\x00Forg.springframework.security.core.userdetails.User$AuthorityComparator\x00\x00\x00\x00\x00\x00\x020\x02\x00\x00xpw\x04\x00\x00\x00\x00xpt\x00\x04user"
5) "sessionAttr:SPRING_SECURITY_SAVED_REQUEST"
6) ""
7) "creationTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xd6!"
9) "maxInactiveInterval"
10) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
11) "sessionAttr:SPRING_SECURITY_LAST_EXCEPTION"
12) ""
13) "lastAccessedTime"
14) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x7fs\xe0\xe2\xc6"
127.0.0.1:6379> ttl spring:session:sessions:a7133f51-f57f-4e05-80ca-ddfbbf2286d0
(integer) 1764
現在は、RedisSessionRepository は、デフォルトの実装ではないこともあってドキュメントが少ない。
https://spring.io/blog/2019/06/18/spring-session-corn-m2-and-spring-session-bean-sr6-released
https://github.com/spring-projects/spring-session/tree/main/spring-session-samples/spring-session-sample-boot-redis-simple
このあたりのドキュメントしかないのでこのへんを参照のこと。
↓のように設定すれば、RedisSessionRepository を利用できる。
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.data.redis.RedisSessionRepository;
@EnableSpringHttpSession
@Configuration(proxyBeanMethods = false)
public class MySessionConfiguration {
@Bean
public RedisOperations<String, Object> sessionRedisOperations(RedisConnectionFactory redisConnectionFactory) {
org.springframework.data.redis.core.RedisTemplate<String, Object> redisTemplate = new org.springframework.data.redis.core.RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean
public RedisSessionRepository redisSessionRepository(RedisOperations<String, Object> sessionRedisOperations) {
return new RedisSessionRepository(sessionRedisOperations);
}
}
See also
https://kagamihoge.hatenablog.com/entry/2018/05/19/163524
import jdk.jfr.consumer.RecordedClass
import jdk.jfr.consumer.RecordedEvent
import jdk.jfr.consumer.RecordingStream
import java.nio.ByteBuffer
import java.time.Duration
import kotlin.system.exitProcess
fun main(args: Array<String>) {
// OOME を発行させるスレッド
Thread {
Thread.sleep(1000)
val bbList = mutableListOf<ByteBuffer>()
while (true) {
try {
val bb = ByteBuffer.allocateDirect(Int.MAX_VALUE)
bbList.add(bb)
} catch (e: Error) {
println("Caught $e")
}
}
}.start()
// OOME があったら harakiri する。
RecordingStream().use { rs ->
rs.enable("jdk.JavaExceptionThrow").withPeriod(Duration.ofSeconds(1))
rs.onEvent("jdk.JavaExceptionThrow") { event: RecordedEvent ->
val message = event.getString("message")
val thrownClass = event.getValue<RecordedClass>("thrownClass")
if (thrownClass.name == "java.lang.OutOfMemoryError" && message.startsWith("Cannot reserve")) {
println("Caught OutOfMemoryError! $event")
exitProcess(4)
}
}
println("Starting RecordingStream")
rs.start()
}
}
Clock を mockito で @Spy しようとした場合などに、最近では制限が厳しくていじりづらくなっているが、、
Java 17 からは Clock の interface が切り出されて、InstantSource という名前になっている。Java 17 以後の場合は、実装コード内では InstantSource を利用するのが基本となっていくだろう。
https://bugs.openjdk.java.net/browse/JDK-8266846
ここまでできれば、あとは fluentd で storage に格納して、flamegraph 等を描画すれば良いだけである。
import jdk.jfr.consumer.RecordedEvent
import jdk.jfr.consumer.RecordingStream
import org.komamitsu.fluency.Fluency
import org.slf4j.LoggerFactory
import java.time.Duration
import java.time.ZoneId
class JfmonClient(
private val fluency: Fluency,
private val tag: String,
private val siteId: String,
private val instanceId: String
) : AutoCloseable {
private var rs: RecordingStream = RecordingStream()
private val logger = LoggerFactory.getLogger(JfmonClient::class.java)
fun start() {
this.rs.start()
}
fun startAsync() {
this.rs.startAsync()
}
override fun close() {
logger.info("Closing jfmon-client")
this.rs.close()
this.fluency.flush()
this.fluency.close()
}
fun flush() {
logger.info("Flushing jfmon-client")
this.fluency.flush()
}
/**
* jdk.SocketRead: [jdk.jfr.events.SocketReadEvent]
*/
fun enable(name: String, period: Duration?, threshold: Duration? = null, stackTrace: Boolean? = null) {
logger.info("Enabling {}(period={}, threshold={}, stackTrace={})", name, period, threshold, stackTrace)
val settings = rs.enable(name)
if (period != null) {
settings.withPeriod(period)
}
if (threshold != null) {
settings.withThreshold(threshold)
}
if (stackTrace != null) {
if (stackTrace) {
settings.withStackTrace()
} else {
settings.withoutStackTrace()
}
}
rs.onEvent(name, this::emitEvent)
}
private fun emitEvent(event: RecordedEvent) {
val data = buildMap(event)
if (logger.isDebugEnabled) {
logger.debug(
"{}: {} {}",
event.eventType.name,
event.startTime.atZone(ZoneId.of("Asia/Tokyo")),
data
)
}
fluency.emit(tag, data)
}
private fun buildMap(event: RecordedEvent): Map<String, Any?> {
return mapOf(
"siteId" to siteId,
"instanceId" to instanceId,
"type" to event.eventType.name,
"data" to buildData(event)
)
}
private fun buildData(event: RecordedEvent): Map<String, Any?> {
return event.fields.associate { dv ->
val value: Any? = when (dv.typeName) {
"boolean", "long", "int", "java.lang.String" -> event.getValue<Any>(dv.name)
"java.lang.Thread" -> event.getThread(dv.name).javaName
"jdk.types.StackTrace" -> {
val stackTrace = event.stackTrace
stackTrace?.frames?.map { frame ->
// 型が違うものを一つの配列に入れると、Elasticsearch が怒る。
listOf(
frame.method?.type?.name,
frame.method?.name?.toString(),
frame.lineNumber.toString(),
)
}
}
else -> null
}
dv.name to value
}
}
}
https://github.com/tokuhirom/jfrdemo
JFR のイベントってどんなんあるんかなーと思ってもドキュメントとかが見当たらないし、確認の方法がよくわからないわけだが、、
実データを見るのが一番良さそうなのだが、実データ見るのがめんどくさいので、ある程度データを見れるように整理したというわけ。
Linux を server, Mac を client として synergy を使おうとした。
[2019-03-13T22:16:56] DEBUG: can't get the active group, use the first group instead
とかなんとか言われて、うまく動かない場合。
MacOS 側に "U.S." キーボードレイアウトがないと日本語入力できないらしい!これはわからん!!
Settings の "Keyboard" -> "Input Sources" から U.S. を追加すればOK。Google IME だけ、とかにしてると使えないのだった。
hirose31 san におしえてもらって解決しました!!!
ref. https://members.symless.com/forums/topic/6176-keyboard-does-not-work-clientmacos-mojave-serverubuntu-1804/
品質向上のために Spark クエリのユニットテストを実施したいという場合、JVM 言語で開発している場合には、Spark/hive をライブラリとしてロードできるから、容易に実装することができる。
dependencies {
implementation 'org.apache.spark:spark-core_2.12:3.0.0'
implementation 'org.apache.spark:spark-sql_2.12:3.0.0'
}
のように、関連するモジュールを依存に追加する。
以下のような、テストに利用するデータを json 形式などで用意する(spark は CSV, TSV などの形式も利用可能だから、好きなものを使えばよい)
{"name": "Nick", "age":35, "extra_fields": "{\"interests\":[\"car\", \"golf\"]}"}
{"name": "John", "age":23}
{"name":"Cathy", "age":44, "extra_fields":"{\"interests\":[\"cooking\"]}"}
あとは、実際に spark session を作成し、local モードで spark を起動させれば良い。
import org.apache.spark.sql.Dataset
import org.apache.spark.sql.Row
import org.apache.spark.sql.SparkSession
// test without
class SimpleTest {
fun run() {
val spark: SparkSession = SparkSession
.builder()
.appName("Java Spark SQL basic example") // your application name
.config("spark.master", "local") // run on local machine, single thread.
.config("spark.ui.enabled", false)
.getOrCreate()
val resourcePath = javaClass.classLoader.getResource("test-data/people.json")!!.toString()
println("++++ read csv from: $resourcePath")
val df = spark.read()
.json(resourcePath)
df.show()
df.printSchema()
println("++++ create table")
df.createTempView("people")
println("++++ select")
val sqlDF: Dataset<Row> = spark.sql("SELECT * FROM people")
sqlDF.show(false)
println("++++ select 2nd")
val sqlDF2: Dataset<Row> = spark.sql("SELECT name, get_json_object(extra_fields, '$.interests') interests FROM people")
sqlDF2.show()
println("++++ select 3rd")
val sqlDF3: Dataset<Row> = spark.sql("SELECT avg(age) avg_age FROM people")
sqlDF3.show()
}
}
fun main() {
SimpleTest().run()
}
hive を使う場合
実際に動かすクエリが select * from your_db_name.your_table
のようにDB 名を指定していて、そのクエリ自体を変えずにテストしたいという場合には、hive サポートを有効にする必要がある。
hive を使う場合、spark-hive を依存に追加する。
dependencies {
implementation 'org.apache.spark:spark-core_2.12:3.0.0'
implementation 'org.apache.spark:spark-sql_2.12:3.0.0'
implementation 'org.apache.spark:spark-hive_2.12:3.0.0'
}
あとは以下のように DB を作って入れるだけ。
import org.apache.spark.sql.Dataset
import org.apache.spark.sql.Row
import org.apache.spark.sql.SaveMode
import org.apache.spark.sql.SparkSession
class TestClass {
fun run() {
val warehouseLocation = createTempDir()
println("++++ warehouseLocation=$warehouseLocation")
val spark: SparkSession = SparkSession
.builder()
.appName("Java Spark SQL basic example") // your application name
.config("spark.master", "local") // run on local machine, single thread.
.config("spark.sql.warehouse.dir", warehouseLocation.toString())
.config("spark.ui.enabled", false)
.enableHiveSupport()
.getOrCreate()
val resourcePath = javaClass.classLoader.getResource("test-data/people.json")!!.toString()
println("++++ read csv from: $resourcePath")
val df = spark.read()
.json(resourcePath)
df.show()
df.printSchema()
println("++++ create table")
spark.sql("create database if not exists foo")
df.write().mode(SaveMode.Overwrite).saveAsTable("foo.people")
spark.sql("show tables").show()
spark.sql("show create table foo.people").show(false)
// If the type of data is the important thing, you need to write your schema by yourself.
// spark.sql("""drop table if exists `foo`.`people`""")
// spark.sql("""
// CREATE TABLE `foo`.`people` (
// `name` STRING,
// `age` long,
// `extra_fields` STRING)
// USING parquet""".trimIndent())
// df.write().insertInto("foo.people")
println("++++ select")
val sqlDF: Dataset<Row> = spark.sql("SELECT * FROM foo.people")
sqlDF.show(false)
println("++++ select 2nd")
val sqlDF2: Dataset<Row> = spark.sql("SELECT name, get_json_object(extra_fields, '$.interests') interests FROM foo.people")
sqlDF2.show()
println("++++ select 3rd")
val sqlDF3: Dataset<Row> = spark.sql("SELECT avg(age) avg_age FROM foo.people")
sqlDF3.show()
}
}
fun main() {
TestClass().run()
}
クエリを変更しなくていいというメリットがある一方で、hive にアクセスするので依存も増えるし、実行もめちゃくちゃ遅くなります。
df.write().mode(SaveMode.Overwrite).saveAsTable("foo.people")
のようにすると、df 側の型をみていい感じにテーブル定義してくれて便利だが、明示的に create table したいときは以下のようにしたほうがいいかも。
spark.sql("""drop table if exists `foo`.`people`""")
spark.sql("""
CREATE TABLE `foo`.`people` (
`name` STRING,
`age` long,
`extra_fields` STRING)
USING parquet""".trimIndent())
df.write().insertInto("foo.people")
両者の比較
hive を利用しない場合、上記コードは 4.427 sec 程度で終わりますが、hive を利用する場合は 19.676 sec 程度かかるようになります。
プロダクションコードのテストをする場合はこの差はそこそこでかいかも。
sample code
https://github.com/tokuhirom/sparksql-unittest
-s, --silent
Silent or quiet mode. Don't show progress meter or error messages. Makes Curl mute.
で silence できるが、これを入れると、error message も抑制されてしまう。
-S, --show-error
When used with -s it makes curl show an error message if it fails.
-S を追加で入れると、エラーは stderr に出るようになるのでちょうどいい感じになる。
昔の gradle には dependency locking 機能がなかった。ビルドするタイミングによって、別の依存モジュールが利用されたりしていた。。
最近、gradle に dependency locking 機能がついたので試してみた。
carton.lock とか package-lock.json とか、そういうのと同じようなことができるようになる。
同じレポジトリからビルドしたら同じ jar が生成されるようになる。便利。
dependency locking を利用すると gradle.lockfile というファイルが生成される。
デフォルトだとフェーズ単位でファイルが生成されるから enableFeaturePreview('ONE_LOCKFILE_PER_PROJECT')
を settings.gradle に書いて1ファイルにまとめるようにしたほうが良い。gradle 7.0 ではこの方式がデフォルトになる予定なので、最初からこの feature flag は enabled にしたほうが良いです。管理上も、そのほうが便利。
./gradlew dependencies --write-locks
ってするとロックファイルが書かれる
./gradlew classes --update-locks org.apache.commons:commons-lang3,org.slf4j:slf4j-api
とかで特定のモジュールだけアップデートできる
たぶんもう普通に使えるけど、まだ開発途中って感じはする。./gradlew dependencies
してもサブプロジェクトのぶんを一括で作れない、とか。。
↓実際に line-bot-sdk-java を利用して試しに生成してみたやつがこれ。
https://github.com/tokuhirom/line-bot-sdk-java/commit/08a53ed86eedcf1072e7c12e77d7e1777f54c933
BeanPostProcessor で探せる。
import org.springframework.beans.factory.config.BeanPostProcessor
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import java.lang.reflect.Modifier
@Component
@Profile("!real")
class MyProcessor : BeanPostProcessor {
override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
val fields = bean.javaClass.declaredFields
val packageName = bean.javaClass.packageName
if ((!packageName.startsWith("org.springframework"))
&& fields.filter { !Modifier.isFinal(it.modifiers) }.count() > 0) {
println("$bean has mutable field.")
}
return bean;
}
}