プロフィール DavidHLPL · RHINE LAB RSS ↗
資料 X-1137 / プロジェクト実践

UltiCode:オンラインジャッジと Agent 工学の検証可能な境界

技術ノート

公開 / DavidHLPL

証拠の状態:主な出典は UltiCode の main の固定コミット f801a1076 で、初期 README/CONTEXT のスナップショットを補助に使う。「実装済み」はソース、設定、テストがあることで、本番配置済みという意味ではない。

範囲:リポジトリ文書は本番環境、トラフィック、認証情報がないと明記している。スループット、HA、ゼロ損失、完全なサンドボックス脱出防止は主張しない。非同期 receipt の複数インスタンス・再起動をまたぐ永続的な冪等性も既知の未完成事項である。

UltiCode は Console と Management の入口を持つオンラインジャッジである。Auth、Admin、App、Submission、Notification はデータ責任を分担する。Judge Worker は提出を消費し Docker サンドボックスを実行して結果を書き、Search Worker は派生検索インデックスだけを維持する。

注目点は、サービス間の事実、配送失敗、再判定の競合、期限切れリース、コンテナ基盤の障害、リリース経路を、検査可能な状態として扱うことにある。

UltiCode 自体は LLM 製品ではないが、不信入力、非同期実行、冪等性、隔離、証拠の扱いは、生成コード、ツール呼び出し、長時間 Agent タスクにも応用できる。

シリーズ:オンラインジャッジを九つの検証可能な問いに分ける

この entity ページは全体図を示す。九つの concept は単独でも、「事実 → 配送 → 実行 → リリース」の順でも読める。

八つの設計上の特徴

特徴 問題 仕組み
owner 境界 読み書きの所有権が口約束になる Data owner、implementation-free contract、port/projection
監査の回復性 業務は commit しても他 owner の監査が残らない owner-local outbox、Stream、consumer inbox、冪等キー
配送の回復性 提出成功時にキューが使えない DB outbox、FOR UPDATE SKIP LOCKED、Streams、再試行・dead-letter
結果の fence 再判定や古い worker が新結果を上書き generation + attemptId + 条件更新
単例タスクの fence 引き継ぎ後に古い worker が完了を書く DB lease、単調増加 fence_token、条件更新
実行隔離 基盤障害を通常のユーザーエラーと誤認 Docker 資源・安全設定、seccomp、分類
モジュールの寿命 複数 context の timeout、遅い応答、二重 close opt-in、allowlist、bounded timeout、close-once handoff
リリースの証拠 ビルド成功では依存やイメージの安全性は分からない zero-infra static contract、Trivy、SBOM、Cosign、provenance

1. Owner 境界:書き込みの所有者を契約にする

文書は五つの Data Owner と二つの Worker を定める。

役割 所有する事実・書き込み 担当しないこと
Auth ログイン、登録、OAuth、認証情報、refresh 状態、RBAC、JWKS 他 owner の業務テーブル
Admin 管理 BFF、監査、設定、監視、バックアップ、read model App・Submission のドメイン書き込み
App プロフィール、Problem、Contest、Solution、Forum、交流、実績、購読 Submission の結果
Submission 提出、判定状態、generation、lease fence、結果、judge outbox Problem、TestCase、Contest テーブル
Notification 通知、設定、配信 ledger、メール、Inbox、再試行 通知状態を各 owner に分散させること
Judge Worker Judge Stream、Problem facts、サンドボックス、verdict 業務テーブルへの直接書き込み、業務 HTTP 入口
Search Worker SearchDocumentChanged、MeiliSearch 派生索引 業務テーブル書き込み、業務 HTTP 入口

