Blog

Perl のワンライナーで複数行置換したい

BEGIN{undef $/;} を入れると良い。

perl -i -pe 'BEGIN{undef $/;} s/START.*STOP/replace_string/smg' file_to_change

参考: https://stackoverflow.com/questions/1030787/multiline-search-replace-with-perl

krangl で unpivot(melt) する方法

R の dplyr みたいなデータ操作ができる kotlin ライブラリである krangl でデータの unpivot をしてみる。

    val df = dataFrameOf(
        "product", "error", "warn"
    )(
        "CMS", 400, 343,
        "Admin", 534, 834,
    )

    df.gather("product", "n", listOf("error", "warn"))
        .print()

krangl はドキュメントがあんま検索に引っかからないが、dplyr でできることはだいたいできるので、krangl でどうしたらいいんだろう。。ってなったときは dplyr でどうしたらいいんだろう?という気持ちでググって見るのが良さそう。

ref. https://holgerbrandl.github.io/kotlin4ds_kotlin_night_frankfurt//emerging_kotlin_ds_ecosystem.html#1

Dell U4021QW というウルトラワイドモニターを買った

モニター3枚ほど並べていたのだが配線がぐちゃぐちゃすぎてうんざりしたので、ウルトラワイドモニター一枚にしてみた。 かなりポートがたくさんついているので、これに接続するだけでほとんどの接続が集約されていて、快適です。USB ハブ機能があるのが便利。デスクが広く使える。

Image from Gyazo

色々つないでたんですが、いまは↑のあたりをつないでいます

not stripped の linux バイナリは strip したら小さくなる

↓のように、非常に大きい実行ファイルがあったとする。

-rwxr-xr-x 1 tokuhirom tokuhirom 30M Jun 23 00:37 /tmp/thrift-linux-x86_64*

こういう場合には file コマンドを実行すると良い。

> file /thrift-linux-x86_64
thrift-linux-x86_64: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=f12655e32dd035a15ec1b40b181c8bcc76c55e4e, with debug_info, not stripped

ここで気になるのは not stripped というところである。これは、シンボルテーブルがついていることを表している。シンボルテーブルは、デバッガでデバッグするときには便利だが、そういう用途が想定されない場合には不要である。

> strip thrift-linux-x86_64
> ls -lah thrift-linux-x86_64
-rwxr-xr-x 1 tokuhirom tokuhirom 3.7M Jun 23 00:42 thrift-linux-x86_64*

strip することで、30MB → 3.7MB と 8割程度の容量が削減される。

ref.

山善の昇降式デスクを買った

昇降式デスクというと、Flexispot が有名なのだが、Amazon で買えて一人でも古いデスクの回収もやってくれて、新しいデスクの組み立ても代行してくれるものという条件で探したらこれになった。 そこまで天板にこだわりないので、板を自分で選ぶとかめんどくさいなっていう感じ。

Android の Launcher は、AIO Launcher がライフチェンジングだった件

Android は Launcher を切り替えて、全く違うルックアンドフィールを体験できるのが強みの一つだと思う。

色々な Launcher を試してきたが、昨日見つけた AIO Launcher ってやつが最高便利なので紹介したい。

イメージとしては、iOS の左側の画面みたいな widget list だけのランチャーというのが近いです。その中にアプリリストウィジェットを埋めて使う感じ。

Notification が home の中にそのまま表示されるというのが新鮮。Notification いちいち開くの、めんどくさかったんですよね。 ウィジェットをホームのウィンドウごとにうまいこと並べる、みたいなのも必要なくなって、スーッと縦にスワイプしていけば、一通りのウィジェットを閲覧できて、今日の分のブリーフィング終わり。って感じ。

Image from Gyazo

Image from Gyazo

Image from Gyazo

あと、ちょっとわかりにくいんだけど、AIO Launcher 自体の設定はフローティングな Search ボタンを長押しすればOK.

ウィジェットは Android widget ももちろん使えるんだけど(多分、有料版 feature だけど)、独自のウィジェット機構があって、lua で拡張できるっぽい。まだ試してないけど。

なお、これ多分日本語には対応してないです。

あと、Galaxy Z fold 3 みたいなフォルダブルの端末で画面を閉じたり開いたりしても画面が崩れないのもいいです。

