fix(hubs): never lower retention on redeploy - #2288
fix(hubs): never lower retention on redeploy#2288Michael Flanakin (flanakin) wants to merge 1 commit into
Conversation
Copy-FileToAzureBlob.ps1 unconditionally overwrote stored retention months with whatever the deploymentScript's Bicep parameters passed. Bicep always resolves a value for an optional parameter (defaulting to 13 if omitted), so the script couldn't tell an explicit redeploy value from a silently-defaulted one. A redeploy that dropped a previously-customized retention value silently reset it, and the next purge pipeline run aged out historical data older than the new cutoff -- oldest data first (#2206). Take the max of stored and incoming retention instead of overwriting: growing is always safe, shrinking is destructive and hard to reverse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
The [Math]::Max guard is the right call — growing retention is safe, shrinking it silently purges data. Three things before this goes in, one of which means the PR as titled doesn't quite do what it says.
The same failure mode is still open on raw, in a path this guard structurally cannot reach. Details inline — short version is that retention.raw.days is written to settings.json and read by nothing, while the real raw retention is an ADX softdelete policy applied straight from the Bicep param at deploy time. Happy for that to be a follow-up rather than scope creep here, but "never lower retention on redeploy" currently overpromises.
Two of the six new tests can never fail. The Should -Not -Match anchors are wrong, so both regression guards pass whether or not the bug comes back. Inline.
No way to deliberately lower retention. After this, retention is a one-way ratchet — a customer who wants to reduce it to cut storage cost can't do it by redeploying, and there's no documented alternative. On a FinOps toolkit that's a pointed limitation, and the PR has no doc or changelog change. Even a line in the hubs docs saying "edit settings.json in the hub storage account to lower it" would close this.
Requesting changes mainly for the test anchors, since those are load-bearing for the regression this PR exists to prevent.
| } | ||
|
|
||
| It 'Should not unconditionally overwrite ingestion retention' { | ||
| $content | Should -Not -Match '\$json\.retention\.ingestion\.months\s*=\s*\[Int32\]::Parse\(\$env:ingestionRetentionInMonths\)\s*$' ` |
There was a problem hiding this comment.
These two negative assertions (this one and the final equivalent on line 44) can never fail, so the regression they're guarding is unprotected.
$ here is end-of-string — -match doesn't set RegexOptions.Multiline — and the trailing \s* can't span the code that follows the assignment. So the pattern only matches if that assignment happens to be the last thing in the file. I checked against a sample with the regression pattern deliberately present mid-file:
PR2288 anchored pattern (\s*$) matches : False
same pattern without the anchor : True
Both tests pass today, and they'd still pass if someone reverted the fix. Dropping \s*$ from both patterns fixes it.
Minor, while you're here: these are source-text assertions, and the repo's convention for those is Tests/Lint/ (KqlJoinKinds.Tests.ps1, HubsKqlOperators.Tests.ps1) rather than Tests/Unit/.
| @@ -144,14 +147,14 @@ else | |||
| $json.retention.raw.days = [Int32]::Parse($env:rawRetentionInDays) | |||
There was a problem hiding this comment.
raw is left unguarded here, and it's the one where a silent shrink does the most damage.
I grepped src/ — nothing reads retention.raw.days back out of settings.json. This line is record-only. The retention that actually takes effect is baked into the KQL at deploy time (Analytics/app.bicep:369):
.alter-merge table ActualCosts_raw policy retention softdelete = $$rawRetentionInDays$$d recoverability = disabled
dataExplorerRawRetentionInDays defaults to 0 (main.bicep:154). So a customer who set it to, say, 30 and later redeploys without repeating the param gets softdelete = 0d recoverability = disabled reapplied to every *_raw table — an immediate and explicitly unrecoverable purge. That's the #2206 failure mode with a worse blast radius, and no settings.json guard can catch it because the value never round-trips through settings.json.
Two options: scope the title/description to the settings.json path, or open a follow-up for the raw policy. Either is fine, but it shouldn't go unrecorded.
| else | ||
| { | ||
| $json.retention.ingestion.months = [Int32]::Parse($env:ingestionRetentionInMonths) | ||
| $json.retention.ingestion.months = [Math]::Max($json.retention.ingestion.months, [Int32]::Parse($env:ingestionRetentionInMonths)) |
There was a problem hiding this comment.
Worth spelling out the consequence in the docs: after this, stored retention can only ever grow. A customer who deliberately wants to lower retention to cut storage cost has no supported path — redeploying with a smaller value is now a no-op, silently.
That's the correct default, but it needs an escape hatch or at least a documented manual one ("edit settings.json in the hub storage account"). Otherwise the next issue is someone asking why their retention change didn't apply.
|
@Michael Flanakin (@flanakin): you have some new feedback! Please review and resolve all comments and I'll let reviewers know by removing the |
Summary
Copy-FileToAzureBlob.ps1unconditionally overwrotesettings.json'sretention.ingestion.months/retention.final.monthswith whatever value the deploymentScript's Bicep parameters passedTest plan
HubsRetentionGuard.Tests.ps1asserting the max-guard is present and the old unconditional-overwrite pattern is gone, plus that first-run seeding (no existingsettings.json) still honors the requested valueRelated: #2206
🤖 Generated with Claude Code