diff --git a/crates/nokv-client/src/append_operations_tests.rs b/crates/nokv-client/src/append_operations_tests.rs index b6130d310..0451a24e5 100644 --- a/crates/nokv-client/src/append_operations_tests.rs +++ b/crates/nokv-client/src/append_operations_tests.rs @@ -170,7 +170,6 @@ fn inspection(observation: OperationStatus, start: u32, end: u32) -> AppendClean object_identity: ObjectIdentity::new(format!("object-{sequence}")).unwrap(), expected_length: 4, expected_digest: sha256_digest_uri(Digest([7; 32])), - multipart_token: None, }) .collect(), next_after: (end < 5).then(|| end - 1), diff --git a/crates/nokv-client/src/artifact.rs b/crates/nokv-client/src/artifact.rs index e7d21979b..9eccd1995 100644 --- a/crates/nokv-client/src/artifact.rs +++ b/crates/nokv-client/src/artifact.rs @@ -3181,7 +3181,6 @@ fn publication_rows( object_identity: object_identity.clone(), expected_length: block.len, expected_digest: digest.clone(), - multipart_token: None, }); manifest_rows.push(ArtifactManifestRow { object_index: block.object_index, diff --git a/crates/nokv-meta/src/workspace/mod.rs b/crates/nokv-meta/src/workspace/mod.rs index 73097ad47..55d97e6b3 100644 --- a/crates/nokv-meta/src/workspace/mod.rs +++ b/crates/nokv-meta/src/workspace/mod.rs @@ -167,8 +167,8 @@ pub use publish_operation_records::{ ArtifactManifestRow, ManifestPosition, PublishAuthority, PublishClaim, PublishOperationRecord, PublishRecordError, PublishResult, PublishTerminalError, PublishTerminalErrorKind, PublishTransition, StagedObjectRecord, MAX_APPEND_SEGMENTS, MAX_MANIFEST_ROWS, - MAX_MULTIPART_ID_BYTES, MAX_STAGED_OBJECTS, MAX_TERMINAL_ERROR_BYTES, - PUBLISH_OPERATION_VALUE_FORMAT_VERSION, PUBLISH_VALUE_FORMAT_VERSION, + MAX_STAGED_OBJECTS, MAX_TERMINAL_ERROR_BYTES, PUBLISH_OPERATION_VALUE_FORMAT_VERSION, + PUBLISH_VALUE_FORMAT_VERSION, STAGED_OBJECT_VALUE_FORMAT_VERSION, }; pub use query::{ aggregate_paths_at, catalog_fields_at, find_workspaces_at, get_current_workspace, diff --git a/crates/nokv-meta/src/workspace/publication.rs b/crates/nokv-meta/src/workspace/publication.rs index 4f01dc7d2..249a37893 100644 --- a/crates/nokv-meta/src/workspace/publication.rs +++ b/crates/nokv-meta/src/workspace/publication.rs @@ -1014,7 +1014,6 @@ fn validate_cleanup_update( if expected.artifact_revision_id != next.artifact_revision_id || expected.object_sequence != next.object_sequence || expected.object_key != next.object_key - || expected.multipart_upload_id != next.multipart_upload_id || expected.expected_length != next.expected_length || expected.expected_digest_uri != next.expected_digest_uri || next.provider_state != StagedProviderState::Aborted @@ -1044,10 +1043,6 @@ fn validate_staged_update( || expected.object_key != next.object_key || expected.expected_length != next.expected_length || expected.expected_digest_uri != next.expected_digest_uri - || expected - .multipart_upload_id - .as_ref() - .is_some_and(|id| next.multipart_upload_id.as_ref() != Some(id)) || !valid_provider_transition(expected.provider_state, next.provider_state) || !valid_cleanup_transition(expected.cleanup_state, next.cleanup_state) { @@ -1064,12 +1059,6 @@ fn valid_provider_transition(expected: StagedProviderState, next: StagedProvider (expected, next), ( StagedProviderState::Planned, - StagedProviderState::Uploading - | StagedProviderState::Uploaded - | StagedProviderState::AbortPending - | StagedProviderState::Ambiguous - ) | ( - StagedProviderState::Uploading, StagedProviderState::Uploaded | StagedProviderState::AbortPending | StagedProviderState::Ambiguous @@ -5324,7 +5313,6 @@ mod tests { artifact_revision_id, u64::from(sequence), ), - multipart_upload_id: None, expected_length: 1, expected_digest_uri: format!("sha256:{sequence:064x}"), provider_state: StagedProviderState::Planned, @@ -8058,7 +8046,7 @@ mod tests { for next in [ StagedObjectRecord { - provider_state: StagedProviderState::Uploading, + provider_state: StagedProviderState::Planned, ..persisted.clone() }, StagedObjectRecord { diff --git a/crates/nokv-meta/src/workspace/publish_operation_records.rs b/crates/nokv-meta/src/workspace/publish_operation_records.rs index 115a126f8..055c06c62 100644 --- a/crates/nokv-meta/src/workspace/publish_operation_records.rs +++ b/crates/nokv-meta/src/workspace/publish_operation_records.rs @@ -18,8 +18,14 @@ use nokv_types::{ WorkspaceRevision, FIXED_ID_BYTES, SHA256_BYTES, }; -/// Durable value format for publication-owned payloads. +/// Durable value format for publication-owned payloads. Clients seal manifest +/// rows against this encoding, so it moves only with the manifest row. pub const PUBLISH_VALUE_FORMAT_VERSION: u8 = 7; +/// Durable value format for the staged-object record. Version 8 dropped the +/// multipart upload id that no provider path ever set; a version 7 record is +/// refused rather than read with the field's bytes shifted into its +/// neighbours. +pub const STAGED_OBJECT_VALUE_FORMAT_VERSION: u8 = 8; /// Durable value format for the publish operation record. Version 9 records /// the initiating principal; version 8 added the sealed digest of a body /// published inline. @@ -36,7 +42,6 @@ pub const MAX_DEPENDENCY_DEPTH: u8 = 8; /// Maximum append-segment sequence admitted by the durable publication codec. pub const MAX_APPEND_SEGMENTS: u32 = 1_048_576; -pub const MAX_MULTIPART_ID_BYTES: usize = 1_024; pub const MAX_DIGEST_URI_BYTES: usize = 256; pub const MAX_TERMINAL_ERROR_BYTES: usize = 4_096; @@ -351,7 +356,6 @@ pub struct StagedObjectRecord { pub artifact_revision_id: ArtifactRevisionId, pub object_sequence: u32, pub object_key: String, - pub multipart_upload_id: Option>, pub expected_length: u64, pub expected_digest_uri: String, pub provider_state: StagedProviderState, @@ -1300,9 +1304,6 @@ impl StagedObjectRecord { }); } validate_object_key(&self.object_key, self.artifact_revision_id)?; - if let Some(upload_id) = &self.multipart_upload_id { - validate_opaque_bytes("multipart_upload_id", upload_id, MAX_MULTIPART_ID_BYTES)?; - } if self.expected_length == 0 { return Err(PublishRecordError::ZeroScalar { field: "expected_length", @@ -1315,15 +1316,10 @@ impl StagedObjectRecord { pub fn encode(&self) -> Result, PublishRecordError> { self.validate()?; let mut encoded = Vec::with_capacity(256); - encoded.push(PUBLISH_VALUE_FORMAT_VERSION); + encoded.push(STAGED_OBJECT_VALUE_FORMAT_VERSION); encoded.extend_from_slice(self.artifact_revision_id.as_bytes()); encoded.extend_from_slice(&self.object_sequence.to_be_bytes()); push_string(&mut encoded, "object_key", &self.object_key)?; - push_optional_bytes( - &mut encoded, - "multipart_upload_id", - self.multipart_upload_id.as_deref(), - )?; encoded.extend_from_slice(&self.expected_length.to_be_bytes()); push_string( &mut encoded, @@ -1347,13 +1343,11 @@ impl StagedObjectRecord { MAX_STAGED_OBJECT_RECORD_BYTES, )?; let mut decoder = Decoder::new(encoded); - decoder.require_value_version()?; + decoder.require_version(STAGED_OBJECT_VALUE_FORMAT_VERSION)?; let artifact_revision_id = ArtifactRevisionId::from_bytes(decoder.fixed("artifact_revision_id")?); let object_sequence = decoder.u32("object_sequence")?; let object_key = decoder.string("object_key", OBJECT_KEY_BYTES)?; - let multipart_upload_id = - decoder.optional_bytes("multipart_upload_id", MAX_MULTIPART_ID_BYTES)?; let expected_length = decoder.u64("expected_length")?; let expected_digest_uri = decoder.string("expected_digest_uri", MAX_DIGEST_URI_BYTES)?; let provider_state = decode_provider_state(decoder.u8("provider_state")?)?; @@ -1363,7 +1357,6 @@ impl StagedObjectRecord { artifact_revision_id, object_sequence, object_key, - multipart_upload_id, expected_length, expected_digest_uri, provider_state, @@ -1576,13 +1569,10 @@ fn validate_staged_state_pair( let valid = matches!( (provider, cleanup), ( - StagedProviderState::Planned - | StagedProviderState::Uploading - | StagedProviderState::Uploaded, + StagedProviderState::Planned | StagedProviderState::Uploaded, StagedCleanupState::Owned ) | ( StagedProviderState::Planned - | StagedProviderState::Uploading | StagedProviderState::Uploaded | StagedProviderState::AbortPending, StagedCleanupState::DeletePending @@ -1692,24 +1682,6 @@ fn validate_text(field: &'static str, value: &str, max: usize) -> Result<(), Pub Ok(()) } -fn validate_opaque_bytes( - field: &'static str, - value: &[u8], - max: usize, -) -> Result<(), PublishRecordError> { - if value.is_empty() { - return Err(PublishRecordError::EmptyField { field }); - } - if value.len() > max { - return Err(PublishRecordError::FieldTooLong { - field, - length: value.len(), - max, - }); - } - Ok(()) -} - fn require_record_size( record: &'static str, length: usize, @@ -1880,21 +1852,6 @@ fn push_bytes( Ok(()) } -fn push_optional_bytes( - target: &mut Vec, - field: &'static str, - value: Option<&[u8]>, -) -> Result<(), PublishRecordError> { - match value { - None => target.push(0), - Some(value) => { - target.push(1); - push_bytes(target, field, value)?; - } - } - Ok(()) -} - fn push_optional_fixed(target: &mut Vec, value: &Option<[u8; N]>) { match value { None => target.push(0), @@ -1966,18 +1923,6 @@ impl<'a> Decoder<'a> { .map_err(|_| PublishRecordError::InvalidUtf8 { field }) } - fn optional_bytes( - &mut self, - field: &'static str, - max: usize, - ) -> Result>, PublishRecordError> { - match self.u8(field)? { - 0 => Ok(None), - 1 => self.bytes(field, max).map(Some), - value => Err(PublishRecordError::InvalidOptionalTag { field, value }), - } - } - fn optional_fixed( &mut self, field: &'static str, @@ -2288,7 +2233,6 @@ mod tests { artifact_revision_id: revision(0x22), object_sequence: 7, object_key: object_key(0x22), - multipart_upload_id: Some(b"upload-1".to_vec()), expected_length: 4_096, expected_digest_uri: "sha256:block".to_owned(), provider_state: StagedProviderState::Uploaded, @@ -2296,14 +2240,11 @@ mod tests { }; let key = object_key(0x22); let expected = [ - &[PUBLISH_VALUE_FORMAT_VERSION][..], + &[STAGED_OBJECT_VALUE_FORMAT_VERSION][..], &[0x22; 16], &7_u32.to_be_bytes(), &(key.len() as u32).to_be_bytes(), key.as_bytes(), - &[1], - &8_u32.to_be_bytes(), - b"upload-1", &4_096_u64.to_be_bytes(), &12_u32.to_be_bytes(), b"sha256:block", @@ -2649,7 +2590,6 @@ mod tests { artifact_revision_id: revision(0x22), object_sequence: 0, object_key: object_key(0x22), - multipart_upload_id: None, expected_length: 1, expected_digest_uri: "sha256:a".to_owned(), provider_state: StagedProviderState::Uploaded, @@ -2760,7 +2700,6 @@ mod tests { artifact_revision_id: revision(0x22), object_sequence: 0, object_key: object_key(0x22), - multipart_upload_id: None, expected_length: 1, expected_digest_uri: "sha256:a".to_owned(), provider_state: StagedProviderState::Uploaded, @@ -2857,24 +2796,6 @@ mod tests { #[test] fn oversized_fields_and_records_fail_before_allocation_or_acceptance() { - let staged = StagedObjectRecord { - artifact_revision_id: revision(0x22), - object_sequence: 0, - object_key: object_key(0x22), - multipart_upload_id: Some(vec![0; MAX_MULTIPART_ID_BYTES + 1]), - expected_length: 1, - expected_digest_uri: "sha256:a".to_owned(), - provider_state: StagedProviderState::Uploading, - cleanup_state: StagedCleanupState::Owned, - }; - assert!(matches!( - staged.encode(), - Err(PublishRecordError::FieldTooLong { - field: "multipart_upload_id", - .. - }) - )); - let oversized = vec![0; MAX_PUBLISH_OPERATION_RECORD_BYTES + 1]; assert_eq!( PublishOperationRecord::decode(&oversized).unwrap_err(), diff --git a/crates/nokv-meta/src/workspace/restore.rs b/crates/nokv-meta/src/workspace/restore.rs index 322e0d1ed..c1f9f8488 100644 --- a/crates/nokv-meta/src/workspace/restore.rs +++ b/crates/nokv-meta/src/workspace/restore.rs @@ -10794,7 +10794,6 @@ mod tests { artifact_revision_id, object_sequence: 0, object_key: object_block_key(shard(), root(), artifact_revision_id, 0), - multipart_upload_id: None, expected_length: body.len() as u64, expected_digest_uri: body_digest_uri(body), provider_state: StagedProviderState::Planned, @@ -10836,7 +10835,6 @@ mod tests { artifact_revision_id, object_sequence: object_index, object_key: object_key.clone(), - multipart_upload_id: None, expected_length: block_size, expected_digest_uri: digest_uri.clone(), provider_state: StagedProviderState::Planned, diff --git a/crates/nokv-protocol/src/artifact_publish.rs b/crates/nokv-protocol/src/artifact_publish.rs index 36fb492fa..d0f0c830d 100644 --- a/crates/nokv-protocol/src/artifact_publish.rs +++ b/crates/nokv-protocol/src/artifact_publish.rs @@ -313,7 +313,6 @@ mod tests { object_identity: ObjectIdentity::new(key).unwrap(), expected_length: 4, expected_digest: digest("sha256:abcd"), - multipart_token: None, } } diff --git a/crates/nokv-protocol/src/codec.rs b/crates/nokv-protocol/src/codec.rs index 47f15ba6f..2940c0b88 100644 --- a/crates/nokv-protocol/src/codec.rs +++ b/crates/nokv-protocol/src/codec.rs @@ -385,7 +385,6 @@ mod tests { object_identity: object(), expected_length: 1, expected_digest: digest.clone(), - multipart_token: None, }], }, ), @@ -445,8 +444,8 @@ mod tests { assert_eq!( <[u8; 32]>::from(golden.finalize()), [ - 95, 157, 43, 219, 156, 245, 126, 182, 88, 13, 148, 128, 96, 253, 190, 238, 237, - 127, 173, 41, 108, 173, 150, 72, 236, 15, 70, 203, 2, 183, 100, 149, + 108, 207, 191, 121, 68, 172, 63, 22, 194, 9, 128, 128, 137, 110, 101, 239, 11, 98, + 201, 199, 14, 192, 40, 140, 37, 233, 60, 50, 125, 247, 177, 184, ], "update only for an intentional fused publication v15 wire change" ); diff --git a/crates/nokv-protocol/src/request.rs b/crates/nokv-protocol/src/request.rs index 8b7dcd27e..c94568f1d 100644 --- a/crates/nokv-protocol/src/request.rs +++ b/crates/nokv-protocol/src/request.rs @@ -2373,7 +2373,6 @@ mod tests { object_identity: crate::ObjectIdentity::new(format!("objects/{sequence}")).unwrap(), expected_length: 1, expected_digest: crate::sha256_digest_uri(Digest([5; 32])), - multipart_token: None, } } diff --git a/crates/nokv-protocol/src/types.rs b/crates/nokv-protocol/src/types.rs index 20d042c61..e39894032 100644 --- a/crates/nokv-protocol/src/types.rs +++ b/crates/nokv-protocol/src/types.rs @@ -539,12 +539,9 @@ pub struct StagedObject { pub object_identity: ObjectIdentity, pub expected_length: u64, pub expected_digest: DigestUri, - pub multipart_token: Option>, } impl StagedObject { - pub const MAX_MULTIPART_TOKEN_BYTES: usize = 4_096; - pub fn validate(&self) -> Result<(), ProtocolError> { if self.expected_length == 0 { return Err(ProtocolError::invalid( @@ -552,16 +549,6 @@ impl StagedObject { "must be greater than zero", )); } - if self - .multipart_token - .as_ref() - .is_some_and(|token| token.len() > Self::MAX_MULTIPART_TOKEN_BYTES) - { - return Err(ProtocolError::invalid( - "staged_object.multipart_token", - format!("exceeds {} bytes", Self::MAX_MULTIPART_TOKEN_BYTES), - )); - } Ok(()) } } diff --git a/crates/nokv-python/README.md b/crates/nokv-python/README.md index af0d0377f..4b2d9bcb9 100644 --- a/crates/nokv-python/README.md +++ b/crates/nokv-python/README.md @@ -135,8 +135,8 @@ not for installing the SDK. same status plus logical `operation_token`, child `publication_token`, object namespace, `registered_count`, `cleanup_cursor`, `remaining_count`, and a page of retained staged `entries`. Each entry has `sequence`, `object_identity`, - `expected_length`, `expected_digest`, and optional `multipart_token`. Retired - keys are not a historical inventory. The limit is 1 to 192; `next_cursor` is + `expected_length`, and `expected_digest`. Retired keys are not a historical + inventory. The limit is 1 to 192; `next_cursor` is opaque `bytes` or `None`. Continue with the unchanged cursor. A changed state returns `Conflict`; restart inspection rather than combining different pages. - `Client.operation_recover(operation_id, expected_state_digest=None)` asks the diff --git a/crates/nokv-python/src/client.rs b/crates/nokv-python/src/client.rs index 6d2ec41cb..f832b7925 100644 --- a/crates/nokv-python/src/client.rs +++ b/crates/nokv-python/src/client.rs @@ -739,7 +739,6 @@ impl PythonWorkspaceClient { row.set_item("object_identity", entry.object_identity.as_str())?; row.set_item("expected_length", entry.expected_length)?; row.set_item("expected_digest", entry.expected_digest.as_str())?; - row.set_item("multipart_token", entry.multipart_token.clone())?; entries.append(row)?; } dict.set_item("entries", entries)?; diff --git a/crates/nokv-server/src/executor.rs b/crates/nokv-server/src/executor.rs index fc67f0f77..3829d1916 100644 --- a/crates/nokv-server/src/executor.rs +++ b/crates/nokv-server/src/executor.rs @@ -2335,7 +2335,6 @@ impl MetadataWorkspaceRequestExecutor { artifact_revision_id: operation.artifact_revision_id, object_sequence: object.sequence, object_key: object.object_identity.as_str().to_owned(), - multipart_upload_id: object.multipart_token.clone(), expected_length: object.expected_length, expected_digest_uri: object.expected_digest.as_str().to_owned(), provider_state: types::StagedProviderState::Planned, @@ -3247,7 +3246,6 @@ impl MetadataWorkspaceRequestExecutor { expected_length: row.expected_length, expected_digest: protocol::DigestUri::new(row.expected_digest_uri) .map_err(|e| internal(e.to_string()))?, - multipart_token: row.multipart_upload_id, }); } Ok(ExecutedRequest { @@ -9237,7 +9235,6 @@ mod tests { object_identity: protocol::ObjectIdentity::new(object_key.clone()).unwrap(), expected_length: length, expected_digest: body_digest.clone(), - multipart_token: None, }]; let manifest_rows = vec![protocol::ArtifactManifestRow { object_index: 0, @@ -12913,7 +12910,6 @@ mod tests { object_identity: protocol::ObjectIdentity::new(object_key.clone()).unwrap(), expected_length: 1, expected_digest: body_digest.clone(), - multipart_token: None, }]; let manifest_rows = vec![protocol::ArtifactManifestRow { object_index: 0, @@ -13172,7 +13168,6 @@ mod tests { expected_digest: protocol::sha256_digest_uri(protocol::Digest( [sequence as u8; 32], )), - multipart_token: None, }) .collect(); let seals = @@ -14071,7 +14066,6 @@ mod tests { object_identity: protocol::ObjectIdentity::new(object_key.clone()).unwrap(), expected_length: 1, expected_digest: body_digest.clone(), - multipart_token: None, }]; let manifest_rows = vec![protocol::ArtifactManifestRow { object_index: 0, diff --git a/crates/nokv-server/src/lifecycle.rs b/crates/nokv-server/src/lifecycle.rs index a699b8f5c..cedbfffea 100644 --- a/crates/nokv-server/src/lifecycle.rs +++ b/crates/nokv-server/src/lifecycle.rs @@ -41,13 +41,11 @@ pub enum LifecycleCleanupPurpose { } /// Provider-neutral cleanup request. Failed append keys must be permanently -/// sealed; ordinary publication and revision GC require absence. Multipart -/// cleanup must also fence the named upload before returning success. +/// sealed; ordinary publication and revision GC require absence. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LifecycleCleanupRequest { pub purpose: LifecycleCleanupPurpose, pub object_key: String, - pub multipart_upload_id: Option>, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -75,7 +73,7 @@ impl LifecycleCleanupProof { /// Build stable provider evidence for one authoritative cleanup request. /// The caller must pass the canonical object key received from the /// lifecycle worker; the digest binds the metadata schema, proof domain, - /// cleanup purpose, exact key, optional multipart id, and disposition. + /// cleanup purpose, exact key, and disposition. pub fn from_cleanup_request( request: &LifecycleCleanupRequest, disposition: LifecycleCleanupDisposition, @@ -89,13 +87,6 @@ impl LifecycleCleanupProof { LifecycleCleanupPurpose::AbortedAppend => 3, }]); hash_part(&mut hasher, request.object_key.as_bytes()); - match request.multipart_upload_id.as_deref() { - None => hasher.update([0]), - Some(multipart_upload_id) => { - hasher.update([1]); - hash_part(&mut hasher, multipart_upload_id); - } - } hasher.update([match disposition { LifecycleCleanupDisposition::Deleted => 1, LifecycleCleanupDisposition::AlreadyAbsent => 2, @@ -144,9 +135,8 @@ impl LifecycleDurabilityBarrier for RecoveryPublisher { /// ordinary publication and revision GC retain physical deletion semantics. /// /// The object boundary cannot prove that a failed destructive call was not -/// dispatched, and it currently exposes no multipart-abort operation. Both -/// cases therefore fail closed as ambiguous so the metadata operation is -/// quarantined instead of retrying an uncertain deletion. +/// dispatched, so that case fails closed as ambiguous and the metadata +/// operation is quarantined instead of retrying an uncertain deletion. #[derive(Clone)] pub struct ArtifactLifecycleCleaner { store: Arc, @@ -166,11 +156,6 @@ where &self, request: &LifecycleCleanupRequest, ) -> Result { - if request.multipart_upload_id.is_some() { - return Err(LifecycleCleanupError::Ambiguous { - evidence: b"configured object provider has no multipart-abort boundary".to_vec(), - }); - } let key = ObjectKey::new(request.object_key.clone()).map_err(|error| { LifecycleCleanupError::Ambiguous { evidence: format!("authoritative lifecycle object key is invalid: {error}") @@ -794,7 +779,6 @@ impl LifecycleRunner { LifecycleCleanupPurpose::AbortedPublication }, object_key: expected.object_key.clone(), - multipart_upload_id: expected.multipart_upload_id.clone(), }; match self.objects.cleanup(&request) { Ok(proof) if proof.matches_request(&request) => { @@ -1511,7 +1495,6 @@ impl LifecycleRunner { let request = LifecycleCleanupRequest { purpose: LifecycleCleanupPurpose::RevisionGarbageCollection, object_key: entry.row.object_key.clone(), - multipart_upload_id: None, }; match self.objects.cleanup(&request) { Ok(proof) if proof.matches_request(&request) => { @@ -2011,7 +1994,6 @@ mod tests { LifecycleCleanupRequest { purpose: LifecycleCleanupPurpose::RevisionGarbageCollection, object_key: "nokv/artifacts/object".to_owned(), - multipart_upload_id: None, } } @@ -2027,7 +2009,6 @@ mod tests { let request = LifecycleCleanupRequest { purpose: LifecycleCleanupPurpose::AbortedAppend, object_key: key.as_str().to_owned(), - multipart_upload_id: None, }; for _ in 0..2 { let proof = cleaner.cleanup(&request).unwrap(); @@ -2095,22 +2076,6 @@ mod tests { assert_eq!(store.delete_calls.load(Ordering::SeqCst), 1); } - #[test] - fn artifact_lifecycle_deleter_quarantines_multipart_before_dispatch() { - let store = Arc::new(FakeArtifactStore { - delete_result: Ok(ObjectDeleteOutcome::Deleted), - delete_calls: AtomicUsize::new(0), - }); - let deleter = ArtifactLifecycleCleaner::new(Arc::clone(&store)); - let mut request = lifecycle_delete_request(); - request.multipart_upload_id = Some(b"upload-id".to_vec()); - - let error = deleter.cleanup(&request).unwrap_err(); - - assert!(matches!(error, LifecycleCleanupError::Ambiguous { .. })); - assert_eq!(store.delete_calls.load(Ordering::SeqCst), 0); - } - #[test] fn artifact_lifecycle_deleter_quarantines_provider_error() { let key = ObjectKey::new("nokv/artifacts/object").unwrap(); @@ -2539,7 +2504,6 @@ mod tests { let request = LifecycleCleanupRequest { purpose: LifecycleCleanupPurpose::AbortedPublication, object_key: "nokv/artifacts/01010101010101010101010101010101/02020202020202020202020202020202/03030303030303030303030303030303/blocks/0000000000000007".to_owned(), - multipart_upload_id: Some(b"multipart-7".to_vec()), }; let proof = LifecycleCleanupProof::from_cleanup_request( &request, @@ -2548,9 +2512,9 @@ mod tests { assert_eq!( proof.digest, [ - 0xdd, 0xcf, 0x71, 0xa4, 0x41, 0x4e, 0x6c, 0x96, 0xc9, 0x0a, 0x30, 0xf1, 0xc2, 0x28, - 0x78, 0x54, 0x50, 0x2e, 0x2c, 0x3f, 0x15, 0x56, 0x2c, 0xf8, 0xbd, 0xed, 0xd2, 0x69, - 0xd0, 0x33, 0x7c, 0xe1 + 0xb3, 0x1c, 0x58, 0xdf, 0x86, 0x18, 0x29, 0x4f, 0xec, 0xae, 0x2d, 0xa7, 0x5f, 0x06, + 0x68, 0x23, 0x20, 0x23, 0xd3, 0x8b, 0x78, 0x84, 0xe9, 0x35, 0x36, 0x1f, 0x34, 0xab, + 0x8c, 0xb7, 0xdf, 0x80 ] ); assert_eq!( @@ -2574,16 +2538,6 @@ mod tests { ) .digest ); - changed = request.clone(); - changed.multipart_upload_id = None; - assert_ne!( - proof.digest, - LifecycleCleanupProof::from_cleanup_request( - &changed, - LifecycleCleanupDisposition::Deleted, - ) - .digest - ); assert_ne!( proof.digest, LifecycleCleanupProof::from_cleanup_request( @@ -3110,7 +3064,6 @@ mod tests { artifact_revision_id: revision, object_sequence: 0, object_key: meta::object_block_key(shard(), root(), revision, 0), - multipart_upload_id: None, expected_length: 8, expected_digest_uri: sha256_uri([0x73; SHA256_BYTES]), provider_state: StagedProviderState::Planned, @@ -3705,7 +3658,6 @@ mod tests { artifact_revision_id: shared_revision, object_sequence: 0, object_key: shared_object_key.clone(), - multipart_upload_id: None, expected_length: block_bytes.len() as u64, expected_digest_uri: sha256_uri([0x61; SHA256_BYTES]), provider_state: StagedProviderState::Planned, @@ -4008,7 +3960,6 @@ mod tests { artifact_revision_id: shared_revision, object_sequence: 0, object_key: shared_object_key.clone(), - multipart_upload_id: None, expected_length: block_bytes.len() as u64, expected_digest_uri: sha256_uri([0x61; SHA256_BYTES]), provider_state: StagedProviderState::Planned, diff --git a/crates/nokv-types/src/workspace.rs b/crates/nokv-types/src/workspace.rs index 5ba7a75cf..5f01d6458 100644 --- a/crates/nokv-types/src/workspace.rs +++ b/crates/nokv-types/src/workspace.rs @@ -843,7 +843,6 @@ durable_enum! { /// Provider-observed state for one staged immutable object. pub enum StagedProviderState { Planned = 1, - Uploading = 2, Uploaded = 3, AbortPending = 4, Aborted = 5, @@ -1477,7 +1476,6 @@ mod tests { ]); assert_durable_registry(&[ (StagedProviderState::Planned, 1), - (StagedProviderState::Uploading, 2), (StagedProviderState::Uploaded, 3), (StagedProviderState::AbortPending, 4), (StagedProviderState::Aborted, 5), diff --git a/crates/nokv/src/main.rs b/crates/nokv/src/main.rs index 54158f40e..cce3b9024 100644 --- a/crates/nokv/src/main.rs +++ b/crates/nokv/src/main.rs @@ -2495,7 +2495,6 @@ mod tests { expected_digest: nokv_protocol::sha256_digest_uri(nokv_protocol::Digest( [0x44; 32], )), - multipart_token: None, }], next_after: Some(1), }, diff --git a/docs/architecture.md b/docs/architecture.md index 103a608c9..eafb9dfd6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -486,7 +486,7 @@ is therefore an opt-in, not the resting state. Durable ledgers, not object listing, recover: -- staged/multipart uploads; +- staged uploads; - commit construction; - restore staging and cleanup; - GC claims and ambiguous deletes. diff --git a/docs/metadata-schema.md b/docs/metadata-schema.md index b41f542df..eb850c9f4 100644 --- a/docs/metadata-schema.md +++ b/docs/metadata-schema.md @@ -133,8 +133,9 @@ fail-closed and unchanged. Durable codecs are independently versioned: the publish operation record uses value version `9` and rejects version `8`; -staged-object and manifest-row records use value version -`7` and reject version `6`; an append publication binds both the full 256-bit +manifest-row records use value version `7` and reject version `6`; the +staged-object record uses value version `8`, which dropped the multipart +upload id, and rejects version `7`; an append publication binds both the full 256-bit intent and logical parent/attempt, with a cleanup retry counter that prevents repeated quarantine from recreating an earlier state token. `AppendOperationRecord` uses value version `2` and retains the current attempt, optional compact receipt, and initiator. An @@ -242,7 +243,7 @@ RestorePhase: Preparing=1, Copying=2, SourceSealed=3, Ready=4, DestinationSealing=11 CommitRetirePhase: Claiming=1, Releasing=2, Complete=3, Quarantined=4 GcPhase: Queued=1, Claimed=2, Deleting=3, Deleted=4, Quarantined=5 -StagedProviderState: Planned=1, Uploading=2, Uploaded=3, AbortPending=4, +StagedProviderState: Planned=1, Uploaded=3, AbortPending=4, Aborted=5, Ambiguous=6 StagedCleanupState: Owned=1, DeletePending=2, Deleted=3, Quarantined=4 GcClaimState: Candidate=1, Claimed=2, Complete=3, Quarantined=4 @@ -485,8 +486,8 @@ RestoreMember StagedObject key: root_id | publish_operation_id | object_sequence - val: artifact revision, object key, multipart/upload id, - expected length/digest, provider state, cleanup state + val: artifact revision, object key, expected length/digest, + provider state, cleanup state CommandDedupe key: root_id | request_id @@ -759,7 +760,7 @@ the old candidate stale. Publication is object-first and metadata-last. Before upload, a `PublishOperation` and exact `StagedObject` ledger own all object keys, -multipart ids, lengths, and digests. +lengths, and digests. The mutually exclusive operation transitions are: @@ -797,7 +798,7 @@ retained staged rows, bound to the exact current logical/child state. Generic pu revision GC retain their existing deletion semantics. A late upload completion must observe the operation state; after abort it joins -cleanup instead of publishing. Ambiguous multipart completion, late PUT, or +cleanup instead of publishing. An ambiguous create, late PUT, or DELETE remains ledger-owned and `Quarantined` until reconciled. Object listing is never used to discover staged ownership. diff --git a/docs/product-design.md b/docs/product-design.md index 6ac3c3a87..d0ab944f5 100644 --- a/docs/product-design.md +++ b/docs/product-design.md @@ -172,8 +172,8 @@ with the same request id. Failed or abandoned uploads remain invisible and are recovered from a durable staged-object ledger. Whole-artifact replacement is the generic write primitive. Logs use immutable -append segments plus a conditional stream-head advance. Range reads and -multipart uploads remain first-class SDK operations. +append segments plus a conditional stream-head advance. Range reads remain a +first-class SDK operation. ## Discovery And Provenance diff --git a/scripts/workbench/append_operations_acceptance_gate.py b/scripts/workbench/append_operations_acceptance_gate.py index be180a95d..75cedcb9d 100644 --- a/scripts/workbench/append_operations_acceptance_gate.py +++ b/scripts/workbench/append_operations_acceptance_gate.py @@ -418,8 +418,7 @@ def inspect_all( require( row["expected_length"] == len(block) and row["expected_digest"] == "sha256:" + hashlib.sha256(block).hexdigest() - and row["object_identity"].endswith(f"/blocks/{row['sequence']:016x}") - and row["multipart_token"] is None, + and row["object_identity"].endswith(f"/blocks/{row['sequence']:016x}"), "public ledger preserves exact expected object identity, bytes, and digest", row, )