コレはほんとにオススメです。

armeria-grpc で io.grpc.StatusRuntimeException: UNKNOWN になるとき

サーバー側がエラーを返しているが、その理由を返していないときにこのエラーになる。

内部コンポーネント同士の通信の場合、サーバー側での問題を propagate してしまうことも可能。

armeria-grpc なら -Dcom.linecorp.armeria.verboseResponses=true すればよい。 https://armeria.dev/docs/server-grpc/#exception-propagation

GINQ - Groovy-Integrated Query

Groovy でデータを処理する DSL

https://docs.groovy-lang.org/docs/next/html/documentation/#_ginq_a_k_a_groovy_integrated_query

def numbers = [0, 1, 2]
assert [0, 1, 2] == GQ {
    from n in numbers
    select n
}.toList()

のように書ける。

yabai と skhd を最近は利用している

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 を左から順番に切り替えるという設定にすることにより、ウィンドウの切り替えが速くなった。

java.util.Random が mockito でモックできないとき

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 に含まれる RedisSessionRepository を利用して redis の容量を節約する

spring-session-data-redis のデフォルト実装である RedisIndexedSessionRepository を利用すると以下の問題があります。

これに対して、大規模サービス用のよりシンプルな実装として 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 をなめて消す処理が走る。しかし、ここには以下のような問題がある。

特定ユーザーのログインセッションを一括で消す機能があって便利だが、これもまぁ別に 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

JFR Event Streaming を利用して、ByteBuffer で OutOfMemoryError が起きたときに harakiri する。

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()
    }
}

new Locale("id", "ID").toString() の結果がバージョンによって違う

import java.util.Locale;

public class Foo {
  public static void main(String[] args) {
    System.out.println(new Locale("id", "ID").toString());
  }
}

Java 17 だと id_ID となるけど、Java 11 だと in_ID となる。

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Locale.html#legacy_language_codes

https://github.com/openjdk/jdk/commit/a4c46e1e4f4f2f05c8002b2af683a390fc46b424

Java 17 以後は Clock の interface として InstantSource が定義されている

Clock を mockito で @Spy しようとした場合などに、最近では制限が厳しくていじりづらくなっているが、、

Java 17 からは Clock の interface が切り出されて、InstantSource という名前になっている。Java 17 以後の場合は、実装コード内では InstantSource を利用するのが基本となっていくだろう。

https://bugs.openjdk.java.net/browse/JDK-8266846

protoc の aarch64 対応は 3.17.3 から。

> Could not resolve all files for configuration ':protobufToolsLocator_protoc'.
   > Could not find protoc-3.15.0-osx-aarch_64.exe (com.google.protobuf:protoc:3.15.0).
     Searched in the following locations:
         https://中略/com/google/protobuf/protoc/3.15.0/protoc-3.15.0-osx-aarch_64.exe

というようなエラーが出るときがある。

この対応は 3.17.3 で入っている。 https://github.com/protocolbuffers/protobuf/pull/8557 https://search.maven.org/artifact/com.google.protobuf/protoc/3.17.3/pom

一旦、x86_64 用のバイナリをダウンロードさせるようになったようだ。

kover - coverage agents for kotlin

https://github.com/Kotlin/kotlinx-kover

kover は kotlin 用のカバレッジ収集ツール。jacoco でも収集は可能だが、以下のような違いがある。

Kitten's game をやっていた

いわゆる放置ゲーなのだが、userjs による自動化をしつつ、なんだかんだ furthest ring に到達するまでやった。challenge も全部やったし、まぁやりきったかなという感じ。 Achievement も 21 個集めきった。

ContainerCreating で k8s が止まってるとき

https://serverfault.com/questions/728727/kubernetes-stuck-on-containercreating

kubectl describe pods したら原因がわかるようだ。

k8s の設定とアプリケーションコードのレポジトリは分けるべきか?

CI のサイクルが違うので、分けておいたほうが開発しやすいようだ。 アプリケーションコードをいじるたびにベースイメージのビルドをしていると時間がかかる。

JFR の event stream を Fluency で fluentd に送る

ここまでできれば、あとは 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
        }
    }
}