MySQL でも楽に bulk insert したい

以前 PostgreSQL で unnest を使って楽に bulk insert する方法を紹介した https://eagletmt.hateblo.jp/entry/2021/09/01/030453 。 これの MySQL 版が欲しかったものの array 型が存在しない MySQL では無理かなと思っていたんだけど、MySQL 8.0 からサポートされた JSON 型なら array を表現できて json_table() という関数を使うと達成できそうなことにふと気付いた。

json_table() を使うと JSON の値からテーブル (行) に変換することができる。つまりこれを使えば unnest に近いことが可能になる。
https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html

MySQL [(none)]> select * from json_table('[{"x": 1, "y": 2}, {"x": 3, "y": 4}]', '$[*]' columns (x integer path '$.x', y integer path '$.y')) as t;
+------+------+
| x    | y    |
+------+------+
|    1 |    2 |
|    3 |    4 |
+------+------+
2 rows in set (0.001 sec)

文法がなかなか独特だけど1つの JSON の値 (配列) から行に変換できている。なのでプログラム側は JSONシリアライズさえできれば insert into select で bulk insert が可能になる。 PostgreSQL 版と同様に Rust の sqlx を使った例はこんなかんじになる。

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

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

    let pool = sqlx::MySqlPool::connect("mysql://...").await?;

    let query = "insert into bulk_inserts (x, y) select * from json_table(?, '$[*]' columns (x integer path '$.x', y integer path '$.y')) as t";
    sqlx::query(query)
        .bind(sqlx::types::Json(records))
        .execute(&pool)
        .await?;
    Ok(())
}

がんばってプレースホルダを組み立てる必要がなくなって便利。

sqlx crate でフィールド毎に独自のデコード処理を挟みたい

sqlx-with というものを作ってみたので、その経緯について書く。

github.com

sqlx crate でフィールド毎に独自のデコード処理を挟みたい

sqlx では query_as() 等の関数を使うことでデータベースから取り出した行を struct にマッピングすることができ、このマッピングsqlx::FromRow という derive macro を利用することで自動で実装できる。

#[derive(sqlx::FromRow)]
struct Row {
    x: i64,
}

このような単純なマッピングではなく、特定のカラムの値に独自の変換処理を入れてから struct に入れたいとする。とくに意味は無いが、たとえばカラム x の値に対して (x, x + 2) という値を struct のフィールド x に入れたいとする。serde であればたとえば split_x という関数を定義して #[serde(deserialize_with = "split_x")] と指定すれば実現できる。これと同じような機能が sqlx::FromRow にあればよさそうだが、現状は無い。なので serde の deserialize_with みたいなものをどうやったら実装できるか考えてみる。最終的には以下のように書けるとよさそうだ。

#[derive(sqlx::FromRow)]
struct Row {
    #[sqlx(decode = "split_x")]
    x: (i64, i64),
}

まず sqlx::FromRow が具体的にどのようなマクロ展開をしているのか実装を見てみると、Row、ColumnIndex、Type、Decode といった様々な trait を使っていることが分かる。
https://github.com/launchbadge/sqlx/blob/v0.6.1/sqlx-macros/src/derives/row.rs
最初の例は以下のような impl を生成している。

struct Row {
    x: i64,
}
impl<'r, R> sqlx::FromRow<'r, R> for Row
where
    R: sqlx::Row,
    &'r str: sqlx::ColumnIndex<R>,
    i64: sqlx::Type<R::Database> + sqlx::Decode<'r, R::Database>,
{
    fn from_row(row: &'r R) -> sqlx::Result<Self> {
        Ok(Self {
            x: row.try_get("x")?,
        })
    }
}

特定のデータベースに依存しないようになっているのが特徴で、struct のフィールド名とその型の情報から

  • &str で特定のカラムにアクセスできる
  • i64 に対応するデータベース側の型がありデコードができる

という制約をつけている。

ここに split_x という関数を挟んでみると以下のような実装になるだろう。

struct Row {
    x: (i64, i64),
}
impl<'r, R> sqlx::FromRow<'r, R> for Row
where
    R: sqlx::Row,
    &'r str: sqlx::ColumnIndex<R>,
    i64: sqlx::Type<R::Database> + sqlx::Decode<'r, R::Database>,
{
    fn from_row(row: &'r R) -> sqlx::Result<Self> {
        Ok(Self {
            x: split_x("x", row)?,
        })
    }
}

