unnest を使って楽に bulk insert する

プライベートでよく PostgreSQL を使っている人の MySQL 不便シリーズ。

ActiveRecord のような高機能な ORM あるいはクエリビルダを使っていると bulk insert するクエリをライブラリがいいかんじに組み立ててくれるが、そんな高級なライブラリを使わずにもっとシンプルなもので済ませたいことがある。 そんなときに bulk insert をしたくなった場合、クエリを組み立てて変数を bind して…… というのを自分でやるのは結構面倒である。

PostgreSQL だと配列型があり unnest() で配列を行に変換できるので、これと insert into select を組み合わせると bulk insert するクエリを簡単に組み立てられる。

eagletmt=> select unnest(array[1,2,3]) as x, unnest(array[4,5,6]) as y;
 x | y
---+---
 1 | 4
 2 | 5
 3 | 6
(3 rows)

PostgreSQL だとこんなかんじで行に展開できるので、あとは insert into bulk_inserts (x, y) unnest($1), unnest($2) みたいなクエリに配列型の値を bind すれば bulk insert になる。たとえば Rust の sqlx だとこれだけ。

#[derive(Debug)]
struct Record {
    x: i32,
    y: i32,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let records: Vec<_> = (0..1000)
        .map(|i| Record {
            x: 2 * i,
            y: 2 * i + 1,
        })
        .collect();

    let pool = sqlx::PgPool::connect("postgres://...").await?;

    let mut xs = Vec::with_capacity(records.len());
    let mut ys = Vec::with_capacity(records.len());
    for r in records {
        xs.push(r.x);
        ys.push(r.y);
    }
    sqlx::query("insert into bulk_inserts (x, y) select unnest($1), unnest($2)")
        .bind(&xs)
        .bind(&ys)
        .execute(&pool)
        .await?;

    Ok(())
}

この方法だと動的に SQL を組み立てる必要がなくて楽。なお大量のレコードをロードするなら copy を使ったほうがいいと思う。

tokio::runtime::Handle でスレッドを跨ぐ

tokio::runtime::Handle を最近まで知らなくて無駄なコードを書いてたのでメモ。

Handle とは意味的には Runtime への参照になっている。
https://docs.rs/tokio/1.7.1/tokio/runtime/struct.Handle.html

Handle のドキュメントにもあるように、これを使うと現在のスレッドの Runtime への参照を取り出して別スレッドで同じ Runtime を使い回すことができる。

#[tokio::main]
async fn main() {
    let handle = tokio::runtime::Handle::current();
    std::thread::spawn(move || handle.block_on(run_in_child_thread()))
        .join()
        .unwrap();
}

async fn run_in_child_thread() {
    todo!()
}

同様に tokio の非同期なコードと rayon のようなスレッドによるデータ並列なコードを組み合わせるのも Handle を使えば簡単にできる。

use rayon::prelude::*;

#[tokio::main]
async fn main() {
    let handle = tokio::runtime::Handle::current();

    [1, 2, 3].into_par_iter().for_each(|i| {
        handle.block_on(async {
            cpu_intensive_func1(i);
            tokio::time::sleep(tokio::time::Duration::from_secs(i)).await;
            cpu_intensive_func2(i);
            println!("Finished {}", i);
        });
    });
}

パスワードマネージャーをストレージとした envchain みたいなやつ

envchain とは macOS の keychain または LinuxD-Bus secret service に API キーのような秘密の情報を安全に保存し、環境変数として取り出せる CLI ツールである。
https://github.com/sorah/envchain
僕は envchain の大ファンで、当初 macOS 向けに書かれていた envchain に Linux サポートを加えたりしていた。

そんな envchain をずっと愛用していたのだけど、Windows を使うようになってから WSL2 内で使いにくいことが気になっていた。 Linux 向けには D-Bus secret service (ほぼ実質 gnome-keyring) を使っているため、WSL2 内で D-Bus を起動したり X サーバを起動したりする必要がある。 無理ではないんだけどやりにくい。

そこでパスワードマネージャをストレージとした envchain のようなものを書いて使ってみている。 具体的には Bitwarden (私用) と 1Password (社用) にそれぞれ書いた。どちらのサービスも保存されたデータにアクセスできる CLI を提供しているので、それをラップしたようなツールになっている。

Bitwarden

