Skip to content

Instantly share code, notes, and snippets.

@legokichi
Last active September 17, 2024 01:57
Show Gist options
  • Save legokichi/89fac9c5be0eb0e50972da85cc8ee4ed to your computer and use it in GitHub Desktop.
Save legokichi/89fac9c5be0eb0e50972da85cc8ee4ed to your computer and use it in GitHub Desktop.

Real World 業務 Rust

前置き これは
  • 多人数の開発で
  • Rustに不慣れな人間もかなり混ざってて
  • 富豪的プログラミングが可能な Web 開発で
  • コードオーナー不在で
  • Rust 1.0 公開直後の error_chain や tokio_core の時代から今までコードを保守してきた

経験から得た 恨み節 生存戦略 知見である

  • ↑の前提がなければツッコミどころ満載なのは自覚している
  • この話は表だって書くと社内の人間や会社の名誉を傷つけかねない

ので gist に放流した

ビルドマシンを買ってもらえ

  • ノートパソコンのCPUとメモリでは限界がある
  • cpu 二桁コアのマシンを何人かで共有して使え
  • VSCode の Remote SSH でがんばれ
  • neovim でもいいぞ

ストレージは可能な限りデカくしろ

  • target はブラックホール
  • 10GB 超はあたりまえ、中には 100GB 超も
  • sccachecargo cachecargo sweep を組み合わせて自衛しろ
  • $ cat ~/.cargo/config.toml
    [build]
    rustc-wrapper = "/home/legokichi/.cargo/bin/sccache"
    
  • docker も使うので大容量ストレージだけが正義だ

mold を入れろ

  • 一番時間とメモリを食うのはシングルスレッドしか使わない ld でのリンク
  • 並列リンクできる mold は絶対使え
  • $ cat ~/.cargo/config.toml
    [target.x86_64-unknown-linux-gnu]
    linker = "/usr/bin/clang"
    rustflags = ["-Clink-arg=-fuse-ld=/usr/local/bin/mold"]
    
  • ビルドでメモリを使うのはリンク時のみ
  • 実はメモリは16GBもあれば十分かもしれない

ビルド時間を調べろ

  • cargo build –timings で計測しろ
  • 謎の features のせいでビルド時間が律速してることが多々ある
  • 計測しろ

docker でビルドしろ

  • 開発メンバーの開発環境はてんでバラバラだ
    • windows10,11, wsl1,2
    • mac x64, m1, m2
    • ubuntu busuter,bullseye,bookworm
    • debian, archlinux, NixOS, asahilinux, amazonlinux1,2
    • iPad, android, chromebook
    • くらい統一されてない
  • docker しか信用できない
  • docker のバージョンも厳密に統一しろ
    • docker engine バージョン違いで挙動は変わる

rust-toolchain.toml をバージョン指定で配置しろ

  • 常識
  • 使いたい機能があっても nightly は論外
  • [toolchain]
    channel = "1.72.0"
    components = [ "rustfmt", "clippy" ]
    profile = "minimal"
    

ci で cargo fmt をチェックしろ

  • 常識
  • オレオレフォーマットを主張するやつは相手にするな

ci で cargo clippy --tests --examples -- -Dclippy::all しろ

  • パラノイアになれ
  • 長いものにまかれろ

docs.rs にしがみつけ、ソースも読め

  • 大抵のことは docs.rs を読めばわかる
  • でも何をやってるのかわからなくなるので docs.rs にある source リンクを押してソースを読むことになる
  • でも何が起きてるのかわからなくて cargo new hogecrate-sandbox で playground を作って試すことになる
  • でも実は docs.rs に書いてあったりする

crate の semver は信じるな

  • rust において信じられるのは major version ではない、作者への信用だけだ
  • 1.x になっても非互換な変更を入れてくるやつはいる、aws-sdk-rust とか
  • 信用できるのは serde とか tokio とか、そのへんの作者

