Add named-blob cleanup container exclusion list - #3286
Conversation
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>
0aca149 to
77c7238
Compare
| if (account == null) { | ||
| return false; | ||
| } | ||
| return excludedContainers.contains(account.getName() + "/" + container.getName()); |
There was a problem hiding this comment.
nit: check if containers with special characters parsed correctly (There are containers with whitespace allowed historically)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
| if (excludedContainers.isEmpty()) { | ||
| return false; | ||
| } | ||
| Account account = accountService.getAccountById(container.getParentAccountId()); |
There was a problem hiding this comment.
do we need this additional look up on account here? does container have accountName in it?
There was a problem hiding this comment.
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,
getAccountByIdis an in-memoryAccountServicelookup, 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.
There was a problem hiding this comment.
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; getAccountByIdis an in-memory cache hit, and the runner already holdsaccountService;- 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>
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.containersis a comma-separated list of fully-qualifiedaccountName/containerNameentries. Excluded containers are skipped byNamedBlobsCleanupRunner, 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 Goneonce 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:
NamedBlobsCleanupRunnerskips any non-DISABLEDcontainer whoseaccountName/containerNameis in the list.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, includingtestNamedBlobCleanupExcludedContainers(default empty + comma-separated parsing).Risk