bw という CLI を提供しているので、これをラップした envwarden というのを書いて使っている。
https://github.com/eagletmt/misc/tree/master/rust/envwarden
envwarden は Bitwarden に Secure Notes として保存された値を bw を使って取り出してそれを環境変数としてセットしているだけである。

bw は一度ログインすると暗号化された状態でデータがローカルファイルシステムに保存された状態になり、bw unlock で BW_SESSION を得てその BW_SESSION を使うと暗号化されたデータを読み出せるようになる、というかんじになっている。 なので envwarden を起動するたびに毎回ネットワークアクセスが発生するわけではない。 したがって後述の 1Password 版と比べて高速に起動できるようになっている。 bw 自体が Node.js を埋め込んだ? バイナリになっているため envchain と比べると起動はやや遅くなってしまっているが、ストレスを感じるほどではない。

1Password

op という CLI を提供しているので、これをラップした envop というのを書いて使っている。
https://github.com/eagletmt/envop
envwarden 同様、1Password の Secure Notes として保存された値を op を使って取り出してそれを環境変数としてセットしている。

bw と大きく違うのは、一度認証して得たセッションが一定時間で切れてしまう点である。op signin ${account} すると OP_SESSION_${account} を得てその OP_SESSION_${account} を使うと 1Password からデータを読み出せるのだが、30分経つと再度 op signin して OP_SESSION_${account} を更新しなければならない。 このため envwarden と同じような素朴なラッパーにすると非常に使いづらいため、ssh-agent のように裏側でずっと起動しているデーモン envop-agent を立てておき、envop コマンドは envop-agent と通信してデータを取り出すようにすることで、OP_SESSION_${account} の更新を envop-agent に任せるような形にした。 先日 ssh-agent の実装を調べていた のはそのような envop-agent を書くためだった。 これにより tmux 等で複数のシェルを起動していても頻繁にサインインし直すことを抑えている。 しかし op がデータを取り出すのが非常に遅くストレスを感じるレベルなので、WSL2 のような特殊な環境でない限りイマイチかもしれない……

そんな envop を書いていたら 1Password から Secrets Automation なるものが発表されていた。
https://support.1password.com/secrets-automation/
まだよく理解できてないけど envop のような用途で使えるものではない気がする……? 気にはなるので後日ちゃんと読んでおきたい。

ssh-agent のしくみ

ssh-agent のように daemon として起動し秘密の情報を保持しつつ別プロセスと通信するようなプログラムを書きたくて、ssh-agent はどう実装しているのかざっくり調べた。
https://github.com/openssh/openssh-portable

通信方法

これは普通に ssh-agent を使っていてもすぐ気付くことだけど、ssh-agent は UNIX domain socket を使って通信している。 eval $(ssh-agent) のように実行すると SSH_AUTH_SOCK と SSH_AGENT_PID の2つの環境変数がセットされ、SSH_AUTH_SOCK は UNIX domain socket のパスを、SSH_AGENT_PID は daemon 化した ssh-agent の pid を指している。 SSH_AUTH_SOCK は /tmp/ssh-*/agent.${parent_pid} というパスになっている。parent_pid は daemon 化する前の pid。/tmp/ssh-* も /tmp/ssh-*/agent.${parent_pid} もオーナー以外アクセスできないようなパーミッションになっている。 このへんは /tmp 以下にできるだけ安全にプライベートなファイルを作るときの一般的な方法をそのままやっているかんじ。
https://github.com/openssh/openssh-portable/blob/V_8_5_P1/ssh-agent.c#L1517-L1543

SSH_AGENT_PID は ssh-agent -k のときに利用される。 sshSSH_AUTH_SOCK が定義されていればそれを使って ssh-agent に通信しにいって、通信内容のプロトコルは独自。この部分は各プログラムが独自に決めればよいことなのであまりよく読んでない。
https://github.com/openssh/openssh-portable/blob/V_8_5_P1/ssh-agent.c#L961-L1061

daemon

daemon 化はだいたい一般的な流れ。fork して setsid して chdir して dup2 で stdin、stdout、stderr を /dev/null に向ける。
https://github.com/openssh/openssh-portable/blob/V_8_5_P1/ssh-agent.c#L1560-L1596

秘密の情報を漏らしにくく

ssh-agent は秘密鍵の情報をメモリ上に持っておく必要があるため、できるだけメモリ情報が漏れないようにする工夫が見られた。

