Skip to content

Add named-blob cleanup container exclusion list - #3286

Merged
zichengl merged 4 commits into
linkedin:masterfrom
zichengl:8845012+zichengl/namedblob-cleanup-container-exclusion
Aug 20, 2026
Merged

Add named-blob cleanup container exclusion list#3286
zichengl merged 4 commits into
linkedin:masterfrom
zichengl:8845012+zichengl/namedblob-cleanup-container-exclusion

Conversation

@zichengl

@zichengl zichengl commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in exclusion list to the named-blob stale-data cleanup runner so operators can exempt specific containers from cleanup. New config frontend.named.blob.cleanup.excluded.containers is a comma-separated list of fully-qualified accountName/containerName entries. Excluded containers are skipped by NamedBlobsCleanupRunner, so their superseded (stale) named-blob versions are retained instead of deleted. Defaults to empty, so there is no behavior change until an operator opts a container in.

Motivation: when named blobs are enabled, re-uploading the same name supersedes the previous version, and the cleanup runner eventually deletes the stale version(s). A consumer that has pinned an internal blob id (instead of reading by name) then gets 410 Gone once that pinned version is cleaned up. Reading by blob name is the durable fix; this exclusion list is a temporary, reversible safety valve that retains stale versions for specific containers while such consumers migrate.

Behavior:

  • NamedBlobsCleanupRunner skips any non-DISABLED container whose accountName/containerName is in the list.
  • Fail-safe: if the parent account cannot be resolved, the container is treated as not excluded, so an account-lookup miss can never silently suppress cleanup across all containers.
  • When the list is empty the runner does no extra account lookups (short-circuits), so the default path is unchanged.

Testing Done

  • ./gradlew :ambry-frontend:test --tests com.github.ambry.frontend.NamedBlobsCleanupRunnerTest — 6/6 pass, including new tests:
    • testExcludedContainersAreSkipped — an excluded container is never pulled/deleted; a sibling container in the same account still is.
    • testContainerProcessedWhenAccountUnresolvedDespiteExclusionList — an unresolved account leaves the container processed (fail-safe).
  • ./gradlew :ambry-api:test --tests com.github.ambry.config.FrontendConfigTest — pass, including testNamedBlobCleanupExcludedContainers (default empty + comma-separated parsing).

Risk

  • Additive and safe-by-default: an empty list preserves current behavior. The feature can only ever cause the cleaner to delete less, never more.
  • Fully reversible: remove entries (or clear the property) to resume cleanup.
  • Possible follow-up: support account-level entries (whole-account exclusion) in addition to per-container.

@zichengl
zichengl marked this pull request as ready for review August 19, 2026 23:25
Add a frontend.named.blob.cleanup.excluded.containers config (comma-separated
"accountName/containerName" entries) that lets operators exempt specific
containers from the named blob stale-data cleanup runner, so their superseded
named-blob versions are retained instead of deleted. Defaults to empty, so
there is no behavior change until an operator opts a container in. Intended as
a temporary safety valve for consumers that still read blobs by internal blob
id and depend on stale versions while they migrate to reading by name.

NamedBlobsCleanupRunner now skips excluded containers (matched by
accountName/containerName). If the parent account cannot be resolved the
container is treated as not excluded, so cleanup is never silently suppressed
fleet-wide.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@zichengl
zichengl force-pushed the 8845012+zichengl/namedblob-cleanup-container-exclusion branch from 0aca149 to 77c7238 Compare August 19, 2026 23:30
if (account == null) {
return false;
}
return excludedContainers.contains(account.getName() + "/" + container.getName());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: check if containers with special characters parsed correctly (There are containers with whitespace allowed historically)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Utils.splitString splits only on , and filters empty segments without trimming, so whitespace inside a name is preserved, and the runner matches accountName/containerName verbatim — names with spaces resolve correctly. I documented the exact-match (whitespace-significant) semantics on the config field, and added testExcludedContainerWithWhitespaceInNameIsSkipped which parses "my account/my container" via Utils.splitString (exactly as FrontendConfig does) and asserts the container is skipped. See 21ab1fd.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Digging in more, documenting "don't pad with spaces" isn't enough — it's a silent footgun. Utils.splitString(value, ",") splits on comma and drops empties but doesn't trim, so a list written the normal way — a/b, c/d — keeps a leading space on every entry after the first (" c/d"). That never matches accountName/containerName, so the container just keeps getting cleaned: the exact data loss this list exists to prevent, and it fails quietly.

