Skip to content

fix(server): scope an unknown storage outcome to its key and branch - #1096

Merged
ragnorc merged 27 commits into
ModernRelay:mainfrom
ragnorc:fix/per-operation-storage-outcomes
Oct 21, 2026
Merged

ragnorc merged 27 commits into
ModernRelay:mainfrom
ragnorc:fix/per-operation-storage-outcomes

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1092. Implements §§1–6 of the draft RFC #1093 and should merge only after that RFC is accepted.

What & why

A storage request with no definite answer no longer stops the server: its uncertainty is scoped to the key it wrote. This closes steps 1, 2, 3 and 5 of #1067 for the served write path. #1092 closed the publisher half of the 2026-10-19 containment.

The design gives each fact about a request one owner, and every consumer reads it from there:

The changes:

  • Unknown publications are fenced by their version key; nothing records them. A publication whose manifest request no re-issue settles answers in doubt, and the branch keeps publishing. The manifest version is created only if absent, so the late request lands before the branch's next publication or never:
    • if it lands first, the next publication builds on it;
    • if the next publication takes the version first, the late request fails its precondition;
    • a race between the two is the ordinary version-CAS retry.
  • No containment for scoped unknowns. ApiError::contains_process separates an unknown storage outcome the engine scopes from one it does not.
    • Scoped, so reported but not contained: a publication in doubt, and an opaque storage or I/O failure (the scope holds its mutable keys unsettled).
    • Still contained: a panic, a lost owner, RecoveryRequired, an opaque DataFusion failure, and an uncertain deployment activation.
    • The log names the class and whether the write was contained.
  • Per-key scope. StorageIoScope drops its process-wide uncertain flag.
    • A mutable key whose request has an unknown outcome refuses later requests to that key, including a recursive delete over it, and nothing else.
    • The set is bounded at 4096 keys; past the bound every mutation is refused.
    • A clean release still requires an empty set.
  • Retries by class. The scoped Lance provider builds a retrying and a single-attempt client per store; the scoped control adapters do the same for S3 and Azure.
    • Reads and fresh immutable writes retry within Lance's defaults: three client retries and three AIMD retries, or three retries within three minutes for the adapters.
    • Conditional requests and mutable keys keep one attempt per request; their owners resolve ambiguity.
    • A refusal of a non-zero OBJECT_STORE_CLIENT_MAX_RETRIES stays.
  • Guard. forbidden_apis::mutable_key_requests_are_registered_issue_1067 registers every unconditional request to a reused key, with why its late landing is harmless. It covers the engine, core, catalog, storage, cluster, server and Azure admission crates.

What changed from the first draft. That draft kept each unresolved publication in a process registry. It refused the branch with a 503 and re-issued the publication from a server resolver.

  • CI's DST crash-state enumeration showed the registry landing a crashed operation's commit under its rerun, which had already captured its authority, so the rerun failed instead of converging.
  • The registry also repeated what the version key already guarantees. It is removed, along with its error, resolver and shutdown hold.
  • unknown_publication_lands_before_the_next_publication_or_never_issue_1067 covers both orders.

Backing issue / RFC

Checklist

  • Change is focused (one logical change: per-operation storage outcomes)
  • Tests:
    • storage scoped::tests::*_issue_1067: per-key refusal, bound, routing;
    • catalog unknown_publication_lands_before_the_next_publication_or_never_issue_1067;
    • server operations::tests containment matrix;
    • the mutable-key guard;
    • DST, including the crash-state enumeration.
  • Public docs:
    • operations/storage-failures.md: unknown publications, retries, when the server stops, logs;
    • deployment.md pointers, within its line cap;
    • control-plane.md and writes.md;
    • changelog.d/storage-outcomes-per-operation.changed.md.
  • Reviewed against invariants: publication is still one conditional __manifest commit with no second record of it; retries are class-bounded and logged.

Local verification

