Sealed manual job can complete after client disconnect but remain permanently INCOMPLETE
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 25/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- docker, go
- 領域
- cli, devops, infrastructure
調査の方向性
internal/engine/job.go の RunJobWithJournalID と runJobPhase、internal/engine/gate.go の Docker コマンドパス、そして既存の systemd の runner/status 処理から始めてください。どの操作を reconcile するのか、またコンテナの証拠を利用できない場合にどの終端状態が正直なのかを決める前に、AcquireLock と operation journal のライフサイクルを追跡してください。完了の条件は、切断によって所有権が孤立しないこと、永続化された結果を operation ID で検査できること、そして列挙された mutation と disconnect のケースにテストがあることです。
索引モデルが issue の本文から書いたものです。
説明
What happens
A sealed manual ob job run is executed through the attached client/SSH context. If that context is interrupted during a long job, the Docker workload can continue and complete on the managed host, while Onebox never writes a terminal journal record. ob audit then reports the operation as INCOMPLETE permanently even though the application effect succeeded.
This is more than a display problem: once the controlling process and heartbeat disappear, Onebox can lose authoritative ownership of a still-running data-changing container. A later operation must not infer that the work stopped or permit unsafe overlap merely because the client disappeared.
Observed
Goal production, sealed destructive job catalog-refresh, 2026-09-09:
- Onebox journal start:
2026-09-09T05:32:57Z - Application dataset ledger created during the run and activated at
2026-09-09T08:38:59Z - Activated Open Food Facts dataset: 4,275,407 accepted records and 460,192 rejected records
- API
/healthzand/readyzboth returned HTTP 200 after activation - No catalog worker remained active
ob auditstill showed operation20260909-053225-54af549-job_run-219d9e8c182casINCOMPLETE
The domain evidence proves that the refresh reached activation. The missing terminal Onebox record means the wrapper exit status itself is unknowable after the fact.
Cause in the current lifecycle
RunJobWithJournalID writes the start record, calls the blocking attached runJobPhase(ctx, ...), and only then calls finish(runErr) (internal/engine/job.go). The finish writer uses the same caller context:
runErr := e.runJobPhase(ctx, writer, nil, remoteDir, remoteCompose, "job", []string{job})
return operationID, result, finish(runErr)
and:
if journalErr := writer.Append(ctx, record); journalErr != nil {
return errors.Join(runErr, fmt.Errorf("journal job finish: %w", journalErr))
}
The underlying command is an attached docker compose run --rm --no-deps (internal/engine/gate.go). Cancellation therefore removes the only actor responsible for appending the finish record. Using the cancelled context for cleanup also means that even a surviving client stack cannot reliably journal interruption.
Expected
A client or SSH disconnect must produce one of two explicit, durable outcomes:
- the host-supervised job continues, retaining coordination ownership, and records its eventual success or failure independently of the client; or
- Onebox reliably stops the workload and records an interrupted/unknown terminal outcome.
It must never leave a live or completed data-changing workload behind an indefinitely INCOMPLETE client-owned operation with no reconciliation path.
Suggested minimal direction
Run approved sealed manual jobs under a host-side supervisor, reusing the systemd runner/status machinery already used by scheduled and opt-in durable jobs. The approval, resolved release/runtime digest, data effect, fence, result protocol, and operation ID remain plan-bound; the CLI submits the approved execution and may attach to logs, but is not its lifetime owner.
A bounded non-cancelled cleanup context for the terminal append is still worthwhile, but it is not a complete fix when the CLI process itself exits. The durable authority must live on the managed host.
Do not infer success merely because the container disappeared. Reconcile the supervisor, container identity, result evidence, and journal state; preserve an honest interrupted/unknown state when zero exit cannot be proven.
Progress
Landed — #180 (2128338). One-off job containers now carry ob.operation and
ob.epoch, so a container on the host can be matched to the journal that started it —
nothing downstream is possible without that. An interrupted run records a terminal
interrupted record on a bounded background context, retried once if the caller's context
dies mid-write. And the application lock is held rather than released when an interrupted
run leaves its container alive: ReleaseLock runs on its own background context, so on
Ctrl-C it previously succeeded while the journal append failed, dropping ownership
immediately over a live data-changing container. That correction is worth noting against
this issue's framing above, which attributes the loss of ownership to heartbeat death and
TTL expiry — true for kill -9 and SIGHUP, but for Ctrl-C the lock was gone at once.
Open — #182. The refusal only: ob deploy and ob job run refuse while another
operation's job container is running. It asks Docker rather than reducing the journal,
because a journal records what a client managed to write and the failure this exists for
is a client that did not write. Matching is on operation and epoch — a sealed plan
carries one operation id for its whole life and is re-runnable, and AcquireLock returns
the lock to a caller presenting the id already in it, so a second run of one plan would
otherwise exempt the container its own earlier run left behind.
Withdrawn — #181. It paired the refusal with reconciliation (writing the terminal
record for runs whose client never did). Five review rounds found seven defects, all of
them in the reconciliation half, and a mutation test showed every call site could be
deleted with the suite green. The refusal survived and is now #182; the reconciliation is
back to unstarted below.
Not started — reconciliation. Closing a run whose client never recorded an outcome, so
it stops being INCOMPLETE forever. Worth building, but it wants designing once rather
than repairing repeatedly, and it needs three decisions #181 kept deferring: which
operations reconcile, what an honest terminal record says when the container is --rm and
its evidence is gone, and how ob audit should group a journal whose invocations
interleave.
Not started — host-side supervision. Neither PR makes execution durable. A client
killed outright still records nothing at the time; #181 only lets the next operation
close it honestly. Acceptance items 3, 4 and 5 below are untouched.
Acceptance
- Killing the local CLI or SSH connection during a sealed manual job cannot orphan coordination ownership.
- A continuing host-side job remains protected from overlapping deploys or job runs.
- …and from every other operation that mutates under the same lock. Twenty-one call
sites take the application lock; two check.ob backup restoreandob destroyare
the sharpest omissions — restore's own contract says it must not interleave with a
deploy or a recovery, and restoring a volume under a live migration container is
exactly that.ob rollback,ob abort,ob service apply,ob exec,
ob secrets push,ob schedule runand bootstrap are the rest. Rolling a release back while an orphaned migration container is mid-flight is
the same hazard as deploying over it. The natural fix is to hoist the check to a point
every mutating path already passes through —AcquireLockis the obvious candidate,
with care for bootstrap, which takes the lock on a host where Docker may not yet be
reachable. - Eventual zero/non-zero exit is durably recorded without the initiating client.
- The operator can reconnect and inspect or follow the same operation ID.
- Tests cover disconnect before container launch, during work, after domain effects but before result commit, and after result commit but before terminal journaling.
- Scheduled-job containers are visible to the same check. They are created by their
owncompose runand carry noob.operation/ob.epoch, so nothing can see them and
they run without checking either. - Migration/destructive approval and plan-staleness guarantees are unchanged.
Related, but not duplicates
- #160 / #161 added durable executions for opt-in scheduled native jobs restricted to
data_effect: none; sealed destructive/migration manual jobs remain on the attached path. - #166 adds a per-job history surface but does not make execution or terminal journaling durable.
- #165 mentioned detached manual runs only as a possible follow-up in a closed, unrelated secret-rotation issue.
- 主要言語
- Go
- スター
- 4
- フォーク
- 0
- 平均マージ
- 2時間 46分
- マージ済み PR(30日)
- 38
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
labstack/onebox のほかの issue
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 80/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 3/5 1〜2日 初心者へのやさしさ 65/100
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 55/100
メンテナーはふだん 1 日以内に返信
labstack/onebox の issue をすべて見る
似ている issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
homarr-labs/dashboard-icons#2939 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
AvengeMedia/DankMaterialShell#3761 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
メンテナーはふだん 1 日以内に返信
-
docs: clarify replacement for outdated Terms of Service URL in readme.md対応中かも @mohitjoshi-hey が今日担当しました。 オープン
難易度 1/5 1〜3時間 初心者へのやさしさ 66/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
runkids/skillshare#530 ·
メンテナーはふだん 1 日以内に返信