fix(datastores): namespace-scope controlPlaneStore keys, migrate root _control/, add lock namespace support (#1889) #242
Loading…
Reference in a new issue
No description provided.
Delete branch "worktree-1889"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
ensureBound()— if a namespace was bound bypullChanged, control-plane operations use it; if unbound, solo mode bindsundefined. Prevents silent root-level writes in namespaced deployments.list()evaluatescontrolPrefixPath()lazily instead of capturing a stale prefix at construction time.migrateRootControlPlaneToNamespace()copies orphaned root-level_control/keys to{namespace}/_control/on push, withhasNamespacedIndexguard (only runs for solo-to-namespace transitions). Tracked viacontrolPlaneKeysMigratedsync state flag.LockOptions.namespaceprefixes the lock key under{namespace}/so namespace-scoped IAM policies can reach it. Core adoption tracked in #1897.wrapErrornow includes the S3 key in error messages soAccessDeniederrors show which key was targeted.Core-side fixes for the root-level global lock derivation and undefined-namespace caller tracked in #1897.
Closes #1889, closes #1896.
Test plan
deno check,deno lint,deno fmt --check,deno install --frozenpass for both extensionsdatastore-s3anddatastore-gcs🤖 Generated with Claude Code
Three independent fixes for namespaced datastore deployments where IAM credentials are scoped to the namespace prefix: 1. controlPlaneStore() now participates in namespace binding via ensureBound() — if a namespace was bound by pullChanged, control-plane operations use it; if no binding exists, solo mode binds undefined. list() evaluates controlPrefixPath() lazily instead of capturing a stale prefix at construction time. 2. New migrateRootControlPlaneToNamespace() migrates orphaned root-level _control/ keys to {namespace}/_control/ on push, guarded by hasNamespacedIndex (only runs for solo-to-namespace transitions). Tracked via controlPlaneKeysMigrated sync state flag. 3. LockOptions.namespace prefixes the lock key under {namespace}/ so namespace-scoped IAM policies can reach it. S3 wrapError now includes the S3 key in error messages so AccessDenied errors show which key was targeted. All changes applied symmetrically to S3 and GCS. Core-side fixes for the root-level global lock derivation and undefined-namespace caller tracked in swamp-club#1897. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Code Review
Blocking Issues
None.
Suggestions
GCS/S3
migrateRootControlPlaneToNamespace: guard silently swallows all errors, inconsistent withmigrateRootDataToNamespace.The existing
migrateRootDataToNamespacerethrows non-NotFoundErrors from the namespaced-index guard check. The newmigrateRootControlPlaneToNamespace(both GCS and S3) uses a bare catch that swallows all errors and comments "Can't check — skip migration". A transient 5xx or network timeout during the guard check will silently skip migration — and once a subsequent push recordscontrolPlaneKeysMigrated = true, the migration opportunity is permanently suppressed. The data-key variant correctly propagates transient errors so they surface as sync failures and are retried on the next call. The control-plane variant should do the same, or at minimum not setcontrolPlaneKeysMigrated = truewhen the guard errored rather than returnedexists: false.s3_client.ts: error message key includes the full S3 bucket-prefix path; span attribute uses the logical key.run()extractsinputKeyfrom the SDK command'sinput.Key(the full S3 key including the configured prefix, e.g.swamp/data/@org/file.yaml) and passes it towrapError. The span attribute correctly strips the prefix to record the logical key. The resulting error message showskey=swamp/data/@org/file.yaml, which may confuse users who only see logical keys. Minor cosmetic issue -- passinglogicalKeytowrapErrorinstead would be consistent with how the span is recorded.preparePushmigration path not covered by tests.Both S3 and GCS test the control-plane migration via
pushChangedbut not viapreparePush(the two-phase path). Since both entry points call the same private method the gap is low-risk, but a test exercisingpreparePush+commitPushagainst a bucket with root_control/keys would complete the coverage.Adversarial Code Review
PR: Namespace-scope controlPlaneStore keys, migrate root _control/, add lock namespace support
Findings
1. GCS migration declares wrong type for listing entries
Severity: MEDIUM
Category: Logic / Correctness
File:
datastore/gcs/extensions/datastores/_lib/gcs_cache_sync.ts:770The
migrateRootControlPlaneToNamespacemethod declares:But
GcsClient.listAllObjects()returnsPromise<GcsListEntry[]>whereGcsListEntryis:The S3 version correctly uses
lastModifiedwhich matches its client return type. The GCS version uses a field name (lastModified) that does not exist on the actual return type. The field is not accessed in the migration body (onlykeyis used), so there is no runtime bug today. However:entry.lastModifiedis available.generationfield is also dropped, preventing future use without re-typing.Fix: Change the type annotation to match
GcsListEntryor import the type directly.2. Silent error swallowing on index existence check can skip needed migration
Severity: LOW
Category: Error Handling
File:
datastore/gcs/extensions/datastores/_lib/gcs_cache_sync.ts:760-767File:
datastore/s3/extensions/datastores/_lib/s3_cache_sync.ts:819-827Both migration methods catch and ignore errors when checking if the namespaced index exists. If the HEAD/metadata call fails due to a transient error (network timeout, temporary auth failure, rate limit), migration is silently skipped. The
controlPlaneKeysMigratedflag is never set totrue, so migration will be re-attempted on the nextpushChangedcall -- making this self-healing.The comment documents the intent. This is a reasonable defensive choice, but persistent transient failures will cause the migration to be silently re-attempted on every push without user-visible diagnostics.
Suggestion: Consider logging a warning on catch so operators can diagnose repeated skips.
3. Delete failures during control-plane migration leave orphaned source keys
Severity: LOW
Category: Data Integrity
File:
datastore/gcs/extensions/datastores/_lib/gcs_cache_sync.ts:807-818File:
datastore/s3/extensions/datastores/_lib/s3_cache_sync.ts:867-879After successfully copying keys, both implementations delete the originals using
Promise.allSettled. Failed deletes are silently ignored. Whencopied === totalbut some deletes fail,controlPlaneKeysMigratedis set totrueand the orphaned root keys persist indefinitely.This is acceptable because the namespaced copies are authoritative after migration. However, the data key migration logs partial failure warnings. The control-plane migration should match this pattern for consistency and observability.
4. Listing failure falsely marks migration as permanently complete
Severity: MEDIUM
Category: Logic / Correctness
File:
datastore/gcs/extensions/datastores/_lib/gcs_cache_sync.ts:771-778File:
datastore/s3/extensions/datastores/_lib/s3_cache_sync.ts:830-838If
listAllObjects("_control/")fails after all retry attempts, migration returns{ copied: 0, total: 0 }. The caller then evaluates:When listing fails:
total = 0,copied = 0, so the condition istrueandcontrolPlaneKeysMigrated = true. Migration is marked complete without ever verifying whether root keys exist.This is a real logic gap: a listing failure (permissions, network) permanently marks migration as done, leaving root
_control/keys unscoped. The migration will never retry.Fix: Return a sentinel that distinguishes "no keys found" from "listing failed", e.g.
{ copied: 0, total: -1 }, and guard:if (total >= 0 && (total === 0 || copied === total)).5. Code pattern inconsistency: GCS vs S3 data-key migration sidecar ordering
Severity: LOW
Category: Logic / Correctness
File:
datastore/gcs/extensions/datastores/_lib/gcs_cache_sync.ts:2032-2052File:
datastore/s3/extensions/datastores/_lib/s3_cache_sync.ts:2160-2171GCS data-key migration duplicates the sidecar write block across two branches (inside and outside
if (copied > 0)), while S3 writes the sidecar once unconditionally before the early return check. Both are functionally equivalent but the divergence means a future fix to one may not be applied to the other.6. ControlPlaneStore.list() prefix semantics undocumented
Severity: LOW
Category: API Contracts
File:
datastore/gcs/extensions/datastores/_lib/gcs_cache_sync.ts:3286-3293File:
datastore/s3/extensions/datastores/_lib/s3_cache_sync.ts:3459-3466The
listmethod stripscpPrefix(e.g.,my-ns/_control/) from keys but does NOT strip the caller-providedprefix. If a caller doeslist("heartbeats/"), they get back["heartbeats/inst-1", ...]not["inst-1", ...].This is consistent with typical list-by-prefix APIs and tests confirm the intended behavior. However, the
ControlPlaneStoreinterface (interfaces.ts:117) does not document whether the prefix is stripped from results.Suggestion: Add a JSDoc comment to the interface clarifying return key semantics.
7. Lock namespace tests verify key placement but not contention isolation
Severity: LOW
Category: Concurrency
File:
datastore/gcs/extensions/datastores/_lib/gcs_lock_test.ts:476-503File:
datastore/s3/extensions/datastores/_lib/s3_lock_test.ts:248-275The new namespace tests verify lock keys are placed under
{namespace}/.datastore.lock, which is correct. However, they do not test that a lock in namespace A does not block acquisition in namespace B. Given that contention isolation is the primary purpose of namespace-scoped locking, a cross-namespace non-contention test would strengthen confidence.Summary
lastModifiedinstead ofupdated(copy-paste from S3)No CRITICAL or HIGH severity findings.
Finding #4 has the most real-world impact potential: a transient listing failure during migration permanently marks migration as complete, leaving root
_control/keys unscoped. The window is narrow (requireslistAllObjectsto fail after all retries on a push where migration has not yet been marked done), but the consequence is silent data skew that will not self-heal.Finding #1 is a clear copy-paste discrepancy from the S3 implementation that should be corrected to avoid misleading future readers, even though it has no runtime impact today.
The overall implementation is solid: the namespace guard prevents cross-tenant key migration, idempotent copy-then-delete handles partial failures gracefully, the
controlPlaneKeysMigratedflag prevents unnecessary re-runs, and test coverage is thorough including both the happy path (migration runs) and the guard path (migration skips for fresh namespaces).