Head d1f65c5e, which carries #1092 at b017cc39 and main with Lance 13. Run with RUST_MIN_STACK=16777216 on macOS arm64; all pass.

  • Six-package suite (-p omnigraph-engine -p omnigraph-server -p omnigraph-cluster -p omnigraph-core -p omnigraph-storage -p omnigraph-catalog --features omnigraph-engine/failpoints,omnigraph-cluster/failpoints): 2725 passed, 0 failed. That includes forbidden_apis with the mutable-key guard.
  • DST:
    • dst_crash_state_enumeration_sampled, which failed in CI with the registry;
    • dst_publication_requests_get_a_definite_outcome_issue_1067;
    • the ack-loss, fault-storm and end-to-end scenarios;
    • the cost golden, unchanged.
  • Static checks: fmt, both workspace clippy graphs, check-docs.py, check-agents-md.sh and typos.

Notes for reviewers

A served write whose publisher failed before its conditional manifest put
was reported in doubt, and the server contained itself. On 2026-10-19 a
throttled `__manifest/_transactions/*.txn` PUT did this: Lance writes the
transaction file before the manifest and returns its error with a bare
`?`, so no manifest request was sent, but the publisher's read-back rule
called absence definite only for a failure before any store request.

Every OmniGraph dataset commits through one configured commit handler.
It now re-issues an ambiguous manifest put-if-absent unchanged, up to four
times, until the store answers it; a held version goes back to Lance's
transaction read-back, which tells this commit from another. A per-commit
probe records whether a request was sent and how it ended.

The catalog publisher derives each failure's completion evidence from that
record and the attempt's phase: no request sent, or one the store refused,
is `Unpublished`; a request that may have applied is read back, and only an
outcome the read-back cannot settle stays in doubt. Every other publisher
failure, candidate writes included, is `Unpublished`; a candidate tag
whose create may still land is in doubt.

The server's `ApiError` carries the engine failure's redacted class, and a
contained owned write logs it before admission closes.
The handler result frame is sized by ApiError, which a guard keeps at four
words. The redacted cause is logged where the engine error is translated,
and a contained owned write logs its status once.
…uests

A slot rule in the Lance-realm store fails, throttles, loses the
acknowledgement of, or lands late the graph's publication requests, which
the seeded weather cannot target. The scenario requires each to end
committed once, unpublished, or in doubt with nothing visible, and the
next write on the handle to publish.
The re-issue logs named the dataset's base path and the store's error text,
which can carry object URIs. They now record the request's object-store
error class, as every other server storage log does.
The deployment page is at its length cap. Storage outcomes get their own
operations page, which the deployment page's shutdown section points to.
A storage request with no definite answer stopped the whole process: the
storage scope kept one process-wide uncertain flag, and an in-doubt
publication or an opaque storage error contained the server until an
operator unlocked it.

- The storage scope tracks unsettled mutable keys instead of one flag. A
  key whose request has an unknown outcome refuses later requests to that
  key, including a recursive delete over it, and nothing else. The set is
  bounded; past the bound every mutation is refused, and a clean release
  still requires it empty.
- Requests route by class. Reads and fresh immutable writes go through a
  retrying client within Lance's defaults; conditional requests and mutable
  keys keep one attempt per request, and their owners resolve ambiguity.
- A `__manifest` commit whose manifest request no re-issue settles is
  pending on its branch. The process keeps the base and the exact Lance
  transaction; the branch's next publication re-issues it once and refuses
  with the typed `PublicationPending` while storage stays silent. The server
  re-issues pending commits on a backoff and once more at clean shutdown,
  which retains admission if any stay pending.
- The server contains only uncertainty the engine does not scope: a panic,
  a lost owner, `RecoveryRequired`, an opaque execution failure and an
  uncertain activation. A pending publication answers 503.
- A guard registers every unconditional request to a reused key across the
  crates that write storage, with why its late landing is harmless.
@cursor

cursor Bot commented Oct 20, 2026

Copy link
Copy Markdown

Reviewed f6d57e6d (the last commit, on 14461efd). The pull request is still a draft. Keep it in draft until the items below are fixed. No other review covers this head.

Why

One storage request with no answer stops the whole server today. This commit keeps that server up. It limits the doubt to the key or the branch that the request touched.

What

A manifest create that stays unanswered is pending on its branch. The process keeps the base dataset and the same Lance transaction. The next publication on that branch re-issues that commit once. While the store stays silent, later writes get HTTP 503 service_unavailable and write nothing. Reads, other branches, and other graphs continue. A mutable key with an unknown outcome refuses later requests to that key. Reads and fresh files use a retrying client. Conditional requests and mutable keys use one attempt.