alias は使うな

  • use hoge::A as HogeA とかするな
  • 愚直に hoge::A とタイプしろ
  • お前は読めても他の人間は読めない
  • type Result<T> = Result<T, MyError> みたいな std の型名を上書きするのは論外
  • お前のことやぞ use anyhow::Result;
  • use std::error::Error;use std::io::Error; を見分けられる人間だけが石を投げなさい
    • use std::time::Duration;use chrono::Duration;
    • use thiserror::Error:use anyhow::Error:
    • 等々
  • お前は読めても他の人間は読めない
  • trait alias も同様
  • お前は読めても他の人間は読めない
  • 部分コピペで動かなくなるコードは作るな

ファイル先頭で use は使うな

  • use std::sync::mpsc::channel とかするな
  • use hoge::Error とかするな std::error::Error と区別がつかなくなるから
  • channel とかの一般名刺が突然出てきてもわからなくなる
  • お前は読めても他の人間は読めない
  • 部分コピペで動かなくなるコードは作るな
  • std::rc::Rc<std::cell::RefCell<T>> とか Box<dyn std::future::Future<Output=T>+ Send + Sync + 'static> とかのタイピング練習を欠かすな
  • でも use std::rc::Rc とか std::sync::Arc とかならゆるしちゃうかも
  • でも use tokio::sync::Mutex とかが突然生えてきたりするので自衛のために std::sync::Mutex<T> と書いてしまう
  • 関数内の先頭なら許す(コピペ可植性が高いので)
  • でもモジュールの先頭とかには書かないでくれ
  • コードを書くときは楽ができるかもしれないが、コードを保守する側としては大変困る
  • git で pull request を作ってもコンフリクトの主要な発生源になり大変
  • use std::{thread::sleep, *} みたいな書き方は言語道断である
  • Smithay/smithay のこのコードを見て発狂しないやつだけが石を投げなさい

use hoge::prelude::* は使うな

  • ファイル先頭に限らず prelude は使うべきではない
  • 何が import されるのか、お前以外は予想できない
  • お前は読めても他の人間は読めない

参照は使うな

  • 脳死で Arc<Mutex<T>> して Clone + Send + Sync + 'static しろ
  • 業務で生体参照ライフタイムソルバするのは不毛
  • メモリ効率とか速度とか気にするな、顧客へのデプロイ速度がすべてだ

オレオレ trait は使うな

  • trait でオレオレ DSL を作ろうとするな
  • お前は読めても他の人間は読めない
  • rust-analyzer で定義に飛んで trait だったときの絶望感を味わえ
  • 誰にも読めない完璧な抽象化コードよりも、誰でも読めてどこにでもコピペできる愚直で平易なコードが長く生き残るコードだ

マクロは使うな

  • お前にしか読めないコードよりも誰でも読めるコピペコードのほうがマシだ
  • 修正箇所が n 箇所のコピペで済むならコピペのほうがマシだ
  • 誰にも読めない完璧な抽象化コードよりも、誰でも読めてどこにでもコピペできる愚直で平易なコードが長く生き残るコードだ

rust-analyzer は頼りにならない

  • 小規模コードならともかく、コードが大きくなるにつれて動かなくなる
  • features を認識しなかったりバージョン違いなどで、いずれ動かなくなる
  • 複数回のコード定義ジャンプしないと理解できないようなコードを書くな
  • 「n回のコピペ(切り貼り)で移植可能なコード」のnが小さいほど可読性、可植性が高い
  • 誰でも読めてどこにでもコピペできる愚直で平易なコードが長く生き残るコードだ

コンパイルエラーは上から順番に直せ

  • cargo clippy --tests --examples 2>&1 | head -n 40 だけを信じろ
  • 下の方のエラーは上のエラーが引き起こしているので読むだけ無駄である

gdb は頼りにならない

  • どうせマルチスレッド、マルチ非同期タスクのコードのデバッグにはなんの役にもたたない
  • printlnlog::debug だけが唯一の信頼できるデバッグ情報だ
  • テストを書いて print debug で二分探索するのが最も早いデバッグ手段だ