自分で同じように秘密の情報を保持するプログラムを書くときはこのへんを真似しておけばよさそう。 通信のプロトコルを gRPC にして実装したサンプルコード https://github.com/eagletmt/misc/tree/master/rust/agent-proto

ドーナドーナ いっしょにわるいことをしよう 感想 (ネタバレなし)

「稼いだ金を力に変えるハルウラレ系RPG」だそうです https://www.alicesoft.com/dohnadohna/ 。エロゲです。 一企業に支配されるディストピア的な街で主人公たちがヒトカリしたりハルウリしたりしながらその企業を倒そうとする物語。 たまたまこれをプレイしている配信を見て、絵が好みだし話は面白そうだしゲームとしても面白いかもと思って買った。 実際ハルウリパートもヒトカリパートも面白くてとても満足でした。人におすすめしたいレベル。

ユニークヒロインのイベント全部回収、全メインヒロインのフィーリングレベル10達成まではやった。エンディングの分岐は一部だけ。

ストーリー

全体的にテンポが良くてよかった。まぁゲーム紹介を見ての通りすごく感動するようなタイプの話ではないけど、常にバカゲー的な楽しさがあったし、その上で全体的なストーリーラインはしっかりしていて違和感はそんなに無かったと思う。

全部がエロシーンなわけではないけど、6人のメインヒロインに対して各フィーリングレベルにイベントを用意してあるの単純にボリュームがすごいと思う。魚介先生の仕事量もすごい。 ただ、エンディングの分岐はヒロイン毎に差分が小さいわりに周回が面倒 (フィーリング値の調整はいいんだけど、何度もラスボスマップ + ラスボス戦やるのが……) だったので、一部しかやってない。 ポルノ、菊千代、キラキラが好きです。

ヒトカリパー

さすがにペルソナや世界樹のような RPG と比べると雑だけど、RPG の楽しさがしっかり抑えられていて RPG の好みが二分しやすい自分にとっても十分楽しい側に入る出来だった。 ただのポチポチゲーではないけど、すごく考えて進めなければならないわけでもない、気軽にやるにはちょうどいいかんじ。 近接は主人公と菊千代がド安定、遠距離からアンテナがバフと全体攻撃、残り1枠はポルノでデバフかけたりキラキラでデバフ + 範囲攻撃だったりメディコで回復させたり。 アリスは使い所が難しいけど時々役に立つ。ザッパは優秀な壁。虎太郎とジョーカーは……

バフ・デバフの影響がわりと強い調整に感じた。とくにボス戦ではアイテムでバフ・デバフを入れると楽。TEC の高さの効果はよく分からん。 MP を使いきっても MP 値をマイナスにしながらスキルを使えて、でも一定以上マイナスになると段階的にデバフがかかっていくシステムは面白かったと思う。まぁ結局雑魚戦相手では MP がマイナスになっても大した影響はなく、ボス戦前には回復させておくというかんじで安定してしまったけど。

ハルウリパート

序盤はヒトカリで手に入るジンザイがしょぼいので初期キャラのメンタルをアイテムで維持していかに長持ちさせるかというゲームだったけど、アイテムやジンザイ枠が揃ってくる中盤以降は「有利な属性を手に入れてほぼメンタル変動しないエース枠」と「不利な属性付与相手にぶつけたり適当に使い潰したりする使い捨て枠」にはっきり分けるのが正解だと思った。 有利属性無しで全員のメンタルを維持するのつらいし、ピルを手に入れるのが地味に面倒 or 運ゲーだし…… こう言うと倫理観が終わってるけど、このゲームには倫理観を崩壊させて向き合うべき (?)。 最終的なエース枠はこんなかんじになった。とくに一番左はハルウリさせてもルックスもメンタルも一切下がらないスーパーエースとなった…… なのでメンタルをカンストさせてるのは完全におまけ。

ユニークヒロインについては高橋菜々実だけ条件を満たすのがやたら面倒だった (+ 自分の趣味とはとくに合わなかった) けど、それ以外は簡単だったのでまぁちょうどいいんじゃないだろうか。 個人的な好みは清水千晴です。


というわけでエロゲと RPG が好きな人にはおすすめです。FANZA や DLsite でダウンロード販売もあります。最後になぜかスクリーンショットをとっていた一場面を載せておきます。

f:id:eagletmt:20210119022000j:plain

API Gateway + Lambda + Rust で開発する (2021-01)

まとめ

この構成で Slack の interactive message や block kit で遊んだサンプルがこれ https://github.com/eagletmt/misc/tree/master/rust/slack-slash-command-sample