How

StorageIoScope replaces the process-wide uncertain flag with a bounded set of unsettled keys. omnigraph_catalog::pending is process memory, not a second catalog. The server contains a panic, a lost owner, RecoveryRequired, an opaque DataFusion error, and an uncertain activation. It does not contain a scoped storage outcome.

Impact

A throttled fresh file or an unanswered manifest no longer takes every graph down. A pending write can still become visible after the client got HTTP 500. A clean shutdown that still has a pending publication or an unsettled key keeps the cluster lock. Restart without an operator still needs the lease. This commit does not add the lease.

Fix before this leaves draft

  1. A full pending registry can drop the publication it just failed to store.

pending::register returns false when the registry already holds 64 other branches, and it stores nothing. publish_attempts ignores that false result and still returns an in-doubt error. ApiError::from_omni marks that error scoped_unknown, so the process stays up and the branch still accepts writes. The manifest request can still land.

full() and register take the lock at different times. Two publications can both pass full() while the registry holds 63 records. The second register then returns false. The same drop happens when another branch fills the last slot during this attempt.

Fail closed. If register returns false, contain the process. The request is already sent, and this process can no longer name it.

  1. The resolver publishes outside admission.

resolve_pending_publications_until_stopped is a background task. It calls CommitBuilder::execute for a pending commit. Graph removal (delete_managed_graph_root) and the write drain do not read pending_publications. A removal can delete a root while the resolver creates that root's next manifest. Shutdown aborts the task and resolves once. Removal has no such wait.

