Skip to content

Compare storage namespaces by physical location - #2105

Merged
ppXD merged 1 commit into
mainfrom
fix/compare-storage-namespaces-by-physical-location
Oct 12, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/compare-storage-namespaces-by-physical-location

Conversation

@ppXD

@ppXD ppXD commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • The retention reaper decided whether a routed object key was shared with another team by comparing namespace fingerprints. A fingerprint was the hash of the configuration exactly as typed. Object keys carry no team segment, so R, R/ and R/. of one local-rwx root counted as different namespaces, and one team's sweep deleted another team's live bytes.
  • Each provider module now projects its configuration onto the physical namespace it reaches. Admission, the agent-run-log bootstrap and the deployment-default materializer store that projection's hash. A configuration already written canonically keeps a byte-identical fingerprint.
    • Local roots: made absolute, trimmed of trailing separators, and resolved through symlinks in the order the OS uses (LocalRootPath.ResolveSymbolicLinks). So L -> a/.. climbs from where a lands, not from L's directory. Names are case-folded where the platform's filesystem ignores case.
    • OSS configurations: reduced to bucket, region and normalised key prefix.
  • The reaper no longer reads stored fingerprints, because revisions are immutable and older ones hash the typed text. It re-derives, at sweep time, the object each colliding placement reaches (StorageProfileRules.TryObjectIdentity).
    • A namespace is not enough for a filesystem: the driver opens <root>/objects/<key>, and the OS follows a link anywhere along that path.
    • So local-rwx implements the new sibling capability IStorageProviderPhysicalObjects. It names the file a delete of the key would unlink: every directory on the way is resolved, and the final name stays as written, because unlink removes a link and not its target.
    • Other providers are identified by namespace plus key.
    • An identity the reaper cannot derive counts as shared (Indeterminate), which keeps the bytes.

Test plan

  • Unit:
    • LocalStorageNamespaceTests:
      • every spelling, relative, .. and // form, for existing and not-yet-created roots;
      • a link whose target climbs through another link;
      • object identity through a root symlink, an objects link and a nested link;
      • a key that is itself a link;
      • invalid keys;
      • a drift detector against the real LocalRwxArtifactStorageDriver;
      • the hard-pinned canonical fingerprint.
    • StorageNamespaceIdentityTests and the OSS namespace tests.
  • Integration over real Postgres with two teams (ArtifactRetentionReaperFlowTests.Namespaces):
    • every spelling, including dotdot-through-link, objects-link and nested-link, ends Indeterminate with the victim's file intact;
    • distinct roots are collected;
    • a key that links to another team's file is collected, and only the link is removed;
    • an unreadable or unregistered revision ends Indeterminate.
  • E2E over HTTP with two teams (StorageNamespaceSharingE2ETests): the victim's download returns 200 after the other team's sweep, for every spelling and link shape.
  • Mutation checks, each of which turns the matching tests red:
    • dropping the physical-object identity;
    • collapsing link targets as text;
    • following the key's final name.
  • Regression:
    • full unit suite (13293 passed, 1 skipped);
    • storage, retention and agent-run-log integration (347/347) and E2E (56/56);
    • ResourceAccessMarkerInventoryTests and ControllersDispatchThroughMediatorConventionTests.

The retention reaper decided whether a routed object key was shared
with another team by comparing namespace fingerprints, and a
fingerprint was the hash of the configuration exactly as a team typed
it. Object keys carry no team segment, so 'R', 'R/' and 'R/.' of one
local-rwx root counted as three namespaces, and one team's sweep
deleted another team's live bytes.

Each provider module now projects its configuration onto the physical
namespace it reaches: a local root is made absolute, trimmed of
trailing separators, resolved through existing symlinks the way
realpath(3) resolves them (a link target is walked component by
component, so 'L -> a/..' climbs from where 'a' lands, not from L's
directory) and case-folded where the platform's filesystem ignores
case; an OSS configuration reduces to bucket, region and key prefix,
merging every host that can only reach that bucket. Admission, the
agent-run-log bootstrap and the deployment-default materializer all
store that projection's hash, and a configuration already written
canonically keeps the fingerprint it had.

Existing revisions are immutable and their fingerprints still name the
typed text, so the reaper no longer reads them: it re-derives the
object each colliding placement reaches at sweep time. A namespace is
not enough for a filesystem, because the driver opens
<root>/objects/<key> and the OS follows a link anywhere along it; a
root the collector owns outright can link 'objects' (or any directory
below it) into the victim's. So local-rwx, through the new
IStorageProviderPhysicalObjects capability, names the file a delete of
the key would unlink, with every directory on the way resolved and the
final name left as written (unlink removes a link, not its target).
Other providers are identified by namespace plus key. An object the
reaper cannot derive (an unregistered provider, an unreadable
configuration, a key no driver accepts) counts as shared, which keeps
the bytes.

The verifier flow tests asserted after one bounded pass over the whole
table. Rows behind roots that earlier tests deleted never move, and the
new two-team fixtures pushed their count past the batch, so those tests
now sweep at the verifier's ceiling and wait on their own row.
@ppXD
ppXD merged commit ee31cd2 into main Oct 12, 2026
7 checks passed
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.

1 participant