障害報告: MysticDoll自宅kubernetesクラスタnode障害

id:MysticDoll です。

2024/02/11 に起きた自宅kubernetesクラスタのサービスが正常に利用出来なかった障害についての報告致します。

と思っていたんですが、長期間ほったらかしていてログを紛失したので雑なまとめです。

障害内容

一部podへのアクセスが失敗、またpodからの通信も一部失敗していた。

一時対応

調査の結果、失敗するpodは特定のnodeに乗っているものだと分かったので、そのnodeをcordonし、正常なcontrol-planeとnodeのみを残して再度スケジューリングしなおしてほぼ解決。

podからの通信失敗については kubectl exec 等で確認したところ名前解決に失敗していたため、node本体に入り、systemd-resolved systemd-networkdなどを再起動してgot事無き。

実際にはステークホルダーが自分しかいないので雑にnodeを再起動したりついでにArch Linuxのシステム更新をしたりとハチャメチャなことをしました。仕事では絶対こんなことしません。

node障害

一時対応の最中、再起動した bengal と名付けたnodeに疎通しなくなる事象が発生しました。

これは自宅kubernetesクラスタ達は(配線が面倒だったのとL2スイッチを持っていなかったので)無線で自宅ネットワークに接続しており、どうやら bengaliwd.service の起動に失敗していたことに起因するようです。

iwd 起動失敗の原因

結論から言うと iwd 自体の問題などではなくdbusのソケットをiwd (正確には iwdが依存している ell.so の内部関数) から見つけられていなかったことが原因でした。

さすがにシステム更新してもdbus周りの挙動が破壊される経験がなく、ドライバを入れ直したり古いバージョンへ各種パッケージをロールバックしたりなどしましたが全く無意味でした。

$ journalctl -xeu iwd.service などしてログを確認したところ以下のように出ていたので、普通にdbusを疑うべきだったのはそうかもしれない。

Feb 11 19:00:49 bengal iwd[330]: Wireless daemon version 2.13
Feb 11 19:00:49 bengal iwd[330]: Loaded configuration from /etc/iwd/main.conf
Feb 11 19:00:49 bengal iwd[330]: Failed to initialize D-Bus
Feb 11 19:00:49 bengal systemd[1]: iwd.service: Main process exited, code=exited, status=1/FAILURE
Feb 11 19:00:49 bengal systemd[1]: iwd.service: Failed with result 'exit-code'.
Feb 11 19:00:49 bengal systemd[1]: Failed to start Wireless service.

iwd 起動失敗の調査

とはいえ、適当にロールバックとかしてダメだった場合ちゃんと調査する必要があって、dbusを疑うにしても /run/dbus/system_bus_socket 自体は存在していたことや、dbusのパッケージバージョンを変えたりしてもダメだったので、コードを見に行くことにしました。

kernel.googlesource.com

Failed to initialize D-Bus というエラーを出している部分はここしかないので、ここを見に行くと l_dbus_new_default という関数の呼び出しで失敗していそうなことが分かります。

// https://kernel.googlesource.com/pub/scm/network/wireless/iwd/+/refs/tags/1.7/src/main.cより引用
    dbus = l_dbus_new_default(L_DBUS_SYSTEM_BUS);
    if (!dbus) {
        l_error("Failed to initialize D-Bus");
        goto failed_dbus;
    }

で、 l_dbus_new_default という関数は以下にありました。どうしてここに辿りついたのかは完全に忘れたのですが、たぶん l_dbus_new_default だけで検索したらこの ell のヘッダファイルのコードがヒットしたので、そこから辿ったのだと思います。

kernel.googlesource.com

蛇足ですが、筋の良い探し方としては、本体のソースにないということは共有ライブラリのコードという推測が立つので、 ldd をかけてそれらしいライブラリ名と共に探すのが良いかと思います。

[mysticdoll@bengal ~]$ ldd /lib/iwd/iwd
    linux-vdso.so.1 (0x00007ffda8d2d000)
    libell.so.0 => /usr/lib/libell.so.0 (0x000070f9a56b6000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x000070f9a5691000)
    libc.so.6 => /usr/lib/libc.so.6 (0x000070f9a54af000)
    /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x000070f9a5837000)

さておき、コードを見ると dbus = l_dbus_new_default(L_DBUS_SYSTEM_BUS); とあるので case L_DBUS_SYSTEM_BUS: の処理を見れば良さそうです。

