88
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RealWorld 業務 Rust

Last updated at Posted at 2024-12-26

RealWorld 業務 Rust

  • 実際に Rust 1.0 の頃から業務で Rust を使ってコードを保守してきてハマった落とし穴についての 知見 恨み言です
  • 気が向いたら追加します

開発環境編

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

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

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

  • target はブラックホール
  • 10GB 超はあたりまえ、中には 100GB 超も
  • sccachecargo cachecargo sweep などを駆使してがんばれ
  • docker も使うので大容量ストレージだけが正義だ

sccache 使用例

$ cat ~/.cargo/config.toml
[build]
rustc-wrapper = "/home/***/.cargo/bin/sccache"

cargo cachetarget/... の使ってないキャッシュを消す例

$ cargo cache --gc --fsck --autoclean-expensive

mold を入れろ

  • 一番ビルド時間とメモリを食うのは ld でのリンク(シングルスレッドしか使わないため)
  • 並列リンクできる mold は絶対使っとけ
  • ビルドでメモリを使うのはリンク時のみ
  • 実はメモリは16GBもあれば十分かもしれない

使用例

$ cat ~/.cargo/config.toml
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = ["-Clink-arg=-fuse-ld=/usr/local/bin/mold"]

ビルド時間を調べろ

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

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

  • rust-toolchain.toml は必ず設置しろ
  • 使いたい機能があっても nightly は論外
[toolchain]
channel = "1.72.0"
components = [ "rustfmt", "clippy" ]
profile = "minimal"

docker でビルドできるようにしとけ

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

glibc のバージョンを揃えるために実行環境と同じ docker コンテナでビルドしろ

  • cargo-zigbuild なら glibc のバージョンが固定できるというは幻想で、できる場合もある、のが正しい
  • aws lambda を使うなら amazonlinux2 の中でビルドできるようにしとけ

CI で cargo fmt をチェックしろ

  • 常識
  • オレオレフォーマットを主張するやつは相手にするな
  • ci に cargo fmt && git diff --exit-code とかすればよい

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

  • とりあえず全部つけとけ
  • パラノイアになれ
  • 長いものにまかれろ
  • ひとつの warning も許すな

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

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

crate の semver は信じるな

  • rust において信じられるのは semver ではない、作者への信用だけだ
  • 0.x はすべて破壊的変更を含んでいると思え
  • 1.x になっても非互換な変更を入れてくるやつはいる、aws-sdk-rust とか

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

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

rust-analyzer は頼りにならない

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

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

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

gdb は頼りにならない

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

ハイゼンバグは実在する

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

musl はあてにならない

  • RealWorld 業務 Rust では libssl や libsqlite3 などの共有ライブラリに頼ることになるので musl は使えないと思え
  • ↑2つは bundled があるのでフルビルドでなんとかなるが RealWorld では他にも共有ライブラリに頼ることになるのでいずれ musl は使えなくなる

本番環境でも常に RUST_BACKTRACE=1 で実行しろ、

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

コーディング編

  • RealWorld 業務 Rust は個人のライブラリ開発ではない
  • この文書は社内のみの業務アプリケーションコードを複数人で書くコツである

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

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

  • ファイル先頭に限らず std 以外の prelude は使うべきでない
  • どのシンボルが import されているのか、そのライブラリに詳しいお前以外は予想できない
  • お前は読めても他の人間は読めない

複雑なライフタイム変数を持つ参照は使うな

  • 脳死で Arc<Mutex<T>> して Clone + Send + Sync + 'static しろ
  • 業務で生体参照ライフタイムソルバするのは不毛
  • メモリ効率とか速度とか気にするな
  • 顧客へのデプロイ速度がすべてだ
  • 実行効率の最適化は問題が起きてからやれ
  • 非同期rustを書いていると Stringclone が頻発する(&strがライフタイム的にできないので)が、気にせず clone しろ
    • Arc<String> を使う最適化は後から考えろ

オレオレ trait は使うな

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

マクロは使うな

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

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

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

e2e テストを書け

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

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

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

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> でFA

  • 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 みたいに名前をつけろ

async fn はあてにならない

  • async fn の返り値の future が持つ参照のライフタイムは記述できない
  • clippy が warning: this function can be simplified using the async fn syntax とか言ってくるが #[allow(clippy::manual_async_fn)] で黙らせろ

async fn が使えないので sqlx のクエリ関数の例

#[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,
{

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

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

長いものに巻かれろ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
88
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?