Put the resolver in the same drain as admitted writes. Refuse root deletion while that root has a pending publication.

  1. This branch is Lance 11. Current main (85a46138) is Lance 13 (build(storage): upgrade lance to 13.0.0 #1072).

key_class follows Lance 11's table layout, including flat files under _indices/<uuid>/. Rebase onto current main and check the classifier, the zero-retry overwrite, and the manifest re-issue against Lance 13 before this leaves draft. #1092 needs the same rebase. I compared docs/dev/lance.md on the two commits. I did not compile this branch against Lance 13.

Optional

  1. On the last publish attempt (attempt == PUBLISHER_RETRY_BUDGET), a resolve that already committed or lost still returns PublicationPending. The message says the commit may still land and that the branch refuses writes. The registry entry is already gone. Continue after a resolved pending publication on that attempt too.

  2. RFC rfc: storage outcomes per operation and restartable ownership #1093 §2 asks for re-issue and read-back of refs and control objects. This commit does not add that. write_text_if_absent still returns on the first error. Mutation::finish treats every other mutable-key error as unknown, including a definite refusal. That key stays refused until a new process, and a clean shutdown then keeps the lock. Narrow the "implements §§1–6" claim, or give those requests the same refused-versus-unresolved split the manifest probe uses.

  3. A local delete_prefix records only the prefix path. unsettled_conflict refuses a later recursive delete that covers an unsettled key. It does not refuse a later write of a child after the prefix itself is unsettled. remove_dir_all has already stopped when it returns an error, so this is a retry hole. A definite IO error also marks the prefix unsettled, so the same delete cannot be retried in this process.

  4. Align this with the open siblings before either side leaves draft. feat(server)!: say on every error body whether the request ran #1076's outcome object must treat PublicationPending as not started. The HTTP 500 for the original in-doubt write stays unknown. Both this 503 and a closed admission use service_unavailable. feat(cli): serve a target for you on loopback #1083 and feat(cli): bridge an agent's MCP over stdio to a target #1084 still wait for this change before they allow writes. rfc: graph access and cross-process writes #1017 is the outcome model these engine results feed. The lease in RFC rfc: storage outcomes per operation and restartable ownership #1093 §7 is still absent, so a stopped process can still hold the lock.

Tradeoff and liability

This removes the failure mode where one request closes every graph. It adds a process-global registry of up to 64 Dataset values and transactions, a second object-store client per scoped store, and a background publisher. The registry is not catalog authority. The manifest put-if-absent remains the publication door. Lance 11 compares the full transaction on a conflict (verify_commit_outcome), so a re-issue of the stored transaction is this commit or a loss.

The new liability is the resolver and the ignored register failure. Five more process-global registries would be a second source of truth. Keep this as the one place a process remembers an in-flight manifest request. The extra client and the key set are the small mechanism. The registry can drift from the store, so the code must fail closed when it cannot remember a request.

Tests

GQT cannot express this contract. A .gqt store place hits StorageAdapter methods. It does not hit a Lance _versions/{n}.manifest put, and it cannot hold that put unanswered across the handler's re-issues and the next write. Process containment is server state. The owners are the storage scope tests, the catalog pending tests, the server operations tests, and the DST scenario dst_pending_publication_completes_once_storage_answers_issue_1067.

Same contract, before and after: a lost acknowledgement on one mutable key must still admit a different mutable key.

  • Before, 14461efd. I added scoped::tests::an_unsettled_key_refuses_only_itself_issue_1067 only for the run, then removed it.
    RUST_MIN_STACK=16777216 cargo test -p omnigraph-storage --lib scoped::tests::an_unsettled_key_refuses_only_itself_issue_1067 -- --exact
    Failed. The second put returned a prior storage mutation has an uncertain outcome.

  • After, f6d57e6d.
    RUST_MIN_STACK=16777216 cargo test -p omnigraph-storage --lib -- issue_1067 --test-threads=8
    16 passed, 0 failed. That includes an_unsettled_key_refuses_only_itself_issue_1067, requests_route_by_class_issue_1067, and unsettled_keys_are_bounded_issue_1067.

  • After, f6d57e6d.
    RUST_MIN_STACK=16777216 cargo test -p omnigraph-catalog --lib pending_publication -- --test-threads=4
    pending_publication_blocks_its_branch_until_the_store_answers_issue_1067 and pending_publication_lost_to_another_commit_releases_its_branch_issue_1067 passed.

I did not run the server operations tests, the DST scenario, clippy, or fmt. I did not run the catalog tests on 14461efd. That commit has no pending registry, so those tests do not build there.

Recommendation: keep this in draft. Fix the registry failure and the resolver drain, then rebase onto Lance 13.

…nce 13

Lance 13's WrappingObjectStore also wraps paginated listings; the fault
aims at puts only.
…t may land

After the last re-issue the handler returned the store's error and kept
reporting a missing version as definitive, so Lance read the version back
as absent and deleted the attempt's transaction file. A manifest that lands
later then names a file that is gone. Only the server's storage scope held
that delete back; embedded and CLI opens did not.

The handler now records the task whose request no re-issue settled, the task
Lance reads the version back on, and reports a missing version as not
definitive there. Lance returns CommitStatusUnknown and keeps the file. A
refused request and a commit that sent none keep the inner answer.
…tion-storage-outcomes

# Conflicts:
#	crates/omnigraph-catalog/src/tests.rs
#	docs/dev/writes.md
Lance deletes an attempt's transaction file when the handler calls a
missing version definitive, and reads that answer with no reference to
the attempt. The per-task marks that stood in for one relied on Lance
reading the version back on the task that sent the request.

The catalog's per-commit handler now answers from its probe, keeping the
inner answer unless the request may still land. A dataset's shared handler
holds no record of the attempt Lance reads back, so it never calls a
missing version definitive; Lance reports the status unknown and the
collector's orphan sweep reclaims a table's leftover transaction file.
A publication whose manifest request no re-issue settles was kept in a
process registry, refused its branch with a 503 and was re-issued by a
server resolver until storage answered. That registry repeated a guarantee
the version key already gives: the manifest version is created only if
absent, so the request lands before the branch's next publication or
never. The registry also landed a crashed operation's commit under a
rerun that had already captured its authority, so the rerun failed instead
of converging.

The registry, its error, the resolver and the shutdown hold are gone. An
unknown publication answers in doubt and the branch publishes on: the
next publication finds the late commit or takes its version first. A
regression test covers both orders.
@ragnorc

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Design change, now at head d1f65c5e: the pending-publication registry is gone. RFC #1093 is updated to match (§5, Alternatives and the decision log).

Why. CI's DST crash-state enumeration (dst_crash_state_enumeration_sampled) failed on the previous head. The registry, which is process memory, survived the harness's in-process revive and landed a crashed optimize's commit inside the rerun's publisher. The rerun had already captured its authority, so it failed with ReadSetChanged instead of converging.

The registry was also redundant. The manifest version is created only if absent, so an unresolved request lands before the branch's next publication or never:

  • if it lands first, the next publication builds on it;
  • if the next publication takes the version first, the late request fails its precondition;
  • a race between them is the ordinary version-CAS retry.

What's left. One owner per fact:

Removed: omnigraph_catalog::pending, OmniError::PublicationPending, the 503 path, the server resolver and the shutdown hold.

Evidence on d1f65c5e:

  • unknown_publication_lands_before_the_next_publication_or_never_issue_1067 covers both orders;
  • six-package suite: 2725 passed, 0 failed;
  • DST: dst_crash_state_enumeration_sampled and the publication, ack-loss, fault-storm and end-to-end scenarios, plus the cost golden;
  • fmt, both clippy graphs, docs checks.

aaltshuler and others added 2 commits October 20, 2026 22:43
…tion-storage-outcomes

# Conflicts:
#	crates/omnigraph-catalog/src/tests.rs
#	docs/user/operations/storage-failures.md
…rage-outcomes

# Conflicts:
#	crates/omnigraph-catalog/src/tests.rs
#	crates/omnigraph-server/src/lib.rs
#	crates/omnigraph-storage/src/scoped.rs
#	docs/dev/writes.md
#	docs/user/deployment.md
#	docs/user/operations/storage-failures.md

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed 9c18c8a8. No other review covers this head. I recommend merging this code once RFC #1093 is accepted. That RFC is still open. The three blockers from the review of f6d57e6d are fixed.

Why

One storage request with no answer used to stop every graph. This commit keeps the server up. The doubt stays on the key the request named, or on the manifest version the publication used.

What

StorageIoScope remembers unsettled mutable keys, up to 4096. A later request to one of those keys is refused before it is sent. Reads continue. A fresh file and a conditional manifest create still settle after an error. The manifest version is created only if it is absent, so a late publication lands before the branch's next publication, or it never lands. That fence is the commit's request record from #1092, plus the version key. This commit has no pending-publication registry.

How

S3 and Azure use two clients. Reads and fresh immutable writes retry inside Lance's bounds (three client retries and three throttle retries). Conditional requests and mutable keys use one attempt. OBJECT_STORE_CLIENT_MAX_RETRIES, when set, must be 0, because Lance applies that variable after the explicit options. The server reports an in-doubt publication and an opaque storage or I/O failure, and admission stays open (contains_process is false when scoped_unknown is set). A panic, a lost owner, RecoveryRequired, an opaque DataFusion error, and an uncertain activation still close admission.

Impact

A lost acknowledgement on one tag still admits a different mutable key. A client that repeats a non-idempotent write after HTTP 500 can apply it twice while the late manifest can still land. The response text tells the caller to look the commit up first. The structured body outcome_unknown with effects: unknown and action: reconcile belongs to #1076. This commit's HTTP code stays internal at status 500. MCP sets outcome_unknown: true when the write's completion_uncertain flag is set. That matches docs/user/operations/storage-failures.md.

Earlier review of f6d57e6d

  1. Resolved. The pending registry is gone. A full registry can no longer drop the publication it failed to store. OmniError::PublicationPending, the 503 path, the resolver, and the shutdown hold are gone.
  2. Resolved. The background resolver is gone, so it no longer publishes outside the write drain. The version key is the fence. A race with the next publication is the ordinary version compare-and-swap retry.
  3. Resolved. The merge base with main (d4aa7fe9) is d4aa7fe9. That is Lance 13. key_class treats flat files under _indices/<uuid>/ as immutable, which matches Lance 13 (LanceIndexStore writes those files directly in the UUID directory; nested paths stay mutable). Two comments in crates/omnigraph-storage/src/scoped.rs still say "Lance 11".

Optional items from that review:

  1. Resolved. PublicationPending is gone, so the last publish attempt no longer returns it after the registry entry disappears.
  2. Open. RFC rfc: storage outcomes per operation and restartable ownership #1093 §2 still asks for re-issue and read-back of refs and control objects. write_text_if_absent returns on the first error. Mutation::finish leaves a mutable key unsettled on every error other than a conditional conflict or a delete NotFound, including a definite refusal such as a permission error. That key stays refused until a replacement scope, and a clean shutdown then keeps the lock. Narrow the "implements §§1–6" claim, or give those requests the same refused-versus-unresolved split the manifest probe uses.
  3. Open. Local delete_prefix records an absolute filesystem path (ObjectStorageAdapter::delete_prefix). Later object keys are relative. On this head a failed local purge unsettles only that absolute path, so the new prefix check does not cover children, and the same purge cannot be retried in this process. On main the same failure poisons every mutation, so the path mismatch did not matter. Cloud list-and-delete tracks each child key. Purge already keeps writers excluded. remove_dir_all has already stopped when it returns the error.
  4. Partly. feat(cli): serve a target for you on loopback #1083 and feat(cli): bridge an agent's MCP over stdio to a target #1084 are closed. The 503 path is gone. This commit keeps completion_uncertain set for an in-doubt write and for opaque storage or I/O, and scoped_unknown keeps the process serving. feat(server)!: say on every error body whether the request ran #1076 at 15eb6699 rewrites the code to outcome_unknown when the disposition action is reconcile and completion_uncertain is set, and it sets effects to unknown for an owned write in that state. rfc: graph access and cross-process writes #1017 assigns that wire body to feat(server)!: say on every error body whether the request ran #1076. Ship feat(server)!: say on every error body whether the request ran #1076 in the same release, so a client does not treat code: internal as safe to replay. feat(server)!: say on every error body whether the request ran #1076's merge path uses into_step_output(); this commit's source-delete path uses into_output() and contains_process(). That will conflict when the two branches meet. main does not need a rebase for it now.

This head

Blocking findings: none.

Optional:

  1. The §2 gap above (refs and control objects, and definite refusals left unsettled).
  2. The local delete_prefix key space above.
  3. OperationRuntime::submit logs "owned write ended without a definite outcome" twice for a contained write. The first log is the new one (contained = result.uncertain). The second is the old log inside if result.uncertain.
  4. Ship feat(server)!: say on every error body whether the request ran #1076 with this commit, as above.

Tradeoff and liability

This removes the failure where one request closes every graph. It also removes the process registry that the earlier review blocked on. Three facts have one owner each: the commit's request record, the version key, and the unsettled-key set. Catalog production code is unchanged. The extra client is one more client per scoped S3 or Azure store. Past 4096 unsettled keys, every mutation is refused and reads continue. After an unacknowledged manifest write, transaction-file deletion stays held for the life of the scope.

The remaining liability is a client that replays HTTP 500 while the late manifest can still land. The version key keeps the branch safe: the next publication builds on that version, or it takes the version first and the late put fails with AlreadyExists. The caller's second write is a new commit. Another process-global registry would be a second source of truth. This design does not add one.

Rebase against main

No rebase is required. git merge-base 9c18c8a8 d4aa7fe9 is d4aa7fe9. #1092 is already that commit. There is no second publication-outcome implementation in this diff.

Tests

GQT cannot express this contract. A .gqt store place hits StorageAdapter methods. It does not hold a Lance _versions/{n}.manifest put unanswered across a re-issue and the next write. Process containment is server state. The owners are the storage scope tests, the catalog publication test, and the server operations tests.

Before, d4aa7fe9. I added scoped::tests::an_unsettled_key_refuses_only_itself_issue_1067 only for the run, then removed it.

CARGO_TARGET_DIR=/workspace/target RUST_MIN_STACK=16777216 cargo test -p omnigraph-storage --lib scoped::tests::an_unsettled_key_refuses_only_itself_issue_1067 -- --exact --nocapture

Failed. The second put returned a prior storage mutation has an uncertain outcome.

Before, same commit. I added operations::tests::unmarked_storage_io_keeps_admission_open_issue_1067 only for the run, then removed it. The same command with -p omnigraph-server --lib and that exact name failed. Admission stayed closed.

After, 9c18c8a8, from a clean checkout of that commit.

CARGO_TARGET_DIR=/workspace/target RUST_MIN_STACK=16777216 cargo test -p omnigraph-storage --lib issue_1067 -- --test-threads=8

16 passed, 0 failed. That includes an_unsettled_key_refuses_only_itself_issue_1067, requests_route_by_class_issue_1067, unsettled_keys_are_bounded_issue_1067, and key_class_follows_lance_table_layout_issue_1067.

After, same commit. The catalog test binary's tests::unknown_publication_lands_before_the_next_publication_or_never_issue_1067 --exact passed in 6.42s. Both orders (late create first, and next publication first) are in that test. I did not run it on d4aa7fe9: try_land and heal are new here, and the fence itself is already on main through #1092.

After, same commit.

cargo test -p omnigraph-server --lib operations::tests::definite_unpublished_storage_failure_keeps_admission_open_issue_1067 -- --exact

Passed.

cargo test -p omnigraph-server --lib operations::tests::typed_engine_uncertainty_controls_capacity_independently_of_status -- --exact

Passed.

CI on this exact SHA is green: Test Workspace, clippy, rustfmt, GQT (ordinary, dst, served), DST pinned suite, S3, Azure, storage upgrade, cargo deny, and the Fix Regression Gate. Run 39499326884 has headSha 9c18c8a82bbb1940f54bdfb066dec7352a404f1a. I did not re-run those suites locally, and I did not run DST.

Recommendation

Approve. Merge after RFC #1093 is accepted. The code on 9c18c8a8 is ready, and the pull request is mergeable with main.

Main made ApiError's status a method; the owned-write log line read it as
a field.
@ragnorc

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Merged main at d111c403 (#1076, error dispositions), which conflicted in the server.

  • c6f01655 takes main's ApiError and re-applies this PR's change on it:
    • the scoped_unknown flag, set in from_omni for an uncertain storage or publication outcome;
    • contains_process();
    • the merge source-branch delete deciding containment by contains_process().
  • An unknown storage outcome still answers outcome_unknown with effects: unknown and action: reconcile through feat(server)!: say on every error body whether the request ran #1076's disposition. Only containment changes.
  • 7125d324 calls status() where the owned-write log line read the old field.

Verification on 7125d324, with RUST_MIN_STACK=16777216, macOS arm64:

  • cargo fmt --all --check: clean.
  • Six-package suite (engine, server, cluster, core, storage, catalog, both failpoint features): 2754 passed, 0 failed.
  • DST: 6 passed (the publication, crash-state enumeration, ack-loss, fault-storm, end-to-end scenarios and the cost golden).
  • Both workspace clippy graphs: clean.

ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 21, 2026
Accepted on the maintainer's instruction after review approved the RFC,
ModernRelay#1096 and ModernRelay#1097. The owned server operations and engine settlement RFCs
get disposition notes for the parts this replaces: unknown storage
outcomes no longer close admission, ambiguous storage outcomes are fenced
by re-issue and the version key instead of fail-stop, and a server's S3
lock is leased.

Section 7 also says, per the approving review, that a claim proves
nothing about its writer and that the owner's timer, not the claim, is
the fence.
@ragnorc
ragnorc added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 21, 2026
@ragnorc

ragnorc commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Merged main again (f027c1c6), after #1117 changed the same two log lines:

  • from_omni sets scoped_unknown, then takes fix(server): log the status an error response sends #1117's with_request_facts, so the warning logs the status the response sends and whether the outcome is scoped.
  • The owned-write log is now one line for every write whose outcome is unknown, with contained saying whether it closed the process. That also removes the duplicate line the review of 9c18c8a8 noted (optional 3).
  • The storage-failures page keeps this PR's behaviour (the server keeps serving) with fix(server): log the status an error response sends #1117's wording about the sent status.

Verification on f027c1c6, RUST_MIN_STACK=16777216, macOS arm64:

  • fmt and both workspace clippy graphs: clean;
  • six-package suite (engine, server, cluster, core, storage, catalog, both failpoint features): 2757 passed, 0 failed;
  • DST: 6 passed.

Re-queued.

Merged via the queue into ModernRelay:main with commit 626cdfb Oct 21, 2026
32 checks passed
@cursor cursor Bot mentioned this pull request Oct 21, 2026
6 of 7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants