
2026/06/06 22:34
Rust コード12 万行に迫る:Nosdesk バックエンドの内側
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
このテキストは、堅牢な Rust デスクトップアプリケーション「Nosdesk」の進化とアーキテクチャの詳細を説明しており、260 のモジュールにわたってコード行数が約 120,000 に達したにもかかわらず、厳格なセキュリティを保証しています。システムは Actix-web、Postgres 上での Diesel、ファンアウト用には Redis を使用し、タスク実行には Tokio というスタック上で動作します。安全性の確保のために危険なロジックを Rust の型システム内に押し込めるアプローチ(例:スコープ付き
TenantConn コネクション)を採用し、パニックを隔離するために catch_unwind を使用しています。重要なアーキテクチャ原則として、純粋なビジネスロジックと I/O を分離することでテスト可能性を確保しています。データ同期は、複数の経路が消費する单一の追加のみログ(sync_actions)を使用して行われ、リアルタイム更新には SSE が使用され、15 秒ごとのハートビートや遅延クライアントに対するタイムアウトなどのメカニズムによりプロキシによる停滞を防いでいます。高度な機能としては、ドキュメントハッシュから導出された CRDT 安全性、SSRF 安全な DNS リゾルバー、そしてサーキットブレーカーおよび FOR UPDATE SKIP LOCKED を活用する耐久性のあるメールキューが含まれています。本プロジェクトは高並行システムにおける業界リファレンスとして機能しますが、現在の v1 の課題としては、独裁的な main.rs の再構築、SIGTERM シグナルに対する優雅なシャットダウンのワイヤリング、および不要な unsafe 実装の削除が含まれています。本文
Nosdesk 技術的基盤と設計思想:Rust で築いた堅牢性の証言
掲載日: 2026 年 5 月 28 日
プロジェクトの現状
当初は数ファイルに過ぎなかったプロジェクトは、約一年間の開発を経て以下の規模へと成長しました。
- コード行数: 約 12 万行(Rust)
- モジュール数: 約 260 モジュール
- テスト数: 約 1,030 テスト
- 起動方法:
の一条令で、単一のバイナリとして配布docker compose up - 技術スタック:
- Web フレームワーク: Actix-web(最上層)
- データベース: Postgres + Diesel
- プッシュ型ファンの配信: Redis
- アシンクロ環境: Tokio
3 つの鉄則と設計哲学
このプロジェクトを通じて定着した習慣がすべてに共通しています。
- 型システムへのミス転嫁
- 「間違えないように促す」のではなく、「間違いがあればコンパイルすら通らない」ようにする。
- ロジックと I/O の分離
- 純粋なロジックと付随する I/O を分離することで、データベースやソケットを用意せずとも独立してテスト可能な関数として実装を分割できる。
- 「なぜ(Why)」の記述
- コメントで「何をするか」ではなく「なぜそうするか」を書く。
- その理由、守るべき RFC、そしてバグから得た教訓を記述する。
データ同期のアーキテクチャ:すべてはパイプラインである
クライアント接続時のブートストラップ同期において、大量データを一度に読み込むとメモリスプライクが発生するため、ストリーム処理を採用しています。
- NDJSON 形式: 行を改行区切り JSON にシリアライズし、
を通してプッシュする。mpsc::channel(64)- バックプレッシャーを活用し、読み込み側が遅くてもプロデューサーから溢れるのを防ぎます。
- 非同期処理:
(同期)クエリをDiesel
で実行し、結果をspawn_blocking
経由で返します。ReceiverStream - トランザクション保証: スナップショット全体を一つのトランザクション内で完結させ、クライアントは一貫した時点のビューを取得できます。
この「プロデューサーが消費者を上回る速度でも動けるパイプライン」視点は、コード全体に適用されています。
リアルタイム同期:Postgres を押し出すように教える
同期エンジンは**追加専用ログ(Append-only Log)**であり、
sync_actions 表の一行書き込みに対して以下の 3 つの消費者がいます。
- クライアント向けの HTTP デルタ同期
- 接続中クライアント向けのリアルタイムプッシュチャネル
- 監査証跡
リアルタイムプッシュの実装工夫
Postgres の
LISTEN/NOTIFY は同期的な Diesel では扱いにくいので、独立した tokio-postgres 接続を使用しています。
- ポーリング API をストリーム化:
これにより、コールバック型の C スタイル API からlet mut messages = stream::poll_fn(move |cx| conn.poll_message(cx));
を橋渡ししています。async/await - 意図的なパヨロード(負荷)を持たせない:
- 通知にペイロード(行 ID など)を含めず、「ウォータマーク以降の全データを受け取る」というシンプルなウェイクアップのみで行います。
- ライサーは
でデータを取得し、失敗モードの蓄積を防ぎます。WHERE sync_id > last_seen - 並発書き下でも正しく動作するアプローチです。ペイロードを信頼してフェッチすると、同一ウィンドウ内のコミット行を見逃すサイレントバグに陥ります。
接続維持と再プレイ:ライブレイヤー
Server-Sent Events (SSE) を使用し、接続一時停止時のギャップ埋め(Backfill)を可能にしています。
- トピック管理: ライブテール(
)と、最近イベント用のリングバッファをペアづきます。tokio::sync::broadcast - クライアントごとのストリーム処理:
- 購読トピックのマージ
- 再プレイバッファのドレインおよび重複排除
- 接続切断防止のための 15 秒間隔ハートビート挿入
- 遅延クライアントの閉鎖(他の消費者への影響防止)
- 自動破棄:
実装により、登録取り消しが自動的に実行され、手動破棄のミスを防ぎます。Drop
意図的な並行性設計
コンパイルエラーやデッドロックを防ぐために、以下のような厳格な依存関係を構築しています。
: 遅くポップアップされるトピックマップ用DashMap
: ファンアウト機能およびバグ検出用tokio::broadcast- 有界 MPSC: バックプレッシャーが必要な部分用
- ロッキング戦略:
クロスしない場合はawait
、クロスする場合はstd::sync::RwLock
を使い分けるtokio::sync::RwLock - シーケンストータル:
AtomicU64
外部ライブラリの安全性:ライブラリがパニックする時
Yjs の Rust ポート(yrs)による CRDT 共同編集機能を実装する際、以下の 2 つの設計判断を採っています。
1. 安定した ID 管理
- サーバー側で
のハッシュから決定論的にクライアント ID を導出し、53 ビットにマスキングします。DocumentID - 効果: バックエンド再起動時にも「新しい参加者」として認識されず、幻覚的な不一致(ファントム・ダイバージェンス)を防ぎます。
2. パニックの封じ込め
が不健全な UTF-8 を検出するとパニックするため、すべての呼び出しをyrs
で囲みます。catch_unwindfn safe_get_fragment_string(fragment: &XmlFragment, txn: &Transaction) -> Option<String> { catch_unwind(AssertUnwindSafe(|| fragment.get_string(txn))).ok() }- 予期せぬパニックを接続全体に波及させず、呼び出し元のみで処理を止めます。
クラッシュ耐性:不幸なパスのための耐久型設計
メールサブシステム(約 14,000 行)は、「幸せなパス」ではなく「不幸なパス(エラーハンドリング)」のために構築されています。
- シルクトブレーカー:
- ローリングウィンドウベースの失敗検知。
- プロバイダーが失敗し始めるとブレーカーが開き、攻撃を停止します。
- 追加のタスクなしで状態遷移(ハーフオープン)を実現しています。
- フル・ジッター(全乱数)バックオフ:
- AWS Builders' Library の式を純粋関数として実装。
- オーバーフロー処理を含み、99 回試行してもパニックしないことが保証されています。
- 理論上「少なくとも一度」の配信:
- FOR UPDATE SKIP LOCKED クエリでバッチ化し、5 分のリース期間を設定。
- ワーカーが死亡してもリース切れで再試行可能。
- 送信途中で死亡しても
で重複削除をサーバー側で行うため、二度送りを許容しつつ確実に配信します。Message-ID
アクター方式の監督システム
- 登録レジストリは長寿タスクが所有し、HTTP ハンドラーは共有マップではなくチャンネルを通じてコマンドを送ります。
- パニックしたワーカーはログに記録され停止され、無限ループへの再起動は行われません(バグとして扱います)。
間違えないように不可能にする:型システムとアクセス制御
Nosdesk はマルチテナントシステムであり、以下の厳格な制約を設けています。
- クエリのスコープ強制:
- ハンドラーには生の DB コネクションを与えず、以下の extractor のいずれかを使用させます。
: Row-Level Security (RLS) を自動的にワークスペースコンテキストでフィルタリングします。TenantConn
: クロステナント操作のため特別ロールへ昇格するもの(稀)。PlatformConn
- 型による宣言:
を取ることで「テナント境界を超えている」と型シグネチャで宣言し、レビュー時に可視化されます。PlatformConn
- プラグインシステムのセキュリティ:
- 署名されたサードパーティコードのみを実行します。
(Ed25519)を必要とし、検証済みモジュール内のみの構築を強制しています。InstallToken- Allowlist ループが存在せず、型システム自体がチェック回避を不可能にします。
攻撃耐性のための微小構造防衛
コードベース全体にパラノイアを増す順序で以下の防御策を実装しています。
- SSRF 安全なアウトバウンド HTTP:
- カスタム DNS リゾルバーを埋め込み、アドレスフィルタリングを行います。
- 非ルーティング範囲の列挙や IPv4-mapped-IPv6 のトリック対策を含みます。
- 等価作業によるログイン防御:
- 存在しないメール、SSO アカウントなど、全ての失敗パスをダミーハッシュに対する bcrypt 検証に集約します。
- プリウォームにより、最初の実行でも一回限りのコストで済みます。
- ドメイン分離付き暗号化:
を介した AES-256-GCM で、コンテキスト文字を認証タグにバインドします。ring- 同一マスターキーでも用途ごとに封じ込め、出力時にバッファをゼロクリアします。
テスト戦略:現実を見守り、再検証する
約 1,030 のテストは、コードが間違えやすい箇所に集約されています。
- 対象: マニフェスト検証、IMAP パース、メールスレッド化など。純粋関数として、DB やソケットなしで実行可能です。
- 意図的ではないもの: DB 呼び出しのスタックなどはマスキが機能するかしか証明しないため、カバー率は薄く保ちます。
特に重視する二つのテストスイート
- データベーステスト: トランザクション内でロールバックし、残滓を残さず並列実行します。
- Lint-as-tests:
- テストがリポジトリを巡回し、書き込み関数が同步イベントを発行するか確認します。
- マーカーがない場合、ビルド自体を失敗させます。
v1 リリースに向けた課題
v1 に向けて以下の点に注意が必要です。
- モノリシックな構造:
は約 1,900 行の巨大ファイルですが、機能動作は保たれています(将来的に分離する計画あり)。main.rs - グレースフルシャットダウン: スキャフォールドはあるがワイヤリング未完成。シグナルハンドラー未実装で、デプロイ時に強制停止となることがあります。
- unsafe コード: 検索サービス内の
は信頼性が低く、将来的に危険なフィールドを隠蔽するリスクがあります。unsafe impl Send/Sync - エラー型: プラグインプロキシとメールワーカーの SMTP コード処理が文字列型エラーにフォールバックしており、ここはクリーンタイピングが緩んでいます(v1 で改善予定)。
結論:遅くても正しく構築する
Rust の徹底した精度が設計を支え、コンパイラがこの規模のバックエンドを正直に構築できる唯一の理由です。
- 時間配分: 「失敗クラスを再現不可能にするための努力を上流に費やすこと」よりも、「後でデバッグするための時間を使うこと」を好みます。
- 設計思想: コンパイラが強制してくれる方法で成果を得る一方で、一年間の取り組みは容易な道では耐えられない場所でもシステムを持続させる方法を教えました。
- 価値観: 肥大化して忘れられるものを出すよりも、自分が守りたいものを作るのに時間をかけるほうが好きです。
ソースコードは読むために公開されています。