2021年のふりかえり

今年も完全にリモートワークで引きこもってて同僚とのランチや勉強会の懇親会での雑談が無いので、去年に引き続き生存確認的な意味合いも込めて雑に今年の振り返りをしたいと思います。

お仕事

Treasure Data 9年目です。

去年から Digdag – Open Source Workflow Engine for the Multi-Cloud Era ベースで開発・運用されているWorkflowサービスのチームに移ってました。
Digdagはタスク間の依存関係や個々のタスクの状態の更新、スケジュールされたタスクのキューイングなどをPostgreSQLでがっつり管理しており、排他制御や楽観的ロックがにぎやかで処理性能や潜在的な性能面でのボトルネックが把握しづらい感がありました。特に、どこまでならスケールアウトできるか?という点はサービスを運用する以上把握しておきたい。で、今まで取得できていなかった遅延測定のためのメトリクスを足したり、指定された時間範囲で実行されたワークフロー群の遅延情報のサマリを取得するツールを書いたり、各種設定&クラスターサイズごとのベンチマークを自動的にとるツールを書いたり、ということを細々とやらせてもらってた。最終的には、開発側で気づいていなかった遅延がある程度存在していて(Digdag単体での性能限界の指標になる)その遅延を劇的に減らせたし、どこまでスケールアウトで対応できるか把握できたので良かった気がする。
あとは割と自由に安定化やバグ潰し、改善系のタスクに取り組ませてもらいました。

で、今年の9月辺りにWorkflowチームから移動してレポートラインがCTOのNahiさんに代わり「TDのサービス開発・運用を全体的にみたときに頑張ったほうが良さそうなところ」をサポートすることになりました。前VPoEと話してた時に「お前はPrincipal Engineerなんだから特定のサービスだけ見てないで、TD全体を見て技術的な問題点見つけて飛び込んでいけ」(実際はもっと丁寧で穏やか)的な提案をされてて、確かにそのほうが貢献できるっちゃあできるなぁと思っていたので、Workflowチームのタスクが一区切りしたタイミングで移動。今回はUKのチームをサポートしつつAWSのCDK、Redshift、Aurora、Lambdaあたりと格闘してました。来年はどうなるのか楽しみ。

先月、太田さん・古橋さんが日本にいるタイミングで社内BBQがあって参加したけど、みんな元気そうで良かった。ビール取りに行く際に誰かとすれ違うたび近況報告会が始まるので非常に楽しいものの肉を食べる時間が無かったのはちょっと残念。

趣味のプログラミング

既存のプロジェクトのメンテナンスをしていたくらいで特に新しいことしてない… 古いRailsで書かれてた親類向けのWebアプリをKotlin + Ktor + Exposedで書き直して、すべての運用をAnsible上にのせたけど、技術的には粛々とやってただけな感じ。去年つくったNES emulatorが開発体験的にはかなりアツかったので、来年はOSやろうかなぁ。もしくは以前作った自作Raftが論文読みつつ書いてて今見直すと設計的にかなりイケてないので、一から書き直したい気持ちもある… まぁ来年適当にやるでしょう。

ゲーム

Outer Wilds

不親切度が高くて万人受けしなさそうですが、個人的には良かった。エンディングまでたどり着けたときはかなり感慨深かった。攻略サイトはガンガン見ました。

Slay the Spire

積んであったのに気が付いて始めてみたら、面白かった。アイアンクラッドとサイレントでアセンション 0の心臓を倒して、いまディフェクトで挑戦しているけど、ディフェクトでの勝ちパターンというか軸がまだつかめてない。来年はアセンション20を目指したい。ただ、時間が溶けるのがやばい…

スプラトゥーン2

引き続きスプラトゥーン2をボケ防止的にちょこちょこやっててSとAを行ったり来たりしてます。勝ち負けで一喜一憂しなくなって味方の面白ムーブで逆転負けしてもそれを楽しむ境地に達しつつあるので、かなり精神面で成長した気がする。