So I switched to trimming each entry (and dropping blanks). Trimming only removes the outer padding, so whitespace inside a name is still significant — my account/my container matches verbatim, and the historical whitespace case you flagged still works. The one case you can't express this way is a name that literally begins or ends with a space; I called that out on the config field as out of scope for this list.

Added FrontendConfigTest coverage for the space-padded / blank-entry list and the internal-whitespace name. c7943f2

…usion list

Account/container names may historically contain whitespace. Utils.splitString
(comma split, empty-segment filter, no trimming) preserves such names, and the
runner matches accountName/containerName verbatim. Document the exact-match
semantics on the config and add a test that a container whose name contains a
space is parsed and skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 36.00000% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.82%. Comparing base (52ba813) to head (ef87010).
⚠️ Report is 410 commits behind head on master.

Files with missing lines Patch % Lines
...github/ambry/frontend/NamedBlobsCleanupRunner.java 16.66% 14 Missing and 1 partial ⚠️
...n/java/com/github/ambry/config/FrontendConfig.java 83.33% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #3286       +/-   ##
=============================================
- Coverage     64.24%   50.82%   -13.42%     
+ Complexity    10398     8706     -1692     
=============================================
  Files           840      938       +98     
  Lines         71755    80491     +8736     
  Branches       8611     9689     +1078     
=============================================
- Hits          46099    40913     -5186     
- Misses        23004    36195    +13191     
- Partials       2652     3383      +731     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

if (excludedContainers.isEmpty()) {
return false;
}
Account account = accountService.getAccountById(container.getParentAccountId());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this additional look up on account here? does container have accountName in it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Container carries only getParentAccountId() (the numeric id), not the account name, so to match on a human-readable accountName/containerName we do need to resolve the name. A couple of things keep the cost down:

  • It's guarded by excludedContainers.isEmpty() and returns before any lookup, so in the normal case (no exclusions configured) there's zero overhead.
  • When a list is set, getAccountById is an in-memory AccountService lookup, not a round trip.

I went with names rather than ids because this is a hand-edited safety-valve config and account-id/container-id is opaque to operators. If you'd rather drop the lookup entirely, I can key on parentAccountId + "/" + containerId (both already on Container), which also happens to be rename-stable — just less readable in the config. Happy to switch if you prefer that trade.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Container only carries the numeric parent account id (getParentAccountId()) — it doesn't expose the account name. So to match the name-based accountName/containerName key we do have to resolve it via accountService.getAccountById(id).getName(); the name simply isn't on the container.

It's intentionally cheap, though:

  • short-circuited by excludedContainers.isEmpty(), so there are zero extra lookups in the normal (no-exclusions) case;
  • getAccountById is an in-memory cache hit, and the runner already holds accountService;
  • a null account is treated as not excluded (fail-safe).

We could drop the lookup by keying the list on the numeric identity the container already has (parentAccountId/containerName, or parentAccountId:containerId = Container.toString()), but the list is human-maintained by name (sourced from the tracking sheet), so I kept it name-based for operability. Happy to switch to id-based — or iterate getAllAccounts() so the Account (and its name) is in hand without the per-container lookup — if you prefer.

The exclusion list is parsed with Utils.splitString(value, ","), which splits
on comma without trimming. A list written the natural way -- "a/b, c/d" -- then
keeps a leading space on every entry after the first, so it never matches the
verbatim accountName/containerName and the container is silently still cleaned
(its stale versions deleted -- the exact outcome the exclusion list prevents).

Trim each entry (dropping blanks) so space-padded lists work. Only the outer
padding is removed, so whitespace *within* a name -- which Ambry historically
allows -- stays significant and "my account/my container" still matches. Names
whose value begins or ends with whitespace cannot be expressed via this list;
documented on the config field. Added FrontendConfigTest coverage for both the
space-padded/blank-entry and internal-whitespace cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve conflicts from master's resilient-scan refactor of NamedBlobsCleanupRunner
(per-container resume cursors, bounded retries, MetricRegistry) by integrating the
exclusion-list feature into the new structure:
- thread excludedContainers through the metric-aware constructors (canonical is now
  (delay, excludedContainers, time, metricRegistry));
- skip excluded containers in the run() loop, right after the DISABLED check;
- keep isExcludedFromCleanup and the exclusion tests; pass
  frontendConfig.namedBlobCleanupExcludedContainers in FrontendRestRequestService.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@zichengl
zichengl merged commit c7401ee into linkedin:master Aug 20, 2026
11 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.

3 participants