fn split_x<'r, R>(index: &'r str, row: &'r R) -> sqlx::Result<(i64, i64)>
where
    R: sqlx::Row,
    &'r str: sqlx::ColumnIndex<R>,
    i64: sqlx::Type<R::Database> + sqlx::Decode<'r, R::Database>,
{
    let n: i64 = row.try_get(index)?;
    Ok((n, n + 2))
}

あとはこのように展開されるようなマクロを作るだけ…… とはいかず、このままだと無理なことが分かる。Row の定義から impl の制約を生成しなければならないが、i64 が Type + Decode であるという条件を Row の定義から知ることができないからである。これを知るには split_x のシグネチャを知る必要があるが derive macro には不可能なはず。というわけで sqlx::FromRow ではこのような decode オプションを実装することができない。

sqlx-with

sqlx::FromRow の derive macro で decode オプションを実装できない原因は実装があまりに generic だからだと考えた。sqlx::FromRow を derive macro を使わずに手書きで実装する場合、普通は特定のデータベースを前提に実装する。実際に sqlx::FromRow のドキュメント https://docs.rs/sqlx/latest/sqlx/trait.FromRow.html#manual-implementation でもそのように案内している。 そこで具体的なデータベースを渡せるような derive macro に変えれば、decode オプションを実装できるだけの generic さを残しつつ sqlx::FromRow と同様の便利さを得られそうだ。実際にそのように sqlx_with::FromRow という derive macro を実装してみた。
https://github.com/eagletmt/sqlx-with

use sqlx::Connection as _;

#[derive(sqlx_with::FromRow)]
#[sqlx_with(db = "sqlx::Sqlite")]
struct Row {
    #[sqlx_with(decode = "split_x")]
    x: (i64, i64),
    y: String,
}

fn split_x(index: &str, row: &sqlx::sqlite::SqliteRow) -> sqlx::Result<(i64, i64)> {
    use sqlx::Row as _;
    let n: i64 = row.try_get(index)?;
    Ok((n, n + 2))
}

#[tokio::main]
async fn main() {
    let mut conn = sqlx::SqliteConnection::connect(":memory:").await.unwrap();
    let row: Row = sqlx::query_as("select 10 as x, 'hello' as y")
        .fetch_one(&mut conn)
        .await
        .unwrap();
    assert_eq!(row.x, (10, 12));
    assert_eq!(row.y, "hello");
}

db というオプションにデータベースを渡すことでそのデータベースを前提とした impl を生成でき、split_x のような関数も中で使えるようになった。sqlx::FromRow と異なり指定したデータベース以外には使えなくなってしまうが、現実的に1つの struct を複数の異なる種類のデータベース間で使いまわしたい場面はまず無いだろう。

おまけ: darling

sqlx_with::FromRow という derive macro を実装するために darling というライブラリを初めて使った。proc macro を実装するための derive macro を提供していて、derive macro を実装するには FromDeriveInput を使うと必要な syn の値にすぐにアクセスできる struct が得られて便利だった。

C のヘッダファイルの解析に bindgen を濫用するアイデア

Rust には bindgen というツール、あるいはライブラリがあって binding を書くときに非常に重宝する。
https://rust-lang.github.io/rust-bindgen/
これを濫用すると C のヘッダファイルを解析してマクロの定数値を読み取ったり構造体のサイズを調べたりといったことが手軽に可能そう。

たとえば errno の名前と値とメッセージの一覧を知りたいとき、こんなかんじで errno.h の中身から定義を取り出すことができた。

lazy_static::lazy_static! {
    static ref MACROS: std::sync::Mutex<std::cell::RefCell<Vec<(String, i32)>>> = Default::default();
}

#[derive(Debug, Default)]
struct MacroCollector {}

