ltmemo

Zero To Production In Rustをやった

tags:
2021-11-15

ToC

Zero To Production In Rustをやった

記事はこれ

webAPIが作れるようになればlambdaに乗せれるし、async/await周りの理解が全然だったので
そのあたりを学ぶのが目的。フレームワークとしてLaravelやflaskあたりとの違いも意識したい

1章

環境構築について説明がある
clippyとかrust-fmtとかは知ってたけど、カバレッジ出せるtarpaulinは知らなかった

2章

ニュースレターサービスを作るにあたって、ユーザストーリーの組み立て方を考える

  • As a XXX (XXXとして)
  • I want to YYY (私はYYYしたい)
  • So that ZZZ (そうすればZZZ)

アジャイルかなんかで見たことあるような組み立て方。

3章

ユーザストーリーの一つ目として、 「ブログ閲覧者として、ニュースレターの購読者に登録し、更新を受け取れるようにしたい」 を実装する
その実現のために、ここでは/subscribeのPOSTエンドポイントを実装する

  • web frameworkを決める
    • こんかいはactix-webを使う
  • testのやり方も決める

合わせてhealth_checkのエンドポイントも作る
200 OKを返すだけだけど簡単で良い

Rustは非同期プログラミングの仕組みはFutureで構成される
非同期ランタイムは標準のものがない(なにか理由があったはずだけど覚えていない。非同期処理と言ってもいろんな方法があるから選べるようにとかそういう雰囲気だった記憶)

actix_webのマクロをcargo expandで表示して実際はどのような記述になるのかを確認した
cargo expandもっと早く知っていれば...

health_checkのテストを作る。
runで待受しちゃうとテストが終わらなくなっちゃうので、runでは作ったサーバインスタンスを返すように変更した
バックグラウンドタスクにするために、tokio::spawnを使う。これを使うと処理をブロックせずにテストコードへ処理が進む

cargo expandで、testファイルもexpandできる
actix_rt::System::new().block_on(async ...)みたいになってるな actix_rt::testのマクロを使うとtokioでの非同期処理を使ったテストが行える

portが競合しないように、テストで使うportを選ぶがここではport0を使う。0を指定するとOSが開いてるportを割り当ててくれるの知らなかった
バインドした情報を知らないとテストができないので、ここではTcpListenerから情報を取得する

3.5章

subscriptionsのエンドポイントを作りつつ、DBへの接続や副作用のあるテストの扱いについて学ぶ

まずはテストの拡充から。テストの入力と求められる結果を配列でまとめておいてループで回すのを
table driven testと呼ぶ(別名でParameterized Testとも)

仮のエンドポイントを作ってテストがこけるところまで進めてからExtractorというactix-webの仕組みに進む

Extractors

名前の通り、requestから必要な情報を抜き出してくれる仕組み ざっくり以下の三つとか。actix_web::web::に生えてる

  • Path
  • Query
  • Json

Formというのもあり、これはurl-encodedされたリクエストボディを抜き出してくれる
一つのハンドラにつき10個までつけられるんだそうな

FormDataにserdeをderiveするだけでテストがpassする。そこからはなぜそれが起きるかを掘り下げていく

  • actix_webのForm自体はTのラッパー
  • FormはFromRequestトレイトを実装している
    • asyncのfrom_request関数を持つ
      • HttpRequestとPayloadを受ける
      • Errorで返すactix_web::ErrorはHttpResponseに変換が可能
  • subscribe関数の引数にweb::Formを指定しているけど、これはRequestをForm::from_requestを通した値でうけとれる

FromRequest for From でTが満たす必要のあるDeserializedOwnedってなに?

  • 借用なしにdeserializeできることを示す
  • DeserializeとDeserializerはともに'deライフタイムを持つ
    • https://serde.rs/lifetimes.html

serde

  • 29のタイプにすでに対応している(data modelと呼ぶそうな)
    • 自前で実装したかったらSerializerを実装したらばよい
  • 速度は速い。
    • monomorphizationのおかげ?
    • RustコンパイラはGenericsを使っていてもそれらの型すべてに対応する関数を作る
      • これによって実行時のコストがかからない
      • 「ゼロコスト抽象化」と呼ばれる概念がこれ
  • Serializeトレイトはserialize(&self)->SerdeDataModelを実装する