World of Tanks (Blitz)

スプラで心折れたときに少しやってみた。復活しない点は生存重視な自分としては相性が良いものの、スピード感があまりないので最近はやってない。あと戦車愛が無いのでそれほど刺さらなかった。

漫画

漫画アプリで少しづつ読んで読み終えたのはファブル、東京スワン、センゴクオールラウンダー廻BECKホーリーランドクレイモア辺り。ファブルは頭一つ抜けて面白かった。連載中のだと、葬送のフリーレン、血と灰の女王、アフターゴッド、日本三國、SPY×FAMILY、腹腹先生、推しの子、働かないふたり、マイホームヒーロー、ハコヅメ、等は購入したりWebで読んだり。怪獣8号は最近メリハリが無い感あるので、読み続けるか迷い中。マイホームヒーロー、ハコヅメは漫画アプリで読んでるけど後で購入しそう。

あと、
www.amazon.co.jp
は本好きとしてはとても良かった。

アニメ

去年ほど観てない。Vivy -Fluorite Eye's Song- は結構よかった。あと、ODDTAXIも。最近は、攻殻機動隊 STAND ALONE COMPLEX を見返してるけど素晴らしい。

健康

健康診断で悪玉コレステロールの数値が基準値超えてることが分かったので、休日に一時間半程度の散歩をするようにして、室内で本読みながらだらだらエアロバイク漕ぐようにしてます。コレステロールは血液検査が必要でこまめに測定できないので、代わりに自宅で測定できつつ何かしらの関連がありそう(期待)な内臓脂肪レベルを見てますが、7.0から5.0まで下がったので何かしらの効果はありそう。まぁ来年の健康診断で勝負。

と、今年について思い浮かぶことを適当に書き出してみました。来年もがんばるぞー

2020年の振り返り

普段、年末の振り返りブログを書いてない勢なのですが、今年は同僚とのランチや勉強会の懇親会での雑談が無くなってしまったので、生存確認的な意味合いも込めて今年の振り返りをしたいと思います。

お仕事

Treasure Data 八年目です。前前職であるNaver Japan(→NHN Japan→現LINE)が在籍期間一年半、前職であるDwangoが一年だったのでこうしてみるとTD長いですね…

仕事内容としては去年から続いていたdata ingestion pipelineの開発・運用を今年三月くらいまで続けてました。割と大量のingested dataをKinesis Stream二層使いでaggregationして、AWS Aurora, S3, Bloom filterを使ってdeduplicationするもので、skewed dataについても考慮してあるのでなかなか面白いものだったと思います。

その後、他チームで既に立ち上がってたプロジェクトを手伝うのかなと思ったら、いつの間にかがっつりmanagementをすることになって驚いていたのですが、これはこれでTDでは今までにない経験だったのである意味面白かったです。大枠の設計が決定済みのプロジェクトを引き受けるのはあまり得意ではないのだなぁと自覚できました。もっと精進しないといかんですね。

で、九月辺りから正式に Digdag – Open Source Workflow Engine for the Multi-Cloud Era をベースとしたworkflowサービスのチームに移りました。TDではかなり重要な役割を担っているところですが、将来的なscalabilityやstabilityの面で興味があったので参加しました。

会社的にはArmがNvidiaに買収されることとなり来年は色々変化がありそうな予感がしていますが、基本的に変化大好き人間なので楽しんでいきたいと思います。

プログラミング

こっちは個人的なprojectですが、今年頭にdistributed consensus algorithmの一つであるRaftをOCamlで実装して GitHub - komamitsu/oraft: Library of Raft consensus algorithm implemented in OCaml(開発メモ:What I learned from implementing Raft consensus algorithm in OCaml | by Mitsunori Komatsu | Medium)、今年末にNESファミコン)のemulatorをKotlinで実装してみました(開発メモ:
Development of NES (Famicon) emulator from scratch is fun, and tough | by Mitsunori Komatsu | Dec, 2020 | Medium)。いずれも「原理は理解しているけど実装してみたことがない」もので、まぁ実際に作ってみると意外な発見が多くて非常に面白かったです。他のネタも溜まってるので来年も何か作ってみると思います。

ゲーム

今年はついにSwitchを買ってしまいました。長男がSuper Mario Maker 2をやりたくて限界っぽく、またCOVID-19で自宅での娯楽を増やそうかと思ったのがきっかけです。で、六月辺りにSplatoon2を買ってしまいました。現状A帯にいて新しいブキに手を出してはB帯に落ちてるんですが、これはゲームというよりも精神修行に近い気がします。初動のホコ割りで味方二人が吹っ飛んでもう一人も溶ける試合が数試合続くと「仕事の方が楽しい…仕事したい…」となるので、Splatoon2逃避的に仕事に打ち込めるという利点もあります。ちなみによく使うブキは、96ガロン・スシベッチュー・わかば、辺りです。あと最近ゼルダBotWを始めました。これも「もうSlpatoon2やりたくない…ゼルダBotWの世界でパラセールしてたい…」というモチベーションで頑張ってます。他にはSteamでMindustryを買ってやってました。採掘や資源移動を頑張って効率化して余裕こいてると予想外のボス敵が壊滅的な打撃を与えてくる、というのは安定運用中のサービスでヤバイ障害が発生して死にかけるのに似ていて良いと思います。

漫画

いろいろ読みましたが「働かないふたり」は久しぶりに全巻揃え中の漫画です。Splatoon2で打ちのめされ殺伐とした心を癒してくれます。あと最近Amazon Unlimitedで「大東京トイボックス」を一気読みしたのですがアツくて良かったです。「New Game」よりもリアルで無駄に可愛くなく良いのですが、根底を流れるテーマが意外と重めなのでもうちょっと軽い開発話を読みたいところ。

アニメ

かくしごと」、「波よ聞いてくれ」、「放課後ていぼう日誌」辺りは普通に面白かったです。「鬼滅の刃」は我妻善逸が出た辺りからギブアップしてしまいました。「ハイキュー」は全シリーズ観ました。基本的には良かったのですが最新シリーズの作画が急に面白くなってしまったので驚きました。あとなんとなく「ID: INVADED」を観てましたが意外と良かったです。「呪術廻戦」は安定して面白く観ています。特に「じゅじゅさんぽ」が良い。

健康

毎日最低1Kmちょい走っているのですが、どうやらじわじわと体脂肪率が上がってる気がします。家族がやってるリングフィット アドベンチャーを僕もやるべき時なのかもしれません。

酒量は明らかに減った気がします。社内外のイベントで飲む機会が無くなったし、オフィスで仕事上がりに同僚とビール飲みながら雑談する機会が無くなったのもありますし、家での晩酌もその後のSplatoon2に影響が出てしまうので少なめになってると思います。まぁ飲まなければ飲まないで全然問題ないので良さげです。


と、今年について思い浮かぶことを適当に書き出してみました。来年もがんばるぞー

サーロインステーキの焼き方メモ

試行錯誤の末、最近焼き方が安定してきたのでまとめておく(下記はレア向けになっているので、ちゃんと焼きたい場合は時間を倍にしてもok)。

  1. 肉は出来るだけ厚めなもの
  2. 調理の一時間前には冷蔵庫から出す
  3. 肉の両面に塩胡椒を振り擦り込む。塩は肉の重さの1%弱
  4. 片面から筋の部分を2cm程度おきに切っておく
  5. そのまま放置
  6. フライパンを熱し、サラダ油を適当にひく
  7. 肉の片面を蓋をして強火で1分間焼く
  8. 反対側を蓋をして45秒焼く
  9. 肉をアルミホイルで包み、フライパンの上に乗せた蓋の上に3分間乗せる
  10. フライパンを強火で熱し、肉の両面をそれぞれ30秒ずつ計1分間焼く