impl bindgen::callbacks::ParseCallbacks for MacroCollector {
    fn int_macro(&self, name: &str, value: i64) -> Option<bindgen::callbacks::IntKind> {
        if name.starts_with('E') {
            MACROS
                .lock()
                .unwrap()
                .borrow_mut()
                .push((name.to_owned(), value as i32));
        }
        None
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    use std::io::Write as _;

    bindgen::builder()
        .header_contents("wrapper.h", "#include <errno.h>")
        .parse_callbacks(Box::new(MacroCollector::default()))
        .generate()
        .expect("unable to collect macros");

    let out_dir = std::env::var("OUT_DIR")?;
    let mut file = std::fs::File::create(std::path::Path::new(&out_dir).join("errno.rs"))?;
    writeln!(file, "pub const ERROR_NUMBERS: &[(&str, i32, &str)] = &[")?;
    for (name, value) in MACROS.lock()?.borrow().iter() {
        let message = unsafe { std::ffi::CStr::from_ptr(libc::strerror(*value)).to_str()? };
        writeln!(file, "(\"{}\", {}, \"{}\"),", name, value, message)?;
    }
    writeln!(file, "];")?;
    Ok(())
}

当然こういう用途に使うものではないのでグローバル変数に結果を書き込むことになっているけど、build.rs 内ならまぁ許されるだろう。 本格的に解析するなら bindgen と同様に libclang を直接使ったほうがよさそうだけど、手軽に定数やシグネチャを拾うくらいなら bindgen を濫用するのが楽そう。

Axum を使ってみた感想

Axum は Tokio のチームから発表された Web アプリケーション向けのフレームワークです。
https://tokio.rs/blog/2021-07-announcing-axum
これを使って少し Web アプリを書いてみたりしたのでその感想を書いてみます。

Rust にもいくつか Web フレームワークがあって個人的には https://github.com/flosse/rust-web-framework-comparison#high-level-server-frameworks の表がしっくりきているんですが、async/await に対応していてハイパフォーマンスで書きやすいものといえば Actix Web が一番かなという状況でした。 ただ、Actix Web は HTTP のレイヤに hyper を利用しておらず独自の実装になっていて、Tokio や hyper をベースにしたライブラリが多い中でちょっと使いづらい面がありました。 そのためか最新の安定版でも Tokio v1 対応が済んでおらず、最近の Actix Web は実質的に beta 版を使うしかない状況になっています。 hyper ベースのフレームワークとしては warp も最近人気ですが、高レイヤの便利機能が不足していたり filter の概念が独特で型が複雑になりがちだったりで、個人的には Actix Web と比べると使いやすさはかなり落ちるという印象です。

そんな状況でリリースされた Axum は Actix Web にかなり似ている使い勝手を提供していて、一番の印象は Actix Web の対抗になれそうというものでした。 Tokio チームが開発しているので当然 Tokio をベースにしていて、hyper、tower、tracing といったデファクトとなりつつあるスタックの上に Axum は作られています。 gRPC サーバでは既に Tonic がデファクトになっていて、それと似たスタックの上に Web サーバを実装できるところも Actix Web より使いやすそうなところです。

実際に ISUCON11 の予選問題の初期実装を Actix Web 4 (beta) から Axum 0.4 に書き直してみたのがこれです https://github.com/isucon/isucon11-qualify/compare/main...eagletmt:rust-axum 。 ルーティングに関しては ISUCON11 予選では Actix Web のマクロを使って書いていたので一見差分が大きいですが、Actix Web にもマクロを使わない書き方があって、それと比べると Axum はかなり似ていると思います。 エラーの表現は Actix Web の ResponseError トレイトの代わりに IntoResponse トレイトを実装するようなかんじになっていて、ハンドラからエラーを返すときは Actix Web と同じように Result で返せばいいだけです。 ハンドラの書き方はリクエストの情報や共有データを extractor 経由で取り出して引数で受け取るという形になっていて、これも Actix Web と非常に似ています。Actix Web では app_data() で DB のコネクションプールのような共有データを登録して web::Data で受け取っていたのに対して、Axum では AddExtension layer として共有データを登録して extract::Extension で受け取るようになっていますし、パス内のパラメータの参照や JSON ボディのデシリアライズやクエリ文字列の取り出しなども同じように extractor 経由で行えます。 actix-session の CookieSession に相当するような便利なものはまだ無さそうだったのでそこは自作しました https://github.com/eagletmt/tower-cookie-store 。FromRequest トレイトを実装すればいいという点も非常に似ていますね。 ちなみにパフォーマンスについては少なくとも ISUCON11 予選問題の初期状態においては Axum のほうが若干上でした。

というかんじで、Actix Web と同じように使えて Actix Web の不満が一通り解消されているので、個人的には今後は Web フレームワークの第一候補として使っていこうと思えました。 まだリリースされたばかりで破壊的な変更もよくある https://github.com/tokio-rs/axum/blob/main/axum/CHANGELOG.md フェーズですが、今後にかなり期待できます。

2021年の思い出

しゃかいじんはちねんめ

去年は https://eagletmt.hateblo.jp/entry/2020/12/31/114259

仕事

去年は SRE グループを2つに分けてみたが、結果的にこれはうまくいかずに再度1つの SRE グループという形になった。失敗の原因はいくつか考えているが、ここは振り返りや反省の場ではないので書かないことにする。1つになった SRE グループで再びリード業をやりつつ、個人としては引き続きレガシーを倒していた。具体的にはレガシーとされていた VPC を消したり、同時に CentOS も消滅したり、Consul を廃止したり、これらの廃止のための諸々の移行作業といったかんじのことを主にやっていた。

今年はオフィスの移転が大きな変化だった。全く近場ではないみなとみらいへの移転となり、通勤にかかる時間が劇的に伸びた。 これと同時にリモートワークではなくオフィスにくるようにというメッセージが強化され、自分も週に1、2回オフィスに行っている。多くの通勤者とは逆方向の移動になるので電車に座れないとかはなく通勤に苦痛は無いんだけど、単純に時間がかかるので自分の時間を維持しようとすると勤務時間が足りなくなるという状態になっている。 かといって自分自身はリモートワーク縮小には否定的ではなくやや肯定よりのニュートラルな気持ちでいる。完全な廃止はされてほしくないけど、かといって全員にリモートワーク適正があるわけではないので、リモートワーク適正に関係なく人を集めたいなら最低限のラインを決めつつ選択性がいいのかなぁというところ。 自宅のほうがすべてを自分好みにできてオフィスよりも自分に合っている椅子、机、モニタ、室温で仕事できるし通勤に時間をかける必要も無いけど、仕事終わりに突発的に飲みながら話したりマリオカートライブホームサーキットが突然始まったりする良さも捨てがたい。 ただ、リモートワークを縮小するならなぜ移転先が都内ではなくみなとみらいなのかという点は納得してないが……

移転とリモートワーク縮小がきっかけなのか他の要因と重なってかは分からないけど、事実として自身のチームや身近なチームからの退職者が相次いだ。悲しい方向ではあるけど自分が中心に去年からやっていたレガシー倒しが現実的な効果を出すことになった。 そんなこともあって、今年の後半くらいからは採用関連で色々やってみたり現在の状況に適した組織構造について上長と話したりと人事関連 (?) にも少しずつかかわるようにしていった。 ソフトウェアエンジニアなんだからずっとコードを書いていたいという話題が Twitter では定期的に流れてくるけど、もちろんそういう志向の人がいるのは全く不思議じゃないし非難されるようなものではないけど、自分に何が向いてるのかなんて分からないので興味と機会があれば色々やってみるほうが自分は良いと思っている。そのために別の状況を求めて転職とかもよくある話だと思うけど、幸か不幸か会社の状況がどんどん変わっていくので、転職の手間をかけずに様々な経験をできている。 とはいえ去年と今年はだいぶ守りに偏ってしまっていて、去年はまぁ他のチームメイトに攻めてほしくて自分は守りに入ることにしたというところはあるけど、来年は死なない程度に守りを薄くしつつ自分も攻めたいですね。

引っ越し

今年の3月に引っ越しをした。旧居には社会人になったときから住んでいたので、7年間くらい住んでいたことになる。 オフィス移転とタイミングが近いのでそれに合わせての引っ越しだと思われがちだけど、実際は全く関係ない。オフィス移転しそうという噂は聞いていたけど時期も移転先も知らなくて、新居の契約を結ぶまであと数日というタイミングでオフィス移転の時期と移転先を知ることになっていた。なので当時そのまま契約するか結構迷ったけど、みなとみらい周辺で探してみても家賃相場は全然安くないどころかやや高いくらいで、結局そのまま契約してオフィスには1時間ちょっとくらいかけて通勤することになっている。

引っ越しのきっかけは収入がそれなりに増えてきたところに新型コロナで家にいる時間が増えたので家に金をかけたいと思ったことだった。 社会人になるときにオフィス近くで選んだところなので、まぁ狭いし設備もそんなによくないところに住んでいた。何なら学生時代よりも少し悪いくらいのところだった。 平日日中はオフィスに行ってゲーセンに寄ったり外食したりして帰ってくるような生活だと自宅が狭くてもそこまで不快じゃなかったし、契約更新のたびに少しずつ安くなっていったので引っ越すモチベーションがあまり上がっていなかったけど、リモートワーク中心で遊びに行ける先も少ない生活ではさすがにストレスが大きくて引っ越しすることにした。 バス、トイレ、洗面台が独立していて一定の広さがあって近くにゲーセンがあって…… みたいな条件で探すとオフィス付近には無いので家賃補助は早々に捨てる判断をして、ゲーセンの分布を見つつある程度知ってる土地から探していっていま住んでいるところに決めた。 許容範囲ギリギリくらいの家賃にはなってしまったが今のところ家計は大丈夫そうだし大いに満足している。 ただ、将来新型コロナが完全に収束して家にいる時間が減ったらまた違う条件で引っ越し先を探すかもしれない。

引っ越しをきっかけに家具にも少し興味を持てるようになってきた。少し快適になるようなものを買って置くような余裕ができたので、とりあえず実用面で良さそうなものを試していきたい。 雑なおたくグッズ置き場として https://www.amazon.co.jp/dp/B08BC71B9V とか良かった。有孔ボードで色々吊るせる上にスチール製なのでマグネットもつく。

趣味

音ゲー関連では CHUNITHM が無事虹レとなった。大型アップデートにより値は大きく変わったけど、虹レはそれより前に達成していたしレート値はほぼ +1 されていたので、順当に成長して虹レになったと言えると思う。最終的に MAX 16.16 まで上がったのは 120fps 化で見え方が変わった影響も大きいかもしれない。感覚的なところだと4鍵なら指押しが求められる階段や乱打をちゃんと押せるようになってきたと思う。5鍵以上は分からん。あとは餡蜜でとるというテクニックもできるようになってきた。 つい先日混沌もギリギリ S に乗せることができ MASTER 全譜面で S をとれたかと思いきや、大型アップデート時に追加された XL TECHNO -More Dance Remix- で S を取れずに泣いてます…… これどういう練習したらできるようになるの。 オンゲキのほうのレートは MAX 15.37 まで上がったので順調に上達はしてそうなんだけどなんかあんまり上手くなってる感が無いんだよな……

今年やったゲームで面白かったのは、マニフォールド ガーデン、メトロイド ドレッド、真・女神転生Ⅴ、アンリアルライフ、メルヘンフォーレスト、Tales of Arise、喫茶ステラと死神の蝶、PARQUET、異世界酒場のセクステットあたり。あとは一応ドーナドーナも今年分に含めておく。 リメイクも含めるとスーパーマリオ 3Dワールド + フューリーワールド、ゼルダの伝説 スカイウォードソード HD、ポケモン BDSP も。 マニフォールドガーデンは新鮮なパズルゲームで面白かった。3D 酔いはしやすいけど。 元々人気のあるシリーズだけど一作もやったことなくて今回初めてプレイして面白かったのがメトロイドドレッドと Tales of Arise。メトロイドドレッドは思ってたより死に覚えゲーだったけどやり直しがそこまで苦じゃないところが良かった。 Tales of Arise は PC にホリの GC コン繋いでやったけどストレス無く楽しめた。ストレスが無さすぎて戦闘が単調すぎるようにも感じたけど、難易度を上げたらまた変わるのかもしれない。 ドーナドーナは独立して感想を書いた https://eagletmt.hateblo.jp/entry/2021/01/19/024517 けどまじで面白かった。それだけに続編が絶望的になってしまったのが悲しい…… PARQUET と異世界酒場のセクステットは手軽に楽しめるギャルゲ、エロゲです。異世界酒場のセクステットは全年齢版を Steam で売って公式サイトで R-18 パッチを無料配布するという形式をとってるんだけど、これアリなんですね…… Steam で買えると楽なのでパクってほしいけど、全年齢版も同時に作ることになるのでフルプライスのエロゲでやるのは難しいか。

趣味開発では envchain 代替を書いたのが大きかったかな https://eagletmt.hateblo.jp/entry/2021/04/23/001333 。プライベートでは envwarden を使っているし仕事では envop を日常的に使っている。

イベント関連だと今年は少しずつ復活してきているように感じる。プリティーシリーズ系のライブは現地開催あったし、イロドリミドリのライブも現地があった。無声なのが悲しいけど配信との差でいうと音響が良いという差は大きいかも。 音響と言えば最後の VIRTUAFREAK にも行った。バーチャルに関係あるようなないような音で満ちていてよかったですね。 バーチャル関連だと HIMEHINA が元々すごく好きなんですがオンラインのみになってしまったライブもとても良かったですね…… 期間限定でいつ消えるか分からないけどダイジェスト版的なものが https://www.youtube.com/watch?v=b4JBZG7_UHA に残っている。 新型コロナのことがなければ藍の華は去年の全国ツアーの予定だったので、またそういう企画が出てくるようになってほしい。

ISUCON での言語移植 (Rust)

ISUCON10 に続き今回の ISUCON11 でも初期実装の Rust 移植を担当したのでそのへんの話を書いてみます。

ISUCON とのかかわり

ISUCON4 から ISUCON7 までは選手として参加していて、何度か本選にも出場しました。ISUCON8 以降に参加しなくなった理由はいくつかあるんですが、Web のインフラやバックエンド界隈を盛り上げて学生の興味を惹く素晴らしいイベントだと思っています。そんなわけで ISUCON からはしばらく離れていたんですが、ISUCON10 では同僚が作問するということと新たな試みとして初期実装に Rust を加えると聞いて、Rust 移植担当に応募して採用されました。そして今回の ISUCON11 でも Rust か Ruby の移植に応募し Rust の移植を担当しました。

言語移植を担当するモチベーション

自分の場合は ISUCON が盛り上がってほしいことに加えて、自分の好きな言語が盛り上がってほしいというのが主なモチベーションです。自分は Rust や Ruby が好きなんですが近年の ISUCON は Go に一極集中していて、元々プログラミング言語自体が好きな自分にとって1つの言語にあまりに偏りすぎる状況はつまらないように感じます。ISUCON11 ではこの傾向が更に加速してしまって、本選出場者のうち9割が Go でそれ以外は Rust と Node.js (TypeScript) のみという結果でした。

isucon.net

Rust に限って数字を見ると ISUCON10 と比較して予選での利用者が大きく増えていて本選通過も2チームに増えたので、Go 一極集中になんとか対抗していきたいところです。

あとは移植作業を通じて色々な言語の差を感じられるというのも楽しみの1つです。Go ではこういう書き方をする・できるんだなぁというのを見ながら、Rust に移植するならどう書くか考えたり、また自分のあまり詳しくない TypeScript や Python ではこう書くんだなというのを眺めるのは楽しいです。

ISUCON での移植作業

問題のテーマが決まってオリジナルである Go の初期実装の原型があるくらいの段階から参加して、Rust に移植する上で十分なライブラリがあるか等の移植する上での問題点のチェックから始まります。この時点で気になった点は作問者にフィードバックして初期実装をどうするか決めてもらいます。ISUCON10 本選では gRPC を扱えるかというのがありましたし、ISUCON11 予選では JWT、本選では UUID *1 や zip の扱いをチェックしました。ある程度初期実装が固まってきたら Rust 版を書き始めて、同時に開発が進んでいるベンチマーカーを手元で実行して通るか確認したりブラウザ上での表示を確認したりしながら移植を進めていきます。その間にも問題の改定が進むので Go 版の初期実装に変更が入ったらそれに追従したり、ベンチマーカーの実装が進んでチェックが厳密になったら失敗するようになった箇所を直したりといった作業が続きます。ISUCON11 はかなり穏やかに進みましたが、ISUCON10 本選のときは https://github.com/isucon/isucon10-final/issues/136 ということがあってだいぶ緊張感がありました。 あとはコードレビューを受けたり、本番環境で Rust 版に対してベンチマークを実行して完走するかチェックしたり初期スコアを確認したりすればだいたい終わりです。

これは厳密には移植担当の仕事ではないんですが、ISUCON11 では事前の試し解きにも参加して、実際に動くポータルの環境とマシン1台を用意してもらってその時点での問題を解き、こういう改善を入れてこれくらいスコアが伸びたみたいなメモを残したり問題に対するフィードバックを書いたりして作問に少しだけ協力していました。自分の場合は移植の動作確認も兼ねて Rust に移植した状態で問題を解いてました。

Go から Rust に移植する上で大変だなと思うのは、ライブラリの細かい挙動の差はもちろんあるんですが、Go から Rust 特有の話としてはエラーの扱い、nil やゼロ値の扱い、ポインタの扱いあたりでしょうか。Go の場合どんなエラーが返ってくるのかの詳細が型の上でもドキュメントでも分からないことが多く、Rust のように細かく分類されたエラーのうちどれを拾えばいいのか分かりにくく感じることがあります。これと似た話で Go では失敗したり存在しなかったりするときにゼロ値を使うことがあり、Rust に移植するときに Go 実装を注意深く読んだりベンチマーカーが投げるリクエストを調べたりしないと Option<T> にすべきか単なる T にすべきか悩みます。また Rust は GC が無くライフタイムの概念のある言語なので、Go で自由奔放にスライスを扱ってるような実装を Rust でどう書くか悩んだりしました。具体例としては ISUCON11 予選問題のこのへんを見比べてみてください。

ISUCON で初期実装として Rust が提供されるようになったのは ISUCON10 が初であり、その移植を担当する上でどのフレームワークやライブラリを選択するかも悩みました。Web フレームワークとしては2回とも Actix Web を選んでいます。

actix.rs

他にも warp も検討したんですが、RubySinatra くらいの機能が標準で提供されているものとなると Actix Web かなぁということもあって決めました。ISUCON11 では Actix Web の beta 版が使われていることが気になった人ももしかしたらいたかもしれませんが、今更 Tokio v1 以前に揃えるのもなぁということで beta 版を選びました。

MySQL に接続するライブラリとしては ISUCON10 では mysql を選びましたが ISUCON11 では sqlx を選びました。ISUCON10 のときも sqlx は検討対象でしたが、まだよく枯れてない印象だったので避けました。ただ今回の ISUCON11 では使っても大丈夫そうかなと考えを変え、async/await に対応していて Tokio v1 への対応も終わってるということで sqlx にしました。Actix Web も sqlx も、ISUCON で利用する上で重厚すぎず、またデファクトスタンダードとして広く使われているものという基準で選んでいます。まだ始めてから2回目なので、もしライブラリ選定に疑問があれば声を上げてもらえればと思います。

sqlx の不具合

ISUCON11 本選の移植作業中に sqlx の MySQL 向け実装固有の不具合の影響を受けていることが発覚し、sqlx の実装を読みつつワークアラウンドを入れて不具合を回避するという対応をしました。実は予選のときにも同じ不具合が発生していたんですが見落としていました。競技中のスコアには影響しないと思いますが、競技中に panic のログが出ていて戸惑ったチームがいたら申し訳無いです。何が起きていたかについては本選では https://github.com/isucon/isucon11-final/blob/0bd78572393513f0b6534365f378c34595e2463e/webapp/rust/src/db.rs に説明を書きました。過去問として利用されるときに混乱させないよう、予選のほうにも同様の修正を後日入れておきました https://github.com/isucon/isucon11-qualify/pull/1454

おそらくこうすれば直るだろうというパッチを sqlx に出しています https://github.com/launchbadge/sqlx/pull/1439 が、本当にこの修正でいいのか現時点では不明です。

まとめ

ここ2回の ISUCON での言語移植について書いてみました。移植作業には移植作業の楽しさがあります。今後も ISUCON が続くことを願っていますし、選手として参加する気持ちが復活しない限りまた Rust か Ruby の言語移植に応募しようと思っています。

*1:最終的には ULID に変わりました

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 を使ったほうがいいと思う。