understanding serde

  • よく使う関数から#[derive(Serialize)]がどのように展開されるのかを追いかけていく
  • リフレクションを使って実行時に型情報を受け取る、といった処理はRustは行えないので、
    Serdeのマクロはワークアラウンド的なことをやっている
    • マクロを展開すると構造体の名前やフィールドの情報をserialize_structに渡しているのがわかる
  • serialize_structの中ではserialize_mapが呼ばれる
    • serialize_mapを読んでいく
    • 最初はbegin_objectにwriterが渡される
      • jsonの場合シンプルに"{"がwrite_allされる
      • そのあとstateにState::Firstを持つCompound::Mapが返される
      • serialize_structの後にserialize_fieldが呼ばれる
        • serialize_structの戻り値はCompound::Mapであるのはさっき確認した
        • serialize_fieldの後にはSerializeStruct::Endが呼ばれる
      • serialize_fieldではSerializeMap::seairlize_keyとserialize_valueが順に呼ばれる
        • ここでkeyとvalueはそれぞれ構造体のkeyの文字列
          • Point構造体ならx,y
          • self.x,self.yが該当する
        • ここでもbegin_object_keyやbegin_object_valueといった関数が呼ばれている
          • 例えばbegin_object_valueは:を書き込む(jsonのkeyとvalueの区切りがこれだから)

serdeのデータモデルは構造体や列挙体の形ではなく、
Serializer,Deserializerのtraitとして、各データ形式で実装された関数の形をしている

databaseを選ぶ

いろんな選択肢があるけど今回はポスグレを使うよ。いろんなクラウドプロバイダでサポートされてるし、
OpenSourceだし、充実したdocumentもある

database(PostgreSQL)を扱えるcrateは

  • tokio-postgres
  • sqlx
  • diesel

などがある。選ぶ基準は

  • compile-time safety
  • SQL-first vs a DSL for query building
  • async vs sync interface

compile time safetyって?

DBの操作では簡単なタイポとかでも正しく動かない
実行時にこれを判定しているからよくない。コンパイルするときに正しくないoperationはエラーにしよう
というアプローチ

dieselはschemaからこれをはやすっぽい。sqlxはマクロでコンパイル時にDBにつないで諸々チェックするみたい

tokio-postgresとsqlxはSQLを直接使用することを想定している。
dieselは一方Queryビルダを提供していて、これはRustの型で表現されている

SQLは移植性が高いが、dieselのDSLはそこでしか意味がない
dieselでも難しいクエリは生のSQLを書く必要があるので結局覚えた方がええ

スレッドは並行して作業するためのもの
非同期は並行して待つためのもの

tokio-postgresもsqlxも非同期なinterfaceを提供する。dieselは同期的
compile-time safetyなのはsqlxとdiesel
なので今回はsqlxを使う

migrationする

ポスグレとsqlxでmigrationする。shellで接続可能になるまでsleepしたりしている
この辺はコンテナの中で完結させないんだなー

configを使って、yamlを読み込めるようにしたり、.envを配置したりして、connection込みのテストを通す

connectionの管理

続けてapplicationでconnection情報を保持するために、app_dataというAppの関数を使う
しかしconnectionはCloneできないのでエラーが出る
HttpServer::newはAppではなく、closureを受ける。これがrequest毎に生成されるイメージかな

なので解決としてweb:Dataを使う。こいつは接続をArcで囲んでくれる

app_dataに入れた内容も、Extractorは引っ張ってこれる
内部的にAnyを使ったhashMapで情報を管理しているとかなんとか
TypeIdで識別しているらしい。引数に指定した型に対応するレコードが、内部的に保持するtype-mapにあるかを調べるらしい
ほかの言語でのDIライクな仕組み

PgConnectionはsqlxのExecuterを実装していないので、PgPoolを代わりに使う
ここにはPgConnectionがPoolされていて、接続をよしなにしてくれるので便利

testでのconnection pool対応

test用の構造体を定義して、spawn_appでそれを生成するように変える

testの冪等性

ここまでのテストは実行前後のDBの状態が同じではないので、何度かテストするとこけたりする
これに対処するために、まず固有の名前を持つlogilcalなdbを作って、migrationして、それからテストする

実際は都度DBを作成してmigrationしてる
クリンナップを端折ってるのは意図的らしい

4章 telemetry

telemetryは遠隔測定法のことらしい

known unknowns(知らないことを知っている) unknown unknowns(知らないことも知らない)

ソフトウェアの不具合は様々な原因で起きる、トラフィックの増加、DBのfailover、長く稼働することによるメモリリーク
そういった挙動は本番環境でしか起きない事が、多い、どのように知るか

Observability(可観測性)

運用環境について、任意の質問ができることをObservabilityと呼ぶ
任意の、というのがポイント

そのためには、アプリからテレメトリーデータを収集できるようにする
データを効率的に切り出して操作できるようにする

今回はとりあえず前者の達成を目指すようだ

Logging