境界を越える際は、Entity、Mapper、業務 Service の共有ではなく provider-owned contract または consumer-owned port を使う。services/api/* は実装を持たない契約、services/platform/* は制約付き共有層である。judge-runtime は再利用依存で、独立プロセスを必ず意味するわけではない。

不変の事実スナップショットで依存を短くする

Submission の書き込み中に App/Auth を問い合わせると、遠隔呼び出し、権限境界、要求タイミングが書き込み経路へ混入する。

intake は SubmissionFactsSnapshot を受け取る。App の RemoteSubmissionWritePort が先に捕捉する項目は:

  • userId と userExists。
  • 問題 ID、タイトル、slug、時間・メモリ制限、starter code。
  • capturedAtEpochMillis と schemaVersion。

Submission は facts.admits(requestedUserId, requestedProblemId) で schema、ユーザーの存在と ID、問題 ID を検査する。出所、捕捉時刻、解釈する版を暗黙の前提からデータへ移す。

有効期限、owner 間の取り消し、業務規則は別途定義が必要で、完全な認可・一貫性の証明ではない。それでも主要トランザクション内の場当たり的な遠隔問い合わせよりテスト・監査しやすい。

2. Outbox + Redis Streams:配送失敗を状態にする

judge outbox 有効時は提出と判定意図を同じ DB トランザクションに保存する。

submit @Transactional
    ├── submissions: Pending
    └── judge_outbox: PENDING
             │
             ├── claim: FOR UPDATE SKIP LOCKED
             ├── atomic SET + XADD
             └── judge-workers
                    └── lease fence -> verdict

JudgeOutboxMapper は PENDING と次回再試行時刻で取得し、FOR UPDATE SKIP LOCKED で競合を避ける。既定バッチは 50、周期は 2 秒で、本番性能の約束ではない。

JudgeOutboxDispatcher は次を区別する。

  • queue provider 不在、enqueue 例外:再試行可能なまま残す。
  • problemId、userId、language、code を欠く payload:送信済みとせず DEAD。
  • 指数バックオフ:現在の上限は 60 秒。
  • real dispatch:cutover watermark と shadow フラグで移行用タスクと実タスクを分ける。

Streams adapter は Lua で重複排除キー設定と XADD を原子的に行う。キーには submissionId:generation を含め、SETNX 成功後 XADD 前のクラッシュによる配送不能な穴を防ぐ。

DB と外部 Redis 操作には境界が残るため exactly-once とは呼ばない。永続意図、再試行可能な配送、メッセージ重複排除、古い・重複した実行を止める結果 fence の組み合わせである。

PEL 回収で未確認メッセージを見えるようにする

consumer group は作成前の項目も見えるよう 0-0 から開始する。NACK は Pending Entries List(PEL)を残す。UnackedStreamEntriesReaper は既定で 10 秒ごとに idle 項目を走査し、sweep ごとに最大一件を回収する。PEL サイズ、キュー遅延、最古項目の年齢、DLQ を観測する。

visibility timeout は 1_800_000L(30 分)。30 分内完了や HA の証明ではない。回収後も generation/attempt fence を通り、再取得しただけでは書き込み権限を得ない。

3. generation + attemptId:DB の入口で古い結果を無効にする

再判定、リース失効、worker 再起動、メッセージ再配送があるため、提出 ID が同じでも有効な実行は一つとは限らない。

  • generation:論理提出または再判定の epoch。
  • attemptId:worker の実行リース token。
acquire lease:  id + status=Pending + generation
renew lease:    id + current_attempt_id
write verdict:  id + generation + current_attempt_id
rejudge/reaper: expected generation -> generation + 1

SubmissionMapper の lease、更新、verdict SQL は条件更新を使う。影響行数 0 はリース喪失、古い generation、引き継ぎを意味し、上書きを続けない。実行器は judge.stale_result.dropped を記録し、安全に破棄した旧結果を ACK する。

SubmissionRejudgeService と JudgingLeaseReaper は generation CAS で epoch を進め、新しい判定意図を作る。遅着した旧メッセージは新条件を満たせない。

上流の完全な一回実行を要求せず、最終書き込み資格を検査可能な条件に集約する。ただし Submission owner の DB に限る fence で、システム間認可、メッセージ永続性、業務監査の代用ではない。

4. Docker D-form:資源を隔離し基盤障害を分類する

SandboxExecutorImpl は一時 job workspace と読み取り専用 input.json を作り、lifecycle runner から Docker を起動し、finally で可能な限り後始末する。

--network none
--cap-drop ALL
--read-only
--user 1000:1000
--security-opt no-new-privileges
--security-opt seccomp=<resolved profile>
--memory <effective limit>
--cpus <effective limit>
--pids-limit <effective limit>
--ulimit nofile=128:128
--tmpfs /tmp:rw,exec,size=64m
<workspace>:/workspace:ro
--rm

seccomp profile の解決失敗ではフィルターを黙って外さず、明確な Docker 設定エラーにする。見かけ上の実行成功のために隔離を暗黙に弱めない。

SandboxOutcomeClassifier は次を分ける。

  • プロセス起動自体の失敗:launch failure。
  • exit 137:OOM。
  • exit 125:OCI 設定または Docker daemon エラー。
  • Cannot fork、pids、RLIMIT_NPROC:fork limit。
  • コンパイラーのエラー:compile error。
  • その他の非ゼロ終了:通常の runtime error。

コンテナ未作成、seccomp パス誤り、fork 資源不足をユーザーコードの Runtime Error と偽らない。診断・再試行を説明可能にするが、完全な脱出防止は専用監査と実環境検証が必要である。

5. 同期プレビューと非同期実行で contract を共有する

即時プレビューと長時間タスクで要求・状態・エラーを別々に定めると、同じコードでも入口ごとに挙動が変わる。

AsyncSandboxExecutor は submit/poll/cancel を定義する。ExecutionRequest は job、test case、visibility、空でない idempotency key を必須とする。既定 key は job.runId:testCase.id、要求には SHA-256 fingerprint も付く。同じ key で異なる payload は誤った結果の再利用ではなく競合として検出できる。

Judge の CodeExecutionProvider は入口で制約する。

  • 同期 execute は PUBLIC_PREVIEW のみ。
  • 業務例外を型付き RpcResult にし、任意文字列の解析を不要にする。
  • 非同期提出は一つの test case と有界のメモリ metadata。
  • 上限 1024 件、TTL 15 分。完了・取消・timeout 後に回収。
  • Docker が既定、Judge0 は任意 adapter。

生成コード、ツール引数、長時間 Agent の結果にも、明確な状態機械、冪等キー、容量上限が必要である。モデル・ツール呼び出し関数を無制限な全体タスク管理へ膨らませない。

非同期 receipt はまだプロセス内の有界 metadata であり、複数 replica・再起動をまたぐ永続冪等性は未完成。外部 Judge0 も既定では検証されていない。

6. 品質ゲート:静的契約と重い検証を分ける

scripts/dev/test.sh と文書はコストと現実性で段階を分ける。

static       -> zero-infra contract checks
unit         -> unit tests, exclude IT/IntegrationTest
quick/full   -> progressively heavier local checks
integration  -> Testcontainers, DB/Redis, sandbox and owner migration

CI の zero-infra 入口:

bash scripts/test/zero-infra-validation-contract.sh --static-only

静的経路は Docker、DB、サービス、Testcontainers、Maven、pnpm install を起動しない。スクリプト契約、パス、設定、安全境界を小さな変更ごとに確認する。DB、Redis、サンドボックス、owner 横断の挙動は unit/integration に残す。静的ゲートは統合証拠の代用ではなく、統合テストも本番トラフィックの証拠ではない。

docker-publish.yml はサービス行列から GHCR イメージを作り、SBOM と provenance を有効にする。Trivy は HIGH/CRITICAL の OS/library 脆弱性を終了コードで阻止し、Cosign は digest を署名し署名・attestation を検証する。GHCR 名は先に小文字に統一する。

これらは自動化された安全・配送ゲートであり、CI がシステム全体の安全性を証明したとは言えない。実行記録と外部認証情報で実際の完走を確認する。

7. LLM/Agent への四つの示唆

UltiCode の対象は prompt ではなく提出コードだが、四つの直接的な類比がある。

7.1 不信入力を能力境界に通す

生成コード、ツール引数、パッチ、外部ページにも能力・資源・データ範囲の境界を設ける。owner + port + adapter は事実の所有者と実行権限を、system prompt の願望でなく型・インターフェースの制約にする。

7.2 非同期タスクには永続意図・冪等性・fence が必要

timeout、再試行、重複配送、取消がある。Outbox は意図、idempotency key は重複要求、generation/attempt fence は古い worker の書き戻しを扱う。メッセージが重複しないことと、結果が古くないことは異なる。

7.3 事実スナップショットは暗黙の文脈より再現しやすい

SubmissionFactsSnapshot は要求時の事実、捕捉時刻、schema を渡す。Agent も可視資源、権限、モデル版、入力要約、実行方針を版付きスナップショットにでき、同じ ID が時刻で別の対象を指す曖昧さを減らせる。

7.4 失敗の分類が自己回復を決める

OOM、OCI、fork 制限、コンパイル失敗を一律に扱うと監視・再試行の判断を失う。LLM も provider 制限、権限拒否、入力不正、出力解析不能、業務失敗を区別する。FAILED 一語より観測可能な状態が役立つ。

8. 実装・検証・主張できない事項

ラベル 正確な表現
Repository Implemented 固定コミットに owner/worker 文書、事実契約、DB outbox、Streams 重複排除・PEL 回収、generation/attempt CAS、Docker 安全設定、分類、CI ゲートがある。
Repository Evidence 対応するソース、設定、境界テストがある。テスト名は追跡可能な証拠で、今回の実行成功ではない。
Locally Validated ブログの KB lint とビルドを検証する予定であり、UltiCode の Maven、Docker、Redis、Testcontainers 検証とはしない。
BLOCKED_EXTERNAL 本番配置・流量、外部 Judge0、実際の複数 replica・再起動 receipt、認証情報、イメージ実行環境、リリース用の認証情報は対象外。
OUT_OF_SCOPE 高スループット、本番 HA、ゼロ損失、完全な脱出防止、任意 Core profile の本番品質は主張しない。

状態は provisional のまま。強い結論には static/unit/integration の再現可能な記録、Docker/Redis/MySQL の実結果、再起動・複数 replica の receipt 検証、別個のサンドボックス監査が必要である。

最小限の読解順

配置図から推測せず、次の順にソースへ戻る。

  1. 構成概要 → owner とモジュール境界。
  2. 事実スナップショット → Submission 書き込み。
  3. outbox dispatcher → Streams adapter → attempt executor。
  4. Docker 実行器 → エラー分類。
  5. 検証状態 → テスト階層。

対応する境界テストの入口

次の名前は追加確認のためであり、今回は UltiCode の代わりに実行していない。

出典と関連文書

研究記録

  1. 元の文書: src/content/note/ja/ulticode.md
  2. 出典リビジョン: f49953fb0d4bd52a39bf81167b0c4b7dd7d78a64
  3. 証拠の状態: 暫定

原文の出典 ↗