Rust 向けの Lambda Runtime

lambda-runtime という準(?)公式の crate がある https://github.com/awslabs/aws-lambda-rust-runtime が、リリースが滞っている。 現在リリースされている中での最新版では async/await の対応すら入っておらず、現在の Rust では正直使い物にならないレベルである。 master には async/await の対応が入ってるのでそれを使うという手もあるが、痺れを切らした Netlify の方が未マージの PR も一部取り込みつつ netlify_lambda という crate でリリースしている https://github.com/awslabs/aws-lambda-rust-runtime/issues/274 。 この netlify_lambda には v0.2 で Tokio v1.0 対応も入っている。 なので現時点では netlify_lambda を利用するのが一番手軽な上にコミュニティの恩恵を受けやすいと思われる。

Lambda の Docker イメージサポート

最初にこのリリースを見たときは zip が Docker イメージに変わっただけくらいの認識だったが、公式に提供されている public.ecr.aws/lambda/provided:al2 等のイメージには aws-lambda-rie というバイナリが含まれており、手元で Lambda を起動するのが手軽になっている点が非常に価値が高い。 https://docs.aws.amazon.com/lambda/latest/dg/images-create.html

これをベースイメージとして Docker イメージを作ることで、手元で docker run -p 9000:8080 my-awesome-app で起動すると curl -XPOST http://localhost:9000/2015-03-31/functions/function/invocations -d '{}' で Lambda のハンドラを動かすことができる https://docs.aws.amazon.com/lambda/latest/dg/images-test.html

これまでも Lambda にアップロードする zip を作るときにはどうせ Docker を使ってビルドしていたわけだし、開発やデプロイのことだけを考えれば Lambda の Docker イメージサポートを利用しない理由は無い気がする。 AWS CDK も既にこの機能をサポートしている。

手元での API Gateway 開発

API Gateway + Lambda で API サーバを作るときにも aws-lambda-rie のおかげで curlAPI Gatewayペイロードを流せば手元で動作確認ができる。 しかしその API Gateway は別のマイクロサービスだったりフロントエンドの JS だったりから呼ばれて使われることが多く、それらとの連携も合わせて手元で動作確認したい。 そこで普通の HTTP リクエストを API Gateway 用のペイロードに変換して aws-lambda-rie のエンドポイントに流すだけの小さいツール https://github.com/eagletmt/aws-lambda-rie-gateway を書いてみたところ、だいぶ便利になった。

cargo watch --shell 'docker build -t handler:dev . && docker run -p 9000:8080 handler:dev を起動しておけばコードを変更するたびに Docker イメージがリビルドされ、普通の HTTP リクエストでアクセスできる状態で開発できる。 普通の HTTP リクエストを処理できるので Slack アプリ開発のようなときには ngrok のプロキシ先にも指定できる。 aws-lambda-rie-gateway はとりあえず自分が必要になった範囲しかサポートしてないけど、API Gateway v1 (REST API) のサポートも入れたりしていくと結構実用的なツールになるかもしれない。 Lambda の Docker イメージサポートと aws-lambda-rie に感謝。

CHUNITHM の手元動画を撮る

チュウニズムと真剣に向き合いたいので手軽に手元動画を撮る方法を調べたらスマホホルダーに磁石をつけて筐体上部の鉄板につけて撮るのがよさそうに思ったので自分もやってみた。タイトーステーションのようなゲーセンだと手元撮影用にスマホアームスタンド的なものを備え付けてくれていたりするんだけど、アームの調整がかなり面倒という問題があったので別の方法を探していた。

とりあえずプロトタイプ的に https://www.daisonet.com/product/4549131357974 を買って足の部分を外して https://www.daisonet.com/product/4549131452815 をセロテープで貼りつけたのがこれ。

f:id:eagletmt:20210108004207j:plain

めっちゃ雑だけどこれでも結構よく撮れた。鉄板のわりと奥めにくっつけるのがポイント (この動画はちょっと手前すぎた)。

これでも十分満足な手元撮影ができるようになったけどスマートフォンにそこそこ強い磁石を近づけた状態になるのが心配なので、もうちょいホルダーと磁石を離して固定したい。あるいは使い捨てるつもりで安いカメラを買ってもよさそうだけど、現代のスマートフォン並の性能を持つ安くて小さいカメラとかあるんだろうか。