ハイゼンバグは実在する

  • print すると再現しないバグは存在する
  • printlnはスレッド間で同期を取る

io をモックできるテストを書け

  • io を伴うテストにはすべからく再現性がない(flaky である)

e2e テストを書け

  • aws-sdk-rust すら実行時の挙動に破壊的変更が入る
  • aws lambda を aws_lambda_runtime で実行する場合 amazon linux で実行することになるが、 glibc のバージョン違いとかでローカルテストが通っても lambda の中では実行時リンクエラーで動作しなかったりする
  • e2e test だけが唯一信用できる

musl はあてにならない

  • RealWorld 業務 Rust では libssl や libsqlite3 などの共有ライブラリに頼ることになるので musl は使えないと思え
  • ↑2つは bundled があるのでフルビルドでなんとかなるが RealWorld では他にも共有ライブラリに頼ることになるのでいずれ musl は使えなくなる
  • cargo-zigbuild なら glibc のバージョンが固定できるというは幻想で、できる場合もある、のが正しい

バージョンはパッチバージョンまで固定しろ

  • パッチバージョンを上げただけでバグる crate は存在する
  • aws-sdk-rust とか

huga()? (the question mark operator) をそのまま使うな

  • エラーを見てもどこで何がおきたかわからん
  • use anyhow::Context; して hoge.huga(param).context(format!("huga {param:?} で落ちた"))? を書きまくれ
  • backtrace も有効化しろ

常に RUST_BACKTRACE=1 で実行しろ

  • release にも debug 情報は残せ
  • [profile.release]
    debug = 1
    しろ
  • error-chainfailurethiserror も信用できない
  • 結局信じられるのは stack trace の関数名と行番号だけだ
  • backtrace が有効なら.expect("ここで落ちた") を頑張る必要はない
  • 安心して .unwrap() してくれ

長いものに巻かれろ

  • 一番使われている crate がいちばんいい crate だ
  • 謎の crate を自作するな公開するな

Builder Pattern はクソ

  • Rust の Builder Pattern は crate で API を公開するときのメジャーバージョンの互換性のために使われている
  • 非公開内製 crate なら Builder Pattern で setter を生やすよりも Paramater struct を引数にとる new method だけで十分
  • お前のことやぞ aws-sdk-rust
  • rusoto は良かった、本当に…
  • init pattern が好き

println するな log::debug しろ

  • log を使っておけばテストやデバッグでも潰しが効く
  • tracing にも対応できるぞ
  • とりあえず main 関数には脳死で env_logger 入れとけ
  • テストにも脳死で env_logger::builder().is_test(true).try_init().ok() って書いとけ
  • これが業務 rust の "おまじない" だ

エラーの型は Result<Result<T, CustomError>, anyhow::Error>

  • Rustのエラー処理はResultのネストが正解
  • エラーには分類(回復)可能なものとそうでないもの(panic相当)がある
  • 回復不能なものを別にanyhowとしてくくりだすことで ? を使いつつ柔軟なエラー処理が書ける
  • #[tokio::main]
    async fn main() -> Result<(), anyhow::Error>{
      let o = match foo().await? {
        Ok(o) => o,
        Err(CustomError::A) => {
          todo!()
        }
        _ => {
          todo!()
        }
      }
    }
  • 単にpanicさせるのではなくエラーレポートを書きたいなどのときに、パニックハンドラのようなlow-levelの処理に頼らなくても良くなるので便利

let _ = hoge() による _ 束縛は使うな _hoge みたいに名前をつけろ

crate 名は常に hoge_huga を使え hoge-huga は使うな

  • ややこしい
  • serde-jsonserde_json か間違えたことがないものだけが石を投げなさい

async fn はあてにならない

#[allow(clippy::manual_async_fn)]
fn run_query<'a, 'c, A>(conn: A) -> impl Future<Output = Result<(), BoxDynError>> + Send + 'a
where
    A: Acquire<'c, Database = Postgres> + Send + 'a,
{
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment