Repository navigation
feat(server): PUT and DELETE one Blob cell over HTTP - #922
Conversation
Every keyed write checked one 32 MiB ceiling on a batch's whole Arrow memory, which includes a managed Blob value's bytes, so a value of exactly 32 MiB never fit beside its row. KeyedBytes splits that count into payload (each managed value's logical length) and framing (the rest), each under its own 32 MiB ceiling, and every keyed check uses it: staging, keyed-write preparation, the update carry scan, branch merge buffering and proven-insert chunking, and the compatibility loader's pre-decode forecast. The halves sum to the old count. A payload refusal names its resource with 'Blob payload bytes' in place of 'bytes'. Two Lance guards pin what the Blob write plan relies on: a whole-row merge-update keeps the row's stable row id, and merge-insert refuses an external reference outside the dataset's bases.
Session::put_blob_at_as and clear_blob_at_as replace or clear one Blob value of an existing node or edge, addressed by exact id like a Blob read. A cell write is a Mutation-protocol write: it stages one upsert of the carried row and runs the Mutation tail, now split into commit_staged_mutation and publish_committed_mutation, which mutate uses too. The target's old payload is never read; the row's other cells are carried as an update carries them and share the 32 MiB payload allowance with the new value, which is bounded at 32 MiB inclusive before anything is captured. An optional precondition (an ETag set, or any existing value) is evaluated against the attempt's base and re-evaluated when a competing commit forces a re-prepare; a change of branch incarnation or accepted schema between attempts fails closed. The receipt's ETag is read from the detached commit before publication.
PUT and DELETE /graphs/{graph_id}/blob replace or clear one Blob value of
an existing node or edge through the engine cell writes, each as one graph
commit under `change`.
PUT is a raw ingress route like /load/ndjson: it authorizes, checks the
media type (415) and a declared length (413) before polling the body, then
collects at most 32 MiB under the body deadline (408) into one buffer the
engine adopts, and shrinks its ingress reservation to the received size.
Both verbs take a branch, refuse snapshot and unknown parameters, run as
owned writes, and answer the exact receipt. If-Match is parsed by a shared
parser in the API types; a failed one is a 412 with the new
blob_precondition_failure detail and the current ETag header.
ragnorc
left a comment
There was a problem hiding this comment.
Recommendation: changes needed before merge. One blocking finding is inline (P2). No additional correctness defect established.
This PR lets a remote client replace one existing node or edge Blob with raw bytes, or clear a nullable Blob. PUT avoids a base64 envelope. DELETE returns a null state. Both enforce branch change permission, support Blob If-Match, and return the exact publication receipt; clearing an already-null cell can return commit: null.
Design and tradeoffs: the routes reuse the existing server-owned operation lifetime and Mutation publication path. This keeps disconnect handling, admission, and graph publication under their existing owners. The engine checks the selected cell and its precondition on each permitted fresh attempt. It stages detached Lance effects before graph publication. This addresses the general ownership problem without a second write protocol. See operation submission, Blob write owner, and Mutation publication.
The new verbs, receipt DTO, header parser and error detail add a lasting compatibility and test obligation. Sharing their helpers and the payload limit reduces duplication. The 32 MiB body bound is not a bound on total process memory. Carrying the other Blob values can also consume the aggregate payload budget or require an allowed external read. These are concrete costs of reusing the current entity rewrite path. Five similar routes should keep using these same owners rather than add independent admission or publication state. I found no measured throughput or heap result to assess.
The blocking defect violates the existing rule that every public vocabulary occurrence must be classified. Fix all five source descriptions and regenerate OpenAPI. Replacing the public prose's row with entity passes the existing guard without adding exceptions. That removes the failure at its source and adds no new state, API or inventory liability.
Validation on the exact reviewed head 04b9e6675bad6fa8d04f6f137e3f1e3efb6f2c47:
- Local:
RUSTUP_TOOLCHAIN=1.97.1 CARGO_TARGET_DIR=/tmp/review-922-guard-target cargo test -p omnigraph-vocabulary-guard --locked --lib tests::openapi_inventory_matches -- --exactfails: 0 passed, 1 failed, five unclassifiedrowoccurrences. With onlyopenapi.jsonreplaced by the specification from parent13572c18d30fb6d3b8de36d7ab1923c6acb4e881, the same command passes 1 test. With only the five descriptions changed temporarily toentityon the reviewed head, it also passes 1 test. This is a targeted specification comparison, not a full parent checkout run. All temporary edits were restored; checkout is clean. - The existing minimal Rust inventory test is the right regression owner. GQT cannot observe generated OpenAPI vocabulary. A new GQT case would not test this defect. The raw HTTP body/header/lifetime contract likewise belongs to the route tests.
- Exact-commit CI: server job passed, including 81 data-route tests, 34 authorization tests and 95 OpenAPI tests. The new receipt/precondition and body-bound tests, plus the extended disconnected-waiter test, executed successfully. Workspace failed on the same inventory test (51 passed, 1 failed in that owner). GQT and DST runs for this head passed.
- Local documentation check passed for 220 Markdown files; AGENTS/doc indexes passed. I stopped the local
cargo test -p omnigraph-server --locked --test data_routes blob_put -- --nocapturebuild while Lance dependencies were still compiling. No local functional pass is claimed; the functional results above are CI evidence.
I read the repository contract, test and systems guides and the relevant upstream Blob, write, storage, schema, version and transaction documentation. Storage assumptions were checked against pinned Lance 11.0.0 source (upstream revision ab6b5bbe46009ed78746b444df8db59a8bc5d842), because current Lance documentation includes newer behavior. Limitations: I did not run live cloud qualification or performance tests. The cancellation test drops the router response waiter; it does not prove real HTTP/1 disconnect or HTTP/2 reset behavior. No optional changes requested.
The compatibility loader's pre-decode forecast charged every `base64:` string as Blob payload, so a declared String whose text starts with `base64:` used the payload ceiling: a 24 MiB Blob beside such a 12 MiB String was refused at 33 MiB although the batch check admits it. The forecast now takes the type's Blob properties, as the Arrow check does: only a Blob property's `base64:` value is payload; every other value is framing.
# Conflicts: # crates/omnigraph-server/src/blob_transport.rs
…ions The public vocabulary guard classifies every graph-storage term in the served OpenAPI. The five new descriptions now use the public entity vocabulary, so no inventory exception is needed.
PUT and DELETE one Blob cell over HTTP
…udget # Conflicts: # crates/omnigraph/src/table_store.rs
# Conflicts: # docs/dev/blob.md
ragnorc
left a comment
There was a problem hiding this comment.
Recommendation: approve this HTTP follow-up. Hold the stack's merge until #903 includes the requested GQT regression. The previous P2 finding is fixed. I found no new blocking defect or optional change to request. This is a COMMENT review because the authenticated account owns the PR.
Reviewed 6c6b4554d1b8198e83217a1b213b5cd05e3b515e, with a follow-up comparison against 04b9e6675bad6fa8d04f6f137e3f1e3efb6f2c47.
This PR lets remote clients replace one existing Blob with raw bytes, or clear a nullable Blob. PUT avoids the base64 envelope used by mutation input. Both verbs enforce branch permission, apply optional If-Match, and return the write's own commit receipt. Clearing an already-null cell publishes nothing. The intended workload is a bounded replacement of one node or edge property.
The source descriptions now use entity in all five affected places. The checked-in OpenAPI has the same change. This fixes the cause under the existing vocabulary rule, without adding inventory exceptions. The shorter server guide links to the Blob guide, which retains the limits and conditional-write contract. These corrections add no runtime state or new maintenance mechanism.
The design keeps the existing owners. Ingress reserves a bounded upload before collection. Authorization precedes body polling. Owned operation submission retains the admitted write when its response waiter disappears. The engine attempt checks the precondition on its accepted base and uses the existing Mutation publication path. A retry rechecks the cell and carries its siblings from the new base. The receipt comes from the published detached version, not a later head read.
The lasting costs are the two public verbs, shared header parser, receipt type, and error detail. The shared helpers and engine-owned payload limit avoid separate definitions for each transport. Five similar operations can use these same admission and publication owners. No new durable coordination state is needed. The concrete tradeoff is whole-entity rewriting: sibling Blobs consume the aggregate payload budget and can require permitted external reads. The 32 MiB input limit does not bound total process memory. I rechecked the reason against pinned Lance 11.0.0 (ab6b5bbe46009ed78746b444df8db59a8bc5d842): direct updates and column rewrites cannot write these Blob values.
Validation:
- Local before/after proof: on actual checkout
04b9e6675bad6fa8d04f6f137e3f1e3efb6f2c47,cargo test -p omnigraph-vocabulary-guard --locked --lib tests::openapi_inventory_matches -- --exactexecutes one test and fails on the five unclassified descriptions. The same command on6c6b4554d1b8198e83217a1b213b5cd05e3b515eexecutes one test and passes. Toolchain:1.97.1. The existing test is the minimal regression owner. GQT cannot observe generated OpenAPI vocabulary. - Local on the reviewed head: the two
data_routes blob_puttests, the one disconnect test, and the one branch-policy test pass. All 95 OpenAPI tests pass, including generated-spec equality.cargo test -p omnigraph-server -p omnigraph-api-types --locked --lib blob_ -j 3passes 3 API-type tests and 27 server tests, including the Blob error mapping. The integration commands usecargo test -p omnigraph-server --locked --test <owner> <filter> -j 3:data_routes blob_put,data_routes disconnected_writes_keep_admission_until_the_original_operation_finishes -- --exact,auth_policy policy_blocks_change_on_protected_main_but_allows_unprotected_branch -- --exact, andopenapiwithout a filter. These runs use Rust 1.97.1 andRUSTFLAGS="--cfg tokio_unstable --cfg tokio_unstable". - Local documentation checks: 234 Markdown files, AGENTS/doc indexes, formatting, and diff whitespace pass.
- CI for this head: workspace, server with AWS features, GQT, and pinned DST pass. I checked the logs for the new route tests, generated-spec test, and vocabulary guard. The CI Gate also passes. The server and workspace jobs checked out PR merge commit
537b558edfb08e966078eb0f9289a937410517b3. Its tree exactly matches the reviewed head (49ad2d0723bbd7bf75b9ed04ef619cf54fef2778).
GQT cannot express raw HTTP bytes, header mapping, admission leases, or response-waiter cancellation. The existing Rust route owners cover those contracts. The inherited payload-budget fix still needs the checked-in GQT case requested in the #903 review. I reused the preserved pre-fix result at b8b631ee8f813e557805baf33efe869011f48275 and reran the same temporary GQT extension on this exact head. Its SHA-256 is fb499d12646ca7dd0f58dddc909d8fa8a0ab508da8036511908c0bdbc0457892. Before the fix, the mutation fails at step 9 because framing pushes 32 MiB of Blob data over the shared limit (33556272 > 33554432). On this head, the selected ordinary filesystem execution passes in 8.22 seconds. This proves the behavior, but does not place a regression in the PR.
The before-run command was /tmp/review-903-followup/target/debug/omnigraph-gqt /tmp/review-903-followup/blob_limit.gqt --target omnigraph-engine --storage local-filesystem --artifacts /tmp/review-903-followup/gqt-before-artifacts. The after-run command was cargo run -p omnigraph-gqt --locked --features omnigraph/failpoints --bin omnigraph-gqt -j 3 -- /tmp/review-903-followup/blob_limit.gqt --target omnigraph-engine --storage local-filesystem --artifacts /tmp/review-922-followup/gqt-artifacts, under the same Rust and RUSTFLAGS settings above. This remains a stack requirement, not a new duplicate inline finding.
All temporary test/source edits are restored and the checkout is clean. Limitations: local checks do not qualify live cloud deployment, real TCP disconnects, HTTP/2 resets, or performance. The cancellation test drops an in-process router waiter. No new throughput or peak-memory claim is verified.
ragnorc
left a comment
There was a problem hiding this comment.
Recommendation: approve this merge follow-up. Hold the stack's merge until #903 includes the requested minimal GQT regression. I found no new blocking defect or optional improvement. The earlier OpenAPI vocabulary finding remains fixed. This is a COMMENT review because the authenticated account owns the PR.
Reviewed 1b9ae12f9baf52709e913059c5a517c76ebc0777 against the previous reviewed head, 6c6b4554d1b8198e83217a1b213b5cd05e3b515e. The new commit merges the engine parent. The HTTP implementation, API types, cell-write engine code, and generated OpenAPI are unchanged. The merge has no manual conflict resolution in those components.
This PR lets remote clients replace one existing node or edge Blob with raw bytes, or clear a nullable Blob. Both operations enforce branch permission and optional If-Match, then return the exact commit receipt. Clearing an already-null cell publishes nothing. PUT avoids a base64 envelope for this bounded, single-cell workload.
The design keeps one owner for each contract. Ingress reserves upload capacity. The handler checks permission before body collection. Owned submission keeps an admitted write alive after its response waiter disappears. The engine attempt checks the cell on one accepted base and uses the existing graph publication path. These shared owners address the general lifetime and publication requirements. Five similar routes should reuse them without adding separate coordination state.
I checked the merge's schema interaction. Metadata-only schema changes can add a null Blob field without adding a data file to old fragments. The descriptor decoder resolves a physical file only for managed values. Sibling rebuilding preserves nulls and reads only managed IDs through the shared managed reader. This follows Lance 11's null-reader contract, without an HTTP-specific schema workaround.
The lasting liability remains two public verbs, a shared header parser, a receipt type, and an error detail. Shared limits and publication code keep those obligations in one place. The main tradeoff remains whole-entity rewriting. Sibling Blob values consume the aggregate payload budget and can require permitted external reads. The 32 MiB input limit does not bound process memory. Lance 11 still refuses Blob-v2 direct updates and column rewrites. This follow-up adds no separate Blob write protocol or durable state.
Local validation uses Rust 1.97.1, CARGO_TARGET_DIR=/tmp/review-928/target, and RUSTFLAGS='--cfg tokio_unstable --cfg tokio_unstable'. The following commands run on the exact reviewed head. All listed tests execute with zero failures or ignored tests:
| Command | Result |
|---|---|
cargo test -p omnigraph-server --locked --test data_routes blob_put -j 3 |
2 pass |
cargo test -p omnigraph-server --locked --test data_routes disconnected_writes_keep_admission_until_the_original_operation_finishes -j 3 -- --exact |
1 passes |
cargo test -p omnigraph-server --locked --test openapi -j 3 |
95 pass, including generated-spec equality |
cargo test -p omnigraph-server -p omnigraph-api-types --locked --lib blob_ -j 3 |
27 server and 3 API-type tests pass |
cargo test -p omnigraph-server --locked --test auth_policy policy_blocks_change_on_protected_main_but_allows_unprotected_branch -j 3 -- --exact |
1 passes |
cargo test -p omnigraph-engine --locked --features failpoints --test schema_apply apply_schema_column_changes_keep_data_files_ids_and_index_coverage -j 3 -- --exact |
1 passes |
cargo test -p omnigraph-engine --locked --features failpoints --test schema_apply schema_apply_preserves_ranged_external_blob_across_column_changes -j 3 -- --exact |
1 passes |
The first schema test initially failed with an older failpoints library from the shared build cache. Its build timestamp matched the earlier #825 run. I forced workspace recompilation without source changes and reran the affected checks. The results above use rebuilt workspace libraries. Documentation checks pass for 239 Markdown files. AGENTS/doc links, pinned-toolchain formatting, and diff whitespace checks pass.
The inherited budget regression remains the existing #903 merge requirement. I reused the preserved pre-fix result at b8b631ee8f813e557805baf33efe869011f48275. The same temporary GQT extension fails there at mutation step 9: framing causes 33556272 > 33554432 for a 32 MiB Blob. Its SHA-256 is fb499d12646ca7dd0f58dddc909d8fa8a0ab508da8036511908c0bdbc0457892. On this exact head, the selected filesystem execution passes. The commands are:
# Preserved before-fix run, at b8b631ee8f813e557805baf33efe869011f48275:
/tmp/review-903-followup/target/debug/omnigraph-gqt /tmp/review-903-followup/blob_limit.gqt --target omnigraph-engine --storage local-filesystem --artifacts /tmp/review-903-followup/gqt-before-artifacts
# Fresh after-fix run, at 1b9ae12f9baf52709e913059c5a517c76ebc0777, with the environment above:
cargo run -p omnigraph-gqt --locked --features omnigraph/failpoints --bin omnigraph-gqt -j 3 -- /tmp/review-903-followup/blob_limit.gqt --target omnigraph-engine --storage local-filesystem --artifacts /tmp/review-922-merge/gqt-artifactsThis proves the behavior but does not add a regression to the PR. The author's proposed generated parameter avoids checking in 44.7 MB of literal test data. That compact case is still absent here. I am not repeating the existing inline finding. GQT's new served target supports ordinary queries and mutations. It still cannot express raw PUT bytes, headers, admission leases, or response-waiter cancellation. The existing Rust route tests own those promises.
For this head, Test Workspace passes 4,085 tests with 17 skipped. The AWS-feature server job, ordinary/served/DST GQT, and pinned DST suite pass. Workspace and server logs identify merge commit 8747825a8ff1a865ff41bc8551dffa8b8327d557. Its tree, 08be45b7e338df3d2b08a14391e74e606997503f, exactly matches the reviewed head.
I checked the current repository guides and retained the complete relevant upstream readings from the earlier review. The Lance 11.0.0 archive checksum matches Cargo.lock. The inspected fragment, update, merge-insert, schema-evolution, and commit sources match that archive. The checkout is clean, with no temporary source or test edits. Local tests do not qualify live cloud deployment, TCP disconnects, HTTP/2 resets, throughput, or peak memory. The cancellation test drops an in-process router waiter. The supported deployment still uses one mutation-capable writer process per cluster.
Main's write_max_bytes setting landed the payload/row accounting split this branch inherited from its accounting prerequisite, so the merge keeps main's accounting and drops the prerequisite's. The Blob put now checks its value against the session's write_max_bytes and stages under that operation's WriteBudget. The prerequisite's two Lance surface guards, which pin facts the put relies on, come along unchanged.
The engine put now honors the session's write_max_bytes. The route's pre-read bound stays BLOB_WRITE_MAX_BYTES, now the setting's maximum, and the server's process write_max_bytes bounds the value after the body is read; data_routes pins that a lowered process value refuses a PUT with 413.
# Conflicts: # crates/omnigraph/tests/detached_commit_matrix.rs
# Conflicts: # changelog.d/blob-cell-put-clear.added.md
# Conflicts: # changelog.d/blob-cell-put-clear.added.md # crates/omnigraph-server/src/lib.rs # crates/omnigraph/src/exec/blob_write.rs # crates/omnigraph/src/lib.rs # docs/dev/blob.md # docs/user/blobs.md
ragnorc
left a comment
There was a problem hiding this comment.
Recommendation: approve. The earlier OpenAPI finding remains fixed. The earlier GQT merge condition is now resolved by #933. I found no new blocking defect or optional change to request. This is a COMMENT review because the authenticated account owns the PR.
Reviewed 86a056a7cd62b8627926a5c16fcec18de2409a2c, including the changes since 1b9ae12f9baf52709e913059c5a517c76ebc0777.
This PR lets remote clients replace one existing node or edge Blob with raw bytes, or clear a nullable Blob. Both verbs check branch permission and optional If-Match, then return the write's exact commit receipt. Clearing an already-null cell publishes nothing. PUT avoids a base64 envelope for this bounded, single-cell workload.
The follow-up makes a lower server write_max_bytes apply before upload collection. Previously, the HTTP path still reserved and collected against 32 MiB before the engine applied the lower value. Now one derived limit feeds ingress reservation, declared-length admission, and the streamed-body collector. The engine session receives the same process defaults. PUT accepts no request settings that could change this value after reservation.
This fixes the cause at the HTTP boundary. It covers declared and undeclared bodies, node and edge selectors, and every valid process limit. The engine remains the owner of the aggregate allowance for the new value and carried siblings. DELETE has no upload to reserve and uses that same engine budget. Mutation and load retain their distinct encoded-body contracts. No separate limit registry or mutable coordination state is added.
The write still uses one accepted base, rechecks its precondition on each permitted fresh attempt, and publishes through the existing Mutation path. Owned submission retains an admitted write after its response waiter disappears. Receipt construction uses the exact detached version before graph publication. A later write cannot change that receipt.
The liability tradeoff is reasonable. Two public verbs, the shared header parser, receipt type, and error detail remain compatibility obligations. This follow-up removes a separate HTTP constant and derives the limit from existing settings. Its private accessor and collector argument add little maintenance cost. Five similar routes can reuse these admission and publication owners without five new protocols.
The physical cost remains a whole-entity rewrite. Other Blob cells share the payload allowance and can require allowed external reads. Pinned Lance 11.0.0 refuses Blob-v2 direct updates and column rewrites. The lower limit reduces reserved upload capacity by construction. Neither it nor the separate row/payload allowances bound total process memory. I make no measured throughput or peak-memory claim.
Local validation used Rust 1.97.1 and this environment:
export RUSTUP_TOOLCHAIN=1.97.1
export CARGO_TARGET_DIR=/tmp/review-928/target
export RUSTFLAGS='--cfg tokio_unstable --cfg tokio_unstable'The HTTP regression is the extension of the existing data_routes owner. I copied that exact test function temporarily to pre-fix commit 3a10b7036b7b579a50737305088be2c088c2b9a2. No production source changed. The same command ran on that commit and the reviewed head:
cargo test --locked -p omnigraph-server \
--features omnigraph/failpoints,omnigraph-cluster/failpoints \
--test data_routes -j 3 \
blob_put_raw_body_bounds_media_type_length_and_deadline -- --exact --nocaptureBefore: one test executed and failed. A declared 4,094-byte body under a 4,093-byte limit reached collection and returned 400 instead of the required early 413. After: one test passed, including the assertion that the body was never polled. The test also checks streamed overflow and exact-limit acceptance. This is a behavioral failure, not a compile or setup failure. GQT cannot express raw HTTP framing or body-poll order, so this Rust owner is the appropriate regression.
Additional local results on the exact head:
data_routeswith filterblob_put: 2 passed, covering node/edge receipts, preconditions, refusals, body limits, and deadlines.- The exact
disconnected_writes_keep_admission_until_the_original_operation_finishescase: 1 passed. - The exact
auth_policy::policy_blocks_change_on_protected_main_but_allows_unprotected_branchcase: 1 passed. - All
openapitests: 95 passed, including generated-spec equality. - Documentation: 243 Markdown files passed. AGENTS/doc indexes, formatting, and diff whitespace passed.
The Rust commands use the same package, features and -j 3 above, with --test data_routes, --test auth_policy, or --test openapi and the stated filter. No listed local test was ignored.
The former #903 condition is resolved: #903 was superseded by #933. The checked-in GQT cases now use small configurable limits to cover exact acceptance, overflow, carried payloads, and edge loads. I ran:
cargo run --locked -p omnigraph-gqt --features omnigraph/failpoints \
--bin omnigraph-gqt -j 3 -- \
crates/omnigraph-gqt/cases/write_max_bytes.gqt \
crates/omnigraph-gqt/cases/write_max_bytes_update_carried_blob.gqt \
crates/omnigraph-gqt/cases/load_write_max_bytes_edge_blob_value.gqt \
/tmp/review-933/default_boundary.gqt \
--target omnigraph-engine --storage local-filesystem \
--artifacts /tmp/review-922-20261011/gqt-artifactsAll four selected executions passed. For the inherited accounting fix, I reused the preserved before result at 7a3bddf544640135a39cdec7952872b3ce0e3918. The identical temporary GQT fixture has one node type, one Blob insert, and a query of its key. The parent lacks the new setting, so this comparison needs its fixed 32 MiB boundary. It fails at the insert because row framing produces 33555360 > 33554432. The same fixture passes on this exact head in 7.91 seconds. Its SHA-256 is c3d5b93e8ddde0057bc144b16722ffec426160b42a8ed255994b1536050f344a. The preserved before command was:
/tmp/review-928/target/debug/omnigraph-gqt /tmp/review-933/default_boundary.gqt \
--target omnigraph-engine --storage local-filesystem \
--artifacts /tmp/review-933/default-beforeThat executable was built from the stated before commit with cargo build --locked -p omnigraph-gqt --features omnigraph/failpoints --bin omnigraph-gqt -j 3 under the environment above. The before result is reused evidence, not a new run.
CI: Test Workspace passed 4,261 tests with 29 skipped. The AWS-feature server job passed the route, authorization, and OpenAPI tests. Their logs identify merge commit 3c34bb4377b382178954ecc6e9ea7aed6a9475f7. Its tree a9dc2f44e1653738b695fcff34bb41e6579dcb93 exactly matches the reviewed head. Ordinary, served and DST GQT, RustFS, Azurite, Clippy, and CI Gate also report success.
I checked the current repository guides and retained the complete relevant upstream readings from the earlier reviews. Material storage assumptions use the pinned Lance implementation, not newer documentation features. All temporary test edits are restored and the isolated checkout is clean. Local validation uses macOS and filesystem storage. The disconnect test drops an in-process response waiter. It does not qualify TCP faults, HTTP/2 resets, live cloud deployment, or performance. The deployment boundary remains one mutation-capable writer process per cluster.
RFC 0033 step 3B: the HTTP half of the Blob write API.
PUTandDELETE /graphs/{graph_id}/blobreplace or clear one Blob value of an existing node or edge, on top of the engine writes from step 3A.It builds on the engine writes from 3A (#908, merged) and on the
write_max_bytessetting (#933). The plan is the RFC 0033 amendment in #900, §5.2, §5.3 and §12.4.The routes
PUTtakes a rawapplication/octet-streambody of at most the server'swrite_max_bytes(32 MiB by default), inclusive. It works like/load/ndjson:AppState::blob_put_limit: the processwrite_max_bytes, at mostBLOB_WRITE_MAX_BYTES. A put takes no request settings, so it is known before the body arrives.changeon the branch, then checksContent-Type(415) and a declaredContent-Length(413), all before polling a body byte.Blob write payload bytesat the server'swrite_max_bytes. The route refuses a declared body past it unread, and a streamed one at the byte past it, so a loweredOMNIGRAPH_WRITE_MAX_BYTESalso lowers what one PUT reserves and reads. A request cannot change it: like a load, aPUTtakes no settings.DELETEhas no body. It clears a nullable cell. Clearing a cell that is already null returnscommit: null.Both verbs:
branch(defaultmain);snapshotand any other unknown parameter is a 400 (deny_unknown_fields);owned_write, so after admission a disconnect loses only the response;BlobWriteOutputreceipt:selector,branch,kind(managedornull),size,etag,actor_id, and the exactcommit. APUTalso sends theETagheader.If-Matchis parsed byomnigraph_api_types::parse_blob_if_match:*alone becomesAnyExisting; a list of tags becomesTags;*next to another element, is a 400.The engine evaluates the precondition against each attempt's base, so the server never compares tags itself.
A failed
If-Matchis a 412 under the existingconflictcode. It carries the newblob_precondition_failure { current_etag }detail, plus anETagheader when the cell holds a managed value. It never carries the graph-commitprecondition_failure.parse_blob_if_matchandblob_write_outputlive in the API types crate so that the CLI's embedded writes (3C) produce the same receipt and accept the sameIf-Matchsyntax.Tests
data_routes.rs::blob_put_and_delete_return_exact_receipts_and_blob_preconditionsGET /commits/{id}, and its ETag is what a branch read and asnapshot=<commit>read report. A clear's receipt; a no-op clear'scommit: null;*on a null cell gets a 412 with no ETag; a put withoutIf-Matchsucceeds. A missing id or branch is a 404. A non-Blob property,snapshotor a malformedIf-Matchis a 400. No refusal publishes.data_routes.rs::blob_put_raw_body_bounds_media_type_length_and_deadlineactual= 32 MiB + 1. A stalled body gets a 408. After each refusal, admission is open and all capacity is released. With a processwrite_max_bytesof 4,093: a declared 4,094-byte body is a 413 before the body is polled, a streamed one is a 413 at byte 4,094, and exactly 4,093 bytes are stored.data_routes.rs::disconnected_writes_keep_admission_until_the_original_operation_finishesPUT /blobandDELETE /blobdoors. A disconnect after admission keeps the write and its capacity until it settles, and it publishes once. The put's ingress reservation equals the received body.auth_policy.rschangeis refused before the body is polled. On an unprotected branch, the receipt and the commit carry the actor.openapi.rs+ regeneratedopenapi.jsonsnapshot, a binary request body, theBlobWriteOutputreceipt with a required nullablecommit, the 412ETagheader, and theblob_precondition_failuredetail.If-Matchparser and the receipt mapper (omnigraph-api-types), and the 412 mapping and itsETagheader (api_error_tests).Not in this PR
system_remotelost-delivery matrix needs the CLI's HTTP test proxy to carry binary bodies, which comes with 3C;blob putandblob clear, with embedded/remote receipt parity.