// https://kernel.googlesource.com/pub/scm/libs/ell/ell/+/refs/heads/master/ell/dbus.c より引用
    case L_DBUS_SYSTEM_BUS:
        address = getenv("DBUS_SYSTEM_BUS_ADDRESS");
        if (!address)
            address = DEFAULT_SYSTEM_BUS_ADDRESS;
        break;

システムかユーザーセッションかどうかで参照すべきdbusソケットのアドレスを切り替える実装のようで、ここでiwdのunitファイルを見てみると

[mysticdoll@bengal ~]$ sudo systemctl cat iwd.service
# /usr/lib/systemd/system/iwd.service
[Unit]
Description=Wireless service
Documentation=man:iwd(8) man:iwd.config(5) man:iwd.network(5) man:iwd.ap(5)
After=network-pre.target
Before=network.target
Wants=network.target

[Service]
Type=dbus
BusName=net.connman.iwd
ExecStart=/usr/lib/iwd/iwd
NotifyAccess=main
LimitNPROC=1
Restart=on-failure
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
PrivateTmp=true
NoNewPrivileges=true
DevicePolicy=closed
DeviceAllow=/dev/rfkill rw
ProtectHome=yes
ProtectSystem=strict

unit内では環境変数 DBUS_SYSTEM_BUS_ADDRESS を指定していないので DEFAULT_SYSTEM_BUS_ADDRESS が採用されていそうと分かります。

kernel.googlesource.com

// https://kernel.googlesource.com/pub/scm/libs/ell/ell/+/refs/heads/master/ell/dbus.c より引用
#define DEFAULT_SYSTEM_BUS_ADDRESS "unix:path=/var/run/dbus/system_bus_socket"

とあるので、いやさすがにあるじゃろ…と思ったんですが、 なんと /var/run が 普通のディレクトリとして生えてました。その上本来あるはずのdbusのソケットは当然ありません。

[mysticdoll@bengal ~]$ sudo ls -la /var/run
total 12
drwx------ 1 root root        78 Feb 13 08:41 .
drwxr-xr-x 1 root root       116 Feb 12 17:27 ..
(snip)

というわけで、以下のようにunitファイルから直接 /run 側のdbusソケットを参照してもらうように環境変数を設定して無事iwdは起動しました。

(snip)
[Service]
Type=dbus
BusName=net.connman.iwd
Environment=DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket
(snip)

他にも docker-cri が動いてない(これもdocker.sockが/var/run にないから)とかの問題があったので一応良く使うところだけsymlinkを生やして応急対応して終了です。

後で /var/run/run へのsymlinkに直しておきます、気が向いたら…nodeのuncordonもしてないのでそのうちします、そのうち…

Rust FFIを活用してGBAのエミュレータの太陽センサーの値を制御する拡張機能を実装した

やったこと

GBAエミュレータ(visualboyadvance-m)上の太陽センサー*1の値をエミュレータ内threadに立てたHTTPサーバによって操作できるようにした。

github.com

今回はRust FFIを利用してできるだけ元のコードを汚さずに、かつHTTPサーバの処理等はRustの世界で閉じたものにする、という方向で実装していた。

ちなみにFFIを触っているが自分はRustはまあともかくC/C++は多少読めるけどちゃんと書くのは無理、ぐらいのC/C++の習熟度です。

Rust FFIを実装する上で必要な作業

正直、割と曖昧なまま触っている部分も多いが概ね以下の作業が必要。Ubuntu(apt)のmxe repoが古く、C++17サポートが不完全であるためWindows target向けのクロスコンパイルが壊れているので、それらをなんとかする作業も必要だったが、本筋ではないので割愛*2

  • C/C++
    • Rust側で触る想定のリソース(変数・関数)をRust側から見つけられるようにexposeする
    • Rustからexposeされている関数等のリソースをextern宣言し、呼び出したい箇所で使用するように実装する。
  • Rust 側
    • C/C++側のリソースの宣言
    • C/C++側で使ってもらう関数の実装、expose
    • staticlibとしてビルドする
  • ビルドツールたち
    • CMakeLists.txt を頑張って編集して、C/C++側のオブジェクトファイルとRust側のstaticlibをリンクする

C/C++側: Rust側で触る想定のリソース(変数・関数)をRust側から見つけられるようにexposeする

今回、Rust側では以下のリソースを触る想定となっている。

  • 太陽センサーの値の取得する関数
  • 太陽センサーの値を更新する関数

そのため、以下のように extern "C" を利用してCのABIで*3 関数をexposeしてあげる必要がある

extern "C" uint8_t systemGetSensorDarkness()
{
    return sensorDarkness;
}
// (snip)

extern "C" int level = 0;

extern "C" void systemUpdateSolarSensor()
{
    uint8_t sun = 0x0; //sun = 0xE8 - 0xE8 (case 0 and default)
    // このlevelはRust側にexposeして触りたい
    // int level = sunBars / 10;

systemUpdateSolarSensor 関数のローカル変数を extern してexposeしているのは、 systemUpdateSolarSensor 関数のシグネチャを変更する場合他の関数の呼び出し方を変更する必要があり、シグネチャを変更せずに済むようにRust側で level を編集してから呼び出すといった形で実装することにしたため。

その後、今回 extern 宣言に変更したリソースを使っている他の箇所で同様に extern "C" する形に変更していく必要がある。(staticextern ではメモリの確保のされ方(主にmangling周り)が変わるので別のアドレスが確保される可能性がありそうなので)

余談だが、最初は sensorDarkness という systemUpdateSolarSensor で更新している値を直接変更していたが、なぜか元の値に戻ってしまうのでこういった形で触るようにした。たぶん触ろうとしている関数が適切じゃないんだと思う。

C/C++側: Rustからexposeされている関数等のリソースをextern宣言し、呼び出したい箇所で使用するように実装する。

Rust側でexposeする関数は void start_server(void) なので以下のように宣言しておく。

// src/wx/wxvbam.cpp
extern "C" {
    void start_server(void);
}

そしてたぶん一回だけ呼ばれるであろう関数で呼び出すようにしておく。このあたりは適当なので本当はちゃんと考えたほうがいい。*4

// src/wx/wxvbam.cpp
bool wxvbamApp::OnInit() {
    start_server();

Rust側: C/C++側のリソースの宣言

Rustonomiconとか参考にしましょう。

doc.rust-lang.org

今回の場合は以下みたいな感じ、特に説明することはないですが、リンク時に undefined reference とか言われる場合は extern 宣言をミスってるとかそういうことだと思います。C++ ABIで単に extern するとおそらく普通にmanglingされるのでRust側から見つけられないです(たぶん)。

リンク時にミスってたらひたすらオブジェクトファイルをnm とかしてシンボル一覧と睨めっこしましょう。俺はした。

extern "C" {
    static mut level: i8;
    fn systemUpdateSolarSensor();
    fn systemGetSensorDarkness() -> u8;
}

Rust側: C/C++側で使ってもらう関数の実装、expose

単純に extern "C" で関数を宣言しましょう。 #[no_mangle] をつけてこちらもmanglingされないように気をつけて。

#[no_mangle]
extern "C" fn start_server() {

また、今回サーバ実装部分をaxumで実装しているので、axumのサーバ実行時にFutureをExecutorで実行してあげる必要があります。(単に実行するだけではFuture内の処理は実行されない)(と思う)

    std::thread::Builder::new()
        .name("solar_server".into())
        .spawn(|| {
            let rt = tokio::runtime::Runtime::new().unwrap();
            rt.block_on(async {
                axum::Server::bind(&"0.0.0.0:8000".parse().unwrap())
                    .serve(app.into_make_service())
                    .await
                    .unwrap();
            });
        });

単に tokio::runtime::Runtime::block_on を実行するとそこでthreadがブロックされるので、適当に std::thread::Builder::spawn してあげましょう。これで別Threadでサーバが動いてくれるはず。

CMakeLists.txt を頑張って編集して、C/C++側のオブジェクトファイルとRust側のstaticlibをリンクする

ここが一番分かっていないけどなんとかしました。適当に以下でいけたぜ、という程適当にはやっていないがあんま詳しくないぜ。

# src/wx/CmakeList.txt
target_link_libraries(visualboyadvance-m ${CMAKE_SOURCE_DIR}/solar-server/target/x86_64-pc-windows-gnu/release/libsolar_server.a userenv ntdll bcrypt)

やっていることとしては wx 内にある CMakeLists.txt で その中のプロジェクト(?)である visualboyadvance-m のオブジェクト達を今回作ったRustのstaticlibとリンクしてあげるという作業です。

userenv ntdll bcrypt あたりはRustのstaticlibが解決できなかったシンボルを解決するためにリンクしています。 GetUserProfileW みたいなWindows APIっぽい関数が見つけられていなかったので、そのあたりで必要そうなライブラリを探してきてリンクしたという形になります。

ビルド

export PATH="${PATH}:/PATH/TO/MXE/usr/bin"
mkdir build && cd build
/PATH/TO/MXE/usr/bin/x86_64-w64-mingw32.static-cmake .. -DCMAKE_RELEASE_TYPE=Release -G Ninja
ninja

やるだけ、リンクに失敗したらシンボルを見てコードを修正していくしかないので頑張りました。

*1:ボクらの太陽シリーズのカートリッジについているもの、内部的にはGPIOを触っているっぽい。コロコロカービィとかの傾きセンサーも似たような実装になっているらしい。なんでカートリッジにGPIOとセンサーが生えてるんだよ

*2:mxeのソースから必要なツールチェインの最新版をビルドして利用すればいいです。あるいはCIを参考にWindowsで開発環境を整えてください。

*3:C++のmanglingを回避して

*4:まあOnInitだし一回だけだろ…2回起動してもRust側でサーバがbindできなくてThreadが落ちるだけだし…

TypeScriptのGenerics Typeで一部の型引数のみを指定する

近況

自分はちゃんと勉強せず雰囲気でググりながらTypeScriptを使っているのに、人にTypeScriptを教えている状況。

Generic Type

TypeScriptにおいてはある型Tに対し、別の型Kを使って型を定義したい場合以下のように記述できる。

interface T<K> = {
  P1: K
};

type TNumber = T<number>; // { P1: number }

このとき型引数にはデフォルトとなる型を指定できる

interface T<K = number> {
  P1: K
};

課題

今回問題になったのが、Generic Typeにおいて複数の型引数を持ち、かつそれぞれの型引数にデフォルト型が設定されている型に対し、一部の型のみ指定するような型を定義したいというところ。

具体的には、express.jsにおいて、req.body.prop にアクセスする際、no-unsafe-member-access でESLintから怒られてしまう、これを回避したいという課題があった。

これ自体は DefinitlyTypedで定義されているように、req の型に Request<core.ParamDictionary, any, { prop: SomeType }>というように指定すれば回避できるが、これだけのために使いもしない core.ParamDictionary をimportするのは微妙では…?という感情が芽生えたのでなんとかしたい

github.com

app.get("/", (req: Request<core.ParamDictionary, any, { prop: SomeType }>, res: Response) => {
    const a: SomeType = req.body.prop;
});

(こういう感じで定義はできる)

Rustでは以下のように指定をスキップする型引数に _ を指定することで実現できるが、TypeScriptにはそういう機能は今はないらしい。

let a: SomeType<_, _, String> = ...;

(id:mizdra さんに聞いたところ、以下で議論はされているらしい)

github.com

と困っていたところ、上司のid:ma2sakaさんからなんか infer というのを使えばできるらしいと聞いたのでとりあえず調べてやってみた、という次第である。

infer

TypeScriptには Conditional Typeというのがあり、ある型 TK を継承している場合とそうでない場合に型を場合分けできるらしい。

type A<T> = T extends K ? B : C;

この際、KがGeneric Typeである場合には以下のようにinferを使うことで、型を推論して取り出すことが可能らしい。

type A<T> = T extends K<infer F, infer S> ? B<F, S> : C;

これを利用すると、express.Request<P, ResBody, ReqBody, ReqQuery, Locals> という型の ReqBody だけ指定したい場合以下の型を定義することで可能となる。

type CustomRequest<T> = Request extends Request<infer P, infer ResBody, infer ReqBody, infer ReqQuery, infer Locals> ? Request<P, ResBody, T, ReqQuery, Locals> : never;

Request という型は何も指定しなければデフォルト型が設定されるので infer で推論してもらえば各デフォルト型を取り出して使える、これを使ってデフォルト引数の指定を実質的に飛ばすことができる。

一応 ReqBody 以降の型引数も記述したが、 ReqBody 以降の型引数は省略できるので、実用上は以下みたいになるだろうか

type CustomRequest<ReqBody> = Reqeust extends Request<infer P, infer ResBody> ? Request<P, ResBody, ReqBody> : never;

正直型引数の指定を飛ばしたいだけなのにここまで必要なのはどうかとも思うが…そのあたりは今後の機能拡充に期待したい。

仮想端末にCTOを表示する

経緯

前職ではチームの先輩や隣のチームのリーダーのアイコンを色つきAAにして.bashrcで表示するようにしていたが、はてな入社後は特にそういったことをしておらず、まずいと思ったため勢いで実装した。

必要なもの

  • 色つきAAにして可視性が高いアイコンをしている同僚
    • 偶然にもCTOのアイコンがtodefyされていたので助かった
    • todefyされていると非常に楽だが、されてなくてもうまくやれば良い感じのAAにはできる
    • 最惡は自分で人のアイコンをtodefyしてください
  • ANSI escape code/SGR(Select Graphic Rendition) parameterの知識

スクリプト

motemenさんを表示したい人はこれでできます。たぶん。

echo -e '\x1b[48;2;195;213;227m \x1b[m\x1b[48;2;195;213;227m \x1b[m\x1b[48;2;252;255;246m \x1b[m\x1b[48;2;252;255;246m \x1b[m\x1b[48;2;19;58;137m \x1b[m\x1b[48;2;19;58;137m \x1b[m\x1b[48;2;157;167;185m \x1b[m\x1b[48;2;157;167;185m \x1b[m\n\x1b[48;2;129;82;38m \x1b[m\x1b[48;2;129;82;38m \x1b[m\x1b[48;2;255;224;211m \x1b[m\x1b[48;2;255;224;211m \x1b[m\x1b[48;2;248;199;182m \x1b[m\x1b[48;2;248;199;182m \x1b[m\x1b[48;2;218;46;32m \x1b[m\x1b[48;2;218;46;32m \x1b[m\n\x1b[48;2;164;172;193m \x1b[m\x1b[48;2;164;172;193m \x1b[m\x1b[48;2;254;196;184m \x1b[m\x1b[48;2;254;196;184m \x1b[m\x1b[48;2;255;179;162m \x1b[m\x1b[48;2;255;179;162m \x1b[m\x1b[48;2;225;107;93m \x1b[m\x1b[48;2;225;107;93m \x1b[m\n\x1b[48;2;174;179;183m \x1b[m\x1b[48;2;174;179;183m \x1b[m\x1b[48;2;55;62;68m \x1b[m\x1b[48;2;55;62;68m \x1b[m\x1b[48;2;83;32;13m \x1b[m\x1b[48;2;83;32;13m \x1b[m\x1b[48;2;89;90;95m \x1b[m\x1b[48;2;89;90;95m \x1b[m'

良く見ると SGRの指定 (空白1つ) デフォルトに戻すSGR としているが、SGRの指定 (空白2つ) デフォルトに戻すSGR とした方が短くて済む。このあたり片手間でやってたのでそんなに頭が働いていない

ANSI escape code/SGR

平たく言えば、仮想端末上でカラーリングや動的に変化する情報の表示(htopとかみたいな)などに使われている仕組み。

今回使うSGRの機能としては文字の背景色の切り替えとなる。

これは、\x1b[48;2;<r>;<g>;<b>m を仮想端末上でエンコードして出力すると、その後の文字の出力時に文字背景色が指定した r g b の値に対応する色となる。という機能である。

背景色の指定の例

これはもう1度SGRの機能で表示設定を変更しない限り続く、これを解除するには \x1b[m を仮想端末上でエンコードして出力する必要がある。*1

表示設定をデフォルトに戻す例

今回の場合偶然にも以下のリポジトリにCTOのアイコンの各色のカラーコードが記載されていたのでNode.jsで雑に生成することができた。改行と幅の調整だけ手作業で入力した。 github.com

const m  = [
   "c3d5e3", "fcfff6", "133a89", "9da7b9",
   "815226", "ffe0d3", "f8c7b6", "da2e20",
   "a4acc1", "fec4b8", "ffb3a2", "e16b5d",
   "aeb3b7", "373e44", "53200d", "595a5f"
 ];
 m.map(c => c.match(/\w{2}/g).map(c => Number.parseInt(c, 16))).map(([r,g,b]) => `\x1b[48;2;${r};${g};${b}m \x1b[m`).map(c => `${c}${c}`).join('');

簡単なやり方

画像をカラーつきAAに変換するツールでHTMLを生成→CLIブラウザ等で表示→その出力をリダイレクトしてテキストファイルに保存。

そうするとSGRも含めて良い感じのテキストが得られるので、後は頑張れば良い感じになる。この場合 \x1b[38;2;<r>;<g>;<b>m での文字色の変更も含まれていたりするので先ほどの手作業よりも表現力の高いAAが仮想端末上で表示できる。

さすがにtodefyアイコンよりは手間がかかるが…

前職ではそういう感じで頑張っていた。

まとめ

現職同僚各位で「俺のアイコンを仮想端末に表示してくれ」という人がいたら連絡ください。

現在の様子(PS1で表示するようにした)

追記: PS1で表示したい人向け(大きいサイズ)

export PS1="\\n\\e[48;2;195;213;227m        \\e[m\\e[48;2;252;255;246m        \\e[m\\e[48;2;19;58;137m        \\e[m\\e[48;2;157;167;185m        \\e[m\\n\\e[48;2;195;213;227m        \\e[m\\e[48;2;252;255;246m        \\e[m\\e[48;2;19;58;137m        \\e[m\\e[48;2;157;167;185m        \\e[m\\n\\e[48;2;195;213;227m        \\e[m\\e[48;2;252;255;246m        \\e[m\\e[48;2;19;58;137m        \\e[m\\e[48;2;157;167;185m        \\e[m\\n\\e[48;2;195;213;227m        \\e[m\\e[48;2;252;255;246m        \\e[m\\e[48;2;19;58;137m        \\e[m\\e[48;2;157;167;185m        \\e[m\\n\\e[48;2;129;82;38m        \\e[m\\e[48;2;255;224;211m        \\e[m\\e[48;2;248;199;182m        \\e[m\\e[48;2;218;46;32m        \\e[m\\n\\e[48;2;129;82;38m        \\e[m\\e[48;2;255;224;211m        \\e[m\\e[48;2;248;199;182m        \\e[m\\e[48;2;218;46;32m        \\e[m\\n\\e[48;2;129;82;38m        \\e[m\\e[48;2;255;224;211m        \\e[m\\e[48;2;248;199;182m        \\e[m\\e[48;2;218;46;32m        \\e[m\\n\\e[48;2;129;82;38m        \\e[m\\e[48;2;255;224;211m        \\e[m\\e[48;2;248;199;182m        \\e[m\\e[48;2;218;46;32m        \\e[m\\n\\e[48;2;164;172;193m        \\e[m\\e[48;2;254;196;184m        \\e[m\\e[48;2;255;179;162m        \\e[m\\e[48;2;225;107;93m        \\e[m\\n\\e[48;2;164;172;193m        \\e[m\\e[48;2;254;196;184m        \\e[m\\e[48;2;255;179;162m        \\e[m\\e[48;2;225;107;93m        \\e[m\\n\\e[48;2;164;172;193m        \\e[m\\e[48;2;254;196;184m        \\e[m\\e[48;2;255;179;162m        \\e[m\\e[48;2;225;107;93m        \\e[m\\n\\e[48;2;164;172;193m        \\e[m\\e[48;2;254;196;184m        \\e[m\\e[48;2;255;179;162m        \\e[m\\e[48;2;225;107;93m        \\e[m\\n\\e[48;2;174;179;183m        \\e[m\\e[48;2;55;62;68m        \\e[m\\e[48;2;83;32;13m        \\e[m\\e[48;2;89;90;95m        \\e[m\\n\\e[48;2;174;179;183m        \\e[m\\e[48;2;55;62;68m        \\e[m\\e[48;2;83;32;13m        \\e[m\\e[48;2;89;90;95m        \\e[m\\n\\e[48;2;174;179;183m        \\e[m\\e[48;2;55;62;68m        \\e[m\\e[48;2;83;32;13m        \\e[m\\e[48;2;89;90;95m        \\e[m\\n\\e[48;2;174;179;183m        \\e[m\\e[48;2;55;62;68m        \\e[m\\e[48;2;83;32;13m        \\e[m\\e[48;2;89;90;95m        \\e[m \\n\\n:\\w \\$ "

*1:これは文字表示の設定をデフォルトに戻すSGRの指定

motemen15パズル

about motemen

美顔器 (@motemen) / Twitter

inspired by

airreader.hatenablog.com

15パズルの実装

https://ja.wikipedia.org/wiki/15%25E3%2583%2591%25E3%2582%25BA%25E3%2583%25ABja.wikipedia.org

15パズルの機能を実装するにあたり、考えるべき要素はサイズ16の配列と入れ替え操作の2つのみである。

サイズ16の配列

これは盤面そのものに対応する。

0を空のパネルとして以下の配列があるとする

[0, 1 , 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

これを盤面として表現する場合4要素ずつに区切り、縦に並べてやれば良い

[
  [ 0,  1,  2,  3],
  [ 4,  5,  6,  7],
  [ 8,  9, 10, 11],
  [12, 13, 14, 15]
]

制約付き入れ替え操作

入れ替え操作は単に選択したインデックス n に対し、空のパネルの位置と入れ替える操作を考えれば良い。

ただし、15パズルには、空のパネルと隣接するパネルのみ移動ができる、という物理的制約がある。 そのためこの制約を定義してやる必要がある。

隣接判定

空のパネルのインデックスをe とし、あるインデックス n が隣接しているかどうかを判定するためには以下の式が利用できる

abs(e-n) == 1 || abs(e - n) == 4

式の前半はパネルが横方向に隣接しているかの判定、後半はパネルが縦方向に隣接しているかの判定である。

(追記) これだけでは空パネルが端にある場合、次の行の最初の要素と入れ替えられてしまうので、次の判定が追加で必要となる。

(e % 4 == 0 && e - n !== 1) || (e % 4 == 3 && n - e !== 1)

つまり最終的な判定式は以下となる。

((e % 4 == 0 && e - n != 1) || (e % 4 == 3 && n - e != 1)) && abs(e-n) ==1 || abs(e-n) == 4

実物

https://mysticdoll.com/motemen15puzzle

実際に動いている様子はここで確認できる。

美しい色の並びですね

実際の実装

React.jsで雑に実装した。

github.com

まず以下のようなmotemenを表すデータを定義しておく

// motemen.js
export const white = "fcfff6";
export const motemen = [
  "c3d5e3", "fcfff6", "133a89", "9da7b9",
  "815226", "ffe0d3", "f8c7b6", "da2e20",
  "a4acc1", "fec4b8", "ffb3a2", "e16b5d",
  "aeb3b7", "373e44", "53200d", "595a5f"
];

そして実際に15パズルそのものを表現するReact Componentは以下のような形となる。

/// Board.jsx
import React from 'react';
import { white, motemen } from './motemen'

export default class Board extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            motemenArray: this.props.motemenRandomized
        };
    }

    sliceMotemen() {
        return [
            this.state.motemenArray.slice(0, 4),
            this.state.motemenArray.slice(4, 8),
            this.state.motemenArray.slice(8, 12),
            this.state.motemenArray.slice(12, 16),
        ];
    }

    swap(n) {
        const w = this.whiteIndex();
        if (!((w % 4 === 3 && n - w === 1) || (w % 4 === 0 && w - n === 1))) {
            const sub = Math.abs(w - n);
            if (sub === 1 || sub === 4) {
                const motemenArray = this.state.motemenArray.slice();
                motemenArray[w] = this.state.motemenArray[n];
                motemenArray[n] = this.state.motemenArray[w];
                this.setState({
                    motemenArray
                });

                if(JSON.stringify(motemen) === JSON.stringify(motemenArray)) {
                    alert('You know motemen.');
                }
            }
        }
    }

    whiteIndex() {
        return this.state.motemenArray.findIndex(c => c === white)
    }

    render() {
        return (
            this.sliceMotemen().map((m, r) => {
                    return (
                        <p style={{ lineHeight: "0px", margin: "0px" }} key={r}>
                            {
                                m.map((color, index)=> {
                                    const style = {
                                        height: "128px",
                                        width: "128px",
                                        background: `#${color}`,
                                        content: " ",
                                        display: "inline-block"
                                    };
                                    const onClick = () => this.swap(index + r * 4);
                                    return (
                                        <span key={color} style={style} onClick={onClick}/>
                                    )
                                })
                            }
                        </p>
                    )
            })
        )       
    }
}

あとはランダムにシャッフルした配列でBoardを初期化してやれば完成となる。

return (
    <div className="App">
      <main>
        <Board motemenRandomized={motemenRandomized}/>
      </main>
    </div>
  );

Rustのtokio::select! macroの使い方を今更理解した初心者

経緯

mysticdoll.hatenablog.jp

これで使ってるtc2vvというアプリケーションが、Twitch側のチャットのWebSocketのハンドラのスレッドが死んでいるっぽいがプロセス自体は生きている半死状態だったのを直したかった。

github.com

結論から言うと、 tokio::join! macroを使っていたが、これは指定した Future (正確には async expression) が全て終了するまで待つという処理をするらしく、チャットのハンドラのThreadとWebSocketをホストするサーバのハンドラのスレッドの両方を待っていたのでちゃんと死んでくれなかった。今回はこれをちゃんと殺すために tokio::select! を使った。

なんで殺したいか

一言で言えばkubernetes上で動いているから、になる。

kubernetesがPodの死活監視をして勝手にreplica数を調整してくれるが、対象のPodのプロセスが半死状態だとそれが叶わない。なのでしっかりと死んでもらう必要があるわけである。

本来は ReadinessProbe や LivenessProbe のようなProbeを設定すべきではあるのだが、WebSocketの死活状態のProbeをどうやってとるのかすぐに思いつかなかったのと、別にステートレスなものではあるのでしっかり死んでもらって再起動してもらった方が健全な気がしたのでしっかり殺すことにした。

tokio::select!

自分は JavaScriptのPromiseの方で例えると理解しやすい気がしたのだが、tokio::join! は どちらかというと Promise.all に近く、tokio::select!Promise.race に近いのかなと思った。

というかドキュメントをちゃんと読んでいなかったということが普通に判明した。

tokio::join!

Waits on multiple concurrent branches, returning when all branches complete.

→ 全部の並列処理が終わるまで待つよ

tokio::select!

Waits on multiple concurrent branches, returning when the first branch completes, cancelling the remaining branches.

→ 平行処理の内最初のものが終わったら他をキャンセルするよ。

読もうね。

今後

とりあえずこれでチャットかWebSocketサーバのどちらかのThreadが予期せず死んだ場合にちゃんとプロセス毎死んでくれるようになったので、kubernetesがAuto healingしてくれるだろうと信じている。

一番厄介なのは実は死んでいなくてWebSocket接続が生きているパターンで、そうだった場合はチャットのハンドラをまた弄らないといけなさそうな気がする…

そういう感じだったらもう通知用のchannel生やしてそこから親Threadからプロセスを殺すことになりそうかな…

ConfigMap/Secretを監視してリソースを再起動してくれる stakater/Reloader を自宅kubernetesに導入した

stakater/Reloader

github.com

通常 kubernetesにおいてはConfigMapやSecretが更新された際、それに紐付いたリソース(deployment/daemonset/statefulset等)はそれを検知して再起動するという機能はない。

これを提供するのが、 stakater/Reloader である。

Install

マニフェストをそのままapplyすることで導入できる。

kubectl apply -f https://raw.githubusercontent.com/stakater/Reloader/master/deployments/kubernetes/reloader.yaml

この他にもHelm ChartやKustomizeでのインストールも可能らしい、詳しくはリポジトリのREADMEを参照してほしい。

Usage

指定したConfigMap/Secretの変更を検知してローリングアップデートを実行する

<configmap|secret>.realoader.stakater.com/reload: "<comma-separated-names>" という形式のannotationをDeployment等に追加するだけで良い。

関連するConfigMap/Secretの変更を検知してローリングアップデートを実行する

また、 reloader.stakater.com/auto: "true" を設定しておけば、そのリソースに紐付いたConfigMap/Secretの更新時に自動で関係するpodをローリングアップデートしてくれるらしい。

この時の対象となるConfigMap/Secretの探索をannotationで指定したものに限定したい場合は、 reloader.stakater.com/search: "true" を設定することで実現できる。

この際ローリングアップデートを発火するConfigMap/Secretには reloader.stakater.com/match: "true" のannotationを指定しておく必要がある。

自宅内運用

自宅では、基本的にはローリングアップデートをトリガーしたいConfigMap/Secretを指定する形でannotationを設定する形で採用した。

これは、意図しないタイミングでのローリングアップデートの実行が怖いのと、ConfigMap/Secretの手動更新時には既に手オペが発生しているので自動更新する必要もないだろうという算段がある。

特に、今回必要となった要因は、OAuthの古いtokenがexpireした際にSecretを更新する方法としてOAuthの砲台からAccess TokenをSecretに保存するpodがいるので、こいつのSecret保存時に手オペするのが面倒という理由だけなので、必要以上に影響範囲を増やさないで運用しようという結論である。

所感

kubernetes内に自身(k8s)内部のConfigMap/Secretを変更しうるサービスが存在しうる場合には非常に助かるツールだとは感じた。

一方でとりあえず入れておいたら便利!という形で導入するにはオーバーパワーな感じもする。

kubernetes内でConfigMap/Secretを変更するようなシステムを運用していて、かつその変更に追従して更新したいリソースがある各位は試してみてはいかがだろうか。(いるのか?)