既存の秘密鍵・証明書からJavaのTrustStore & KeyStoreを作る手順メモ

すぐに忘れそうなので、メモメモ。

KeyStore

openssl pkcs12 -inkey server.key -in server.crt -export -out keystore.pkcs12
keytool -importkeystore -srckeystore keystore.pkcs12 -srcstoretype pkcs12 -destkeystore keystore.jks -destkeypass keypassword -deststorepass storepassword
# 内容を確認
keytool -list -v -keystore keystore.jks

TrustStore

keytool -import -file server.crt -alias mytruststore -keystore truststore.jks

# 内容を確認
keytool -list -v -keystore truststore.jks

IntelliJでRobocodeのRobot開発メモ

毎回忘れて色々試しては時間を食っている気がするのでメモ。
1. Robocode自体のインストール (See http://robowiki.net/wiki/Robocode/Download)
2. IntelliJで適当にprojectを作り、`Project Settings` -> `Modules (Dependencies)` に `JARs or directory` として上記1でインストールしたRobocode配下にある `libs/robocode.jar` を追加
3. http://robowiki.net/wiki/Robocode/My_First_Robot 辺りを参考に適当なRobotを作成
4. IntelliJ上で `Build Project` しておき class files を出力させておく
5. Robocodeの `Options` -> `Preferences` -> `Development Options` に上記4の class files が出力されている root directory を指定
6. あとは普通にRobocode上の `New` 以降で作成した Robot が指定できるようになっているはず

JVM Tool Interfaceを少しさわってみたメモ

ふと、JVM(TM) Tool Interface 1.2.3 に触れたことがないことに気がつき、少しさわってみたのでメモ。

OpenJDKのソースコードjdk/src/share/demo/jvmti の下に幾つか例があるので、それを試してみることに。ちなみに概要は上記URLのドキュメントの各APIの説明の直前まで眺めれば良さそう。APIの種類が多いので少し圧倒されるけど、実際に何か作る際に目的に合ったものを見れば問題無いかと。

今回見てみたのは versionCheck / mtrace の二つ。

  • versionCheck

Agent_OnLoad()でJVMTI_EVENT_VM_INITイベントにコールバック関数をセットして、その中でjvmtiEnvから引っこ抜いたバージョン番号をstdoutに表示するだけのシンプルなもの

Mac OS X上でのビルドは以下のような感じ。ちなみにagent_util/agent_util.cはjdk/src/share/demo/jvmtiが依存しているヘルパー関数群。

$ pwd
/Users/komamitsu/src/openjdk/jdk/src/share/demo/jvmti/versionCheck
$ clang -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -I../agent_util -c ../agent_util/agent_util.c *.c
$ clang -dynamiclib -L${JAVA_HOME} -o libversionCheck.so *.o

これを利用する場合は以下のように指定する

$ java -agentpath:/Users/komamitsu/src/openjdk/jdk/src/share/demo/jvmti/versionCheck/libversionCheck.so Main
Compile Time JVMTI Version: 1.2.1 (0x30010201)
Run Time JVMTI Version: 1.2.3 (0x30010203)
Hello world
  • mtrace

Javaのメソッド呼び出し回数をトレースするもの。Agent_OnLoad()で色々コールバックを設定するが、面白いのはJVMTI_EVENT_CLASS_FILE_LOAD_HOOKで都度ロードされたクラスファイルの各メソッドを、同梱されているMtrace.javaのメソッドを最初に呼ぶように書き換えている(と思う)。Mtrace.javaのメソッドは当該Cライブラリファイルの関数(この中でメソッド呼び出し回数をカウント)をnative methodとして呼ぶように紐づけられているが、その際、JNIのFindClass()とRegisterNative()を使っている。ちなみにjava_crw_demo/java_crw_demo.c はクラスファイル操作系ヘルパー関数群。

$ pwd
/Users/komamitsu/src/openjdk/jdk/src/share/demo/jvmti/mtrace
$ clang -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -I../agent_util -I../java_crw_demo -c ../agent_util/agent_util.c ../java_crw_demo/java_crw_demo.c *.c
$ clang -dynamiclib -L${JAVA_HOME} -o libmtrace.so *.o
$ mkdir -p classes
$ javac -d classes Mtrace.java
$ jar cf mtrace.jar *

使い方で少しハマった点としては、FindClass()でMtrace classを探す際、ロードされる前に探しに行くとJVMがクラッシュしてしまう(FindClass()の戻り値がNULLになることを期待してたけど...)。なので -Xbootclasspath/a でmtrace.jarを先にロードする必要あり

$ java -Xbootclasspath/a:/Users/komamitsu/src/openjdk/jdk/src/share/demo/jvmti/mtrace/classes/mtrace.jar -agentpath:/Users/komamitsu/src/openjdk/jdk/src/share/demo/jvmti/mtrace/libmtrace.so  Main
    :
Class java/lang/String 8084 calls
        Method charAt (I)C 3941 calls 3941 returns
        Method length ()I 832 calls 832 returns
        Method equals (Ljava/lang/Object;)Z 543 calls 543 returns
    :

なお、途中で、調査のため-Xcheck:jni -verboseを有効にしたら捗った。特に-verbose指定時に以下のエラーが出たのでJDKのバージョン違いに気がつけた(途中JDK8 -> 7にして試してた)

[Loaded java.lang.ClassFormatError from /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.UnsupportedClassVersionError from /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/jre/lib/rt.jar]

Rustを使ってPostgreSQLの拡張ライブラリ (FDW) を書いてみた話

というタイトルですが、具体的には "Rustで拡張ライブラリ全体を書いた" のではなく "Cで拡張ライブラリの本体を書いてその中からRustのライブラリを呼んで、非同期でRust側からの結果を取得" してます。

ちなみに、Foreign Data Wrapper (FDW) とは、PostgreSQL管理外のデータソースにIOできるPostgreSQLのテーブルの一種です。今回の場合 "Treasure DataというSaaSに裏でクエリーを投げて結果を取得するFDW" となります。

github.com

なぜこんなことをやろうかと思ったか?

という背景がありました。

何が必要?

  1. Treasure DataのAPI経由でSQLを投げて結果を取得できるRust製のクライアントライブラリ
  2. Cで書かれたFDW本体。このFDWに対応したPostgreSQLのテーブルに対してSQLが発行されると、PostgreSQLに登録してあるFDWの関数が適宜コールバックされる(実行計画に必要な情報提供、クエリ開始時、クエリ結果一行読み込み、など)
  3. 上記1と2をつなぐ何か

1 は普通にREST APIのクライアントライブラリを書けば良いので、すでに書いてあり (GitHub - komamitsu/td-client-rust: Rust Client Library for Treasure Data) crates.ioにもpush済み (https://crates.io/crates/td-client)
2 は既存のPostgreSQL FDW等をベースすれば難しくなさそう。Treasure DataはPrestoとHiveをサポートしているので、PostgreSQLの構文との差異を適当に吸収する必要はあり ( "~~" => "LIKE" 等)

この記事では3について説明をしたいと思います。ちなみに3はソースコード的には2と同じプロジェクトに含まれます。

CとRustの連携部分

今回のケースでは、以下のような連携が必要となります

  1. 外部データソースのスキャン開始時に、td-client-rustを用いてTreasure DataにSQLを投げて処理が終わりクエリ結果が生成されるのを待つ。クエリ発行時の初回のみ
  2. 生成されたクエリ結果を1レコードずつ読みRust側からC側に返す。これはPostgreSQL側から要求がある度、通常複数回処理が発生 (LIMIT句で限定されているか、最後までクエリ結果を読みきるまで)

実際のコードではFDWとtd-client-rustの間に橋渡し的なCとRustの関数が入っています。

以降は簡単のためこれらを bridge(C)、bridge(Rust) と呼びます。

全体の流れを図にすると
f:id:komamitsu:20161212004619j:plain
f:id:komamitsu:20161212004628j:plain
このようになります。

クエリ結果の非同期取得

Treasure Dataから返ってきたSQLクエリ結果をPostgreSQLに返すタイミングは、PostgreSQLからの非同期なFDW関数呼び出しとなるため少し工夫が要ります。今回は...

  • bridge(Rust)内で、Treasure Dataのクエリ処理終了を確認後、std::sync::mpsc::channel() でstd::sync::mpsc::SenderとReceiverの組を生成
    let (tx, rx) = mpsc::channel();
  • さらにスレッドを生成し、その中で td-client-rustのtd::client::Client::each_row_in_job_result() を呼び出す。この関数にはクエリ結果1レコードごとに呼ばれるclosureを渡せるので、そのFnの中でレコードをSender::send() でキューに追加するようにする。全レコード取得後、門番的な意味合いのレコードをキュー末尾に入れておく
    thread::spawn(move || {
        match client.each_row_in_job_result(
            job_id,
            &|xs: Vec<Value>| match tx.send(Some(xs)) {
                Ok(()) => (),
                Err(err) => {
                    log!(error_log, "Failed to pass results. job_id={:?}, error={:?}",
                             job_id, err)
                    // TODO: How to propagate this error....?
                }
            }
        ) {
            Ok(()) => match tx.send(None) {
                Ok(()) => (),
                Err(err) => {
                    log!(error_log, "Failed put sentinel in the queue. job_id={:?}, error={:?}",
                             job_id, err)
                }
            },
            Err(err) => {
                log!(error_log, "Failed to fetch result. job_id={:?}, error={:?}",
                         job_id, err)
            }
        }
    });
  • Receiverの方は、そのアドレスをbridge(Rust)、bridge(C) の関数の戻り値としてFDW内に保存
    let query_state = TdQueryState {
        job_id: job_id,
        result_receiver: rx
    };

    let td_query_state = Box::into_raw(Box::new(query_state));

    td_query_state    // <<< return value
  • Treasure Data FDWからbridge(C) -> bridge(Rust) 側にReceiverを渡し、bridge(Rust) でReceiver::recv() を呼びクエリ結果1レコードを取得
    match query_state.result_receiver.recv() {
        Ok(result) => match result {
            Some(xs) => {
                for x in xs.into_iter() {
                    match x {
                        Value::Nil => add_nil(context),
                        Value::Boolean(b) => add_bool(context, b),
                        Value::Integer(Integer::U64(ui)) => add_u64(context, ui),
                        Value::Integer(Integer::I64(si)) => add_i64(context, si),
                        Value::Float(Float::F32(f)) => add_f32(context, f),
                        Value::Float(Float::F64(d)) => add_f64(context, d),
                        Value::String(s) => {
                            let bytes = s.as_bytes();
                            add_string(context, bytes.len(), bytes)
                        },
                        Value::Binary(bs) => {
                            let bytes = bs.as_slice();
                            add_bytes(context, bytes.len(), bytes)
                        },
                        other => {
                            log!(debug_log, "fetch_result_row: {:?} is not supported", other);
                            add_nil(context)
                        },
                    }
                };
                true
            },
            None => {
                drop(td_query_state);
                false
            }
        },
        Err(RecvError) => {
            drop(query_state);
            false
        }
    }
  • bridge(Rust) -> bridge(C) に何とかしてレコードを返し (後述)、それをFDWに戻す

というように、mpsc::channel() で生成される内部的なキューをクエリ結果の受け渡しに使ってみました。

クエリ結果の受け渡し

PostgreSQLからFDWにクエリ結果1レコードを要求された際の値の返し方ですが、試行錯誤 & 二転三転の末、現状以下のようになっています。

  • FDW側でレコードの要素数分のchar *の配列を生成しReceiverと共にbridge(C)に渡す
  • bridge(C)では、char *の配列をvoid *なコンテキストとしてbridge(Rust)に渡す。その際、Receiverと各型に応じたコールバック関数群も渡す
int fetchResultRow(void *td_query_state, int natts, char **values)
{
	int ret;
	fetch_result_context context;

	context.index = 0;
	context.values = values;

	ret = fetch_result_row(
	          td_query_state,
	          &context,
	          add_nil,
	          add_bool,
	          add_u64,
	          add_i64,
	          add_f32,
	          add_f64,
	          add_string,
	          add_bytes,
	          debug_log,
	          error_log);
  • bridge(Rust)では、Receiver::recv()で取得したクエリ結果レコードの要素の、各型に応じたコールバック関数に要素の値とvoid *なコンテキストを渡す
    match query_state.result_receiver.recv() {
        Ok(result) => match result {
            Some(xs) => {
                for x in xs.into_iter() {
                    match x {
                        Value::Nil => add_nil(context),
                        Value::Boolean(b) => add_bool(context, b),
                        Value::Integer(Integer::U64(ui)) => add_u64(context, ui),
                        Value::Integer(Integer::I64(si)) => add_i64(context, si),
                        Value::Float(Float::F32(f)) => add_f32(context, f),
                        Value::Float(Float::F64(d)) => add_f64(context, d),
                        Value::String(s) => {
                            let bytes = s.as_bytes();
                            add_string(context, bytes.len(), bytes)
                        },
                        Value::Binary(bs) => {
                            let bytes = bs.as_slice();
                            add_bytes(context, bytes.len(), bytes)
                        },
                        other => {
                            log!(debug_log, "fetch_result_row: {:?} is not supported", other);
                            add_nil(context)
                        },
                    }
                };
                true
            },
  • bridge(C)のコールバック関数では、コンテキストから復元したchar *の配列の適切な位置に、bridge(Rust)から渡ってきた値を文字列としてコピー
static int add_string(fetch_result_context *context, size_t len, const char *s)
{
	char *buf = (char *)ALLOC(len + 1);
	memcpy(buf, s, len);
	buf[len] = '\0';
	context->values[context->index] = buf;
	context->index++;
	return 0;
}

要するに、"FDWで用意したクエリ結果1レコード用のchar *の配列に各要素を文字列として書き込む" ために bridge(C)->bridge(Rust)->Callback function in bridge(C)というややこしい呼び出しになっています。なぜこのようになっているかというと

  • PostgreSQL内での動的メモリ確保はpalloc()というPostgreSQLの関数を用いることが推奨 (メモリリークの影響を限定) されており
  • 可能であればクエリ結果の各要素の値のコピー時にもpalloc()でメモリ確保したい
  • とはいえ、bridge(Rust)側で直接palloc()を呼び出すのは色々面倒そうなので避けたい

という事情があったりします... しかし、各要素型分のコールバック関数群をbridge(C)->bridge(Rust)で渡すより、palloc()を呼ぶ動的メモリ確保用のコールバックを渡したほうがすっきりするので、後ほど修正しそうな気がします。

C -> Rustでのハマリどころ

他にも色々はまった気がするのですが主なものは以下の通り

RustオブジェクトをCに渡す方法

Boxでヒープに置くだけでは不十分で、mem::transmuteとBox::into_rawのいずれかでRust側でのメモリ管理から外す必要あり。Box::into_rawだとunsafeいらないので少し嬉しい

Rustでraw pointerを複数回dereferenceする場合

Box::from_raw()を使うと元のアドレスが二重で解放されてしまうので、その場合は unsafe { &mut *x } を使う必要あり

この辺りは、clangの "-fsanitize=address" を使うと非常に便利でした。


この、Treasure Data FDWですが色々と修正の余地がありそう、かつPostgreSQLのFDW対応テーブルへのINSERT -> Treasure Dataへのbulk insertもそのうち実装したいので、今後も細々と暇を見つけて弄りたいと思います。