logクレート...ではなくactix_webの提供するLoggerミドルウェアを使う
複数requestが並行して走るので、logを複数出力すると何も考慮しないと見づらくなる
logにrequest idもしくはcorrelation idを設定するようにする(ここでidはUUIDなどの重複しないもの)

ただしこの方法では、actix_webがどのようなresponseを返したのかなどまでrequest_idの紐づけが行えない
かといって、actix_webのloggerを書き換えたりするのは筋が悪い

ここでtracingというcrateが登場する、これは今回のケースのように因果関係があったり、ネストしている場合にも使える

Spanという仕組みを使ってHttpのrequest全体に適用する
spanにenterすると、それがdropされるまでのlogやinfoがこのspanの子扱いになる
RAII(Resource Aquisition is initialization)というパターンのようだ

tracing_futureを使うと非同期処理(sqlxのqueryとか)でもspanで見ることができる
ただしこれにはenv_loggerが対応していないので、置き換える必要がある

tracingのSubscriber

tracing単体ではsubscriberを提供しない
その代わりにtracing-subscriberを使う
このクレートはLayerという概念を持ち込んでくれる。これを使うとスパンの処理パイプラインを作れるんだそう
同様にRegisterという概念も存在し

env_loggerっぽいサブスクライバーを作るぞ
そのためには以下の3つが必要だ

  • tracing_subscriber::filter::EnvFilter
    • 環境変数を参照してlevelが適当でないlogをfilterしてくれる
  • tracing_bunyan_formatter::JsonStorageLayer
    • spanデータと関連するメタデータを下流のレイヤが使いやすいようjsonで保存してくれる
  • tracing_bunyan_formatter::BunyanFormatterLayer
    • topのJsonStorageLayer、logレコードをbunyan-compatibleなJSONとして出力する

tracingとテスト

テストで実行するとき、subscriberの初期化を1度だけにするためにonce_cellを使用する
また、test時のログ出力が邪魔なので、envを見て出し分けするような修正を行った

subscribe関数の全部をrequest_spanで包みたい

tracing::instrumentを使おう~
このマクロ使うだけで処理がだいぶ見やすくなる

request_idをリクエストのlog全体に付与する

actix_web::Loggerだと呼び出し元のIPやstatusコードにアクセスできない
ここではtracing-actix-webを使う

5章 Deployment

メモ

  • try!マクロ
    • ?でResultはがすのと似た感じ。unwrapと違いエラーにせずreturnしてくれる
    • どうやらdeprecatedのようだ
  • std::any::Any
    • どんな値も格納できるHashMapが作れる
      • 値にBox

crateメモ

  • tarpaulin
    • コードのカバレッジをレポートしてくれるツール
  • cargo-audit
    • 報告されている脆弱性のチェックをしてくれる
    • cargo audit で実行
  • cargo-expand
    • マクロとかをコンパイラが展開したあとの表示がどのようになっているかを教えてくれ~る
  • reqwest
    • httpのclient。テストで使った
  • chrono
    • timestamptz型のDateTimeに対応してくれるみたい
    • 日時を扱うcrateでデファクトっぽい?
      • 標準では暦やタイムゾーンを扱わないので、それらが必要な時はこのcrateを使う
  • config
    • 環境変数やjsonなどのファイル等々から設定情報を抜き出してくれるcrate
    • 型定義してyaml読み込んでtry_intoでその構造体にしてくれてびっくり
  • log
    • logging用のクレート
    • ログレベル毎のマクロ
  • env_logger
    • loggerを実装しているcrateの一つ
    • 環境変数で出力するログレベルを変えられるらしい
  • tracing
    • 時間や因果関係に関する追加情報を持つイベントが記録できるようになる
    • 開始時刻と終了時刻を持つ
  • tracing-subscriber
    • tracing公式が提供してくれるsubscriber
  • tracing-bunyan-formatter
    • spanデータをいい感じに整形してくれるformatter
  • tracing-log
    • tracingのlog facadeを実装しているクレート
    • 全部のログイベントを収集して、tracingのeventとしてoutputしてくれる
  • cargo-udeps
    • 使っていないcrateなどをチェックしてくれる
  • once_cell
    • lazy_staticを置き換える仕組み
    • macroを使わずとも、staticな変数の初期化処理を行うことができる

単語メモ

  • table driven test(prameterized test)
    • inputとexpectの組の配列を作ってテストを行う

衛生的なマクロ

Rustのマクロは衛生的(健全)らしい
理由としては以下で、

  • マクロ内の変数名とマクロ呼び出し側の変数名が衝突しない
  • 構文の優先順位の違いによる直感に反した挙動が起きない
  • 渡されていない値に影響がでない

最終更新: 2021-12-27