Repository navigation
fix(finops-hub): Keep retention on redeploy and upgrade unless specified - #2288
Conversation
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.
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Re-reviewed at 845afc8. The only change since the last pass is the changelog entry, so all three earlier threads are still open. Re-running the analysis against the full script and every consumer of settings.json turned up something more serious than those: the guard sits behind a hardcoded default, so this PR makes it impossible to configure retention below 13 on a fresh deploy.
The guard never sees a virgin object
Both paths that reach the guard pre-seed retention with a hardcoded 13 — the no-blob literal (~L70-86) and the pre-0.4 backfill (~L94-114). By the time line 137 runs, $json.retention.ingestion is always truthy, so:
| deploy | requested | pre-PR stored | this PR stores |
|---|---|---|---|
| fresh | ingestion 6 / final 3 | 6 / 3 | 13 / 13 |
| existing 36mo | default (13) | 13 (the #2206 bug) | 36 |
I ran the head script against stubbed Az cmdlets to confirm both rows. The #2206 fix does work — that's the second row, and it's worth keeping. But a customer who deploys a new hub asking for 6 months silently gets 13, and there is no redeploy that can bring it back down.
The corollary is that the Add-Member branches the guard falls back to are unreachable, which is why the two "first-run behavior unchanged" tests pass: they assert dead code, on precisely the case this PR breaks.
ingestion is not a purge control
I grepped every consumer of the retention settings. retention.ingestion.months has exactly one reader, and it isn't a purge:
ManagedExports/app.bicep:237 @subtractFromTime(startOfMonth(utcNow()), ...retention.ingestion.months, 'Month')
That's the managed-export backfill start date. The field that actually drives the purge pipeline is final:
Analytics/app.bicep:1244 @coalesce(...retention.final.months, 999)
So of the two fields this PR guards, only final is the #2206 mechanism. Ratcheting ingestion upward has the opposite of the intended effect: every redeploy re-backfills more months of exports than the caller asked for. The inline note ("shrinking silently ages out historical data the next time the purge pipeline runs") and the changelog entry both attribute purge behavior to ingestion that it doesn't have.
The unguarded sibling with a live consumer
My earlier thread flagged raw (still open, still valid — no settings.json consumer, real retention comes from the ADX softdelete policy). Re-reading the consumers, msexports is the one that matters more here, and it's still an unconditional overwrite at L124:
Exports/app.bicep:1636 @lessOrEquals(coalesce(...retention.msexports.days, 0), 0)
A redeploy that drops a customized msexportRetentionInDays resets it to the 0 default, and the pipeline goes back to deleting export files immediately after ingestion. Same failure mode as #2206, same trigger, unguarded.
Suggested shape
Distinguishing "caller asked for this" from "Bicep defaulted this" is the actual root cause, and a max-guard can't do it. Options, roughly in order of preference:
- Make the Bicep params nullable (
param ingestionRetentionInMonths int?) and have the script skip any field whose env var is empty. That fixes #2206 for every field includingmsexportsandraw, keeps lowering supported, and drops the need for a guard entirely. - Keep the max-guard but move it ahead of the hardcoded seeding, and apply it only to
final. Fresh deploys then honor the requested value, and the fields it doesn't apply to keep working as documented.
Either way, if increase-only stays for any field, it needs a documented way out (the escape-hatch thread) and the title/changelog need to say which field it covers.
Also: the PR is currently in a conflicting merge state against dev.
Still open from the last pass — none addressed: the \s*$ anchors that make both regression guards unfailable, the raw scope question, and the missing escape hatch for increase-only retention.
|
🤖 [AI][Claude] PR Update Summary Addressed: 1 of 6 open thread(s)
Implemented: Fixed the Left for maintainer review (not implemented — each involves retention-logic/data-lifecycle judgment):
Also flagging: the PR is currently in a conflicting merge state against |
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Regex fix verified, resolved that thread.
On the approach question in the L137 thread: agree that "keep the stored value" is cleaner than max. Either way, the guard has to run before the hardcoded months = 13 seeding (L72-83, L98-110). Right now the if (!($json.retention.ingestion)) branch never runs, so L137 is effectively Max(13, requested) and a new hub requesting 6 months stores 13 (still reproduces at e952c89). With "keep the stored value" as-is, the seeded 13 would always win, so the reorder is needed in both designs.
Also still open:
rawandmsexportsretention still overwrite unconditionally (L124, L147). Please either scope the title/description to ingestion/final or link a follow-up.- A short note on how to lower retention deliberately (edit
settings.json). - The "first-run behavior unchanged" tests still match the branch that can't execute; a mocked-Az test with no existing blob and
ingestionRetentionInMonths=6asserting 6 is written would cover the seeding bug. - The branch conflicts with dev (
docs-mslearn/toolkit/changelog.mdonly); please mergeorigin/dev.
|
Hubs + ADX (managed) deployment failed. View logs. |
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>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two 'should not unconditionally overwrite' assertions used a trailing \s*$ anchor. Without RegexOptions.Multiline, $ matches only the end of the whole file content, not end of line, so the pattern could only match if the offending assignment were the very last thing in the file. Both assertions passed whether or not the #2206 regression was present. Removed the anchor. Verified by temporarily reintroducing the old unconditional-overwrite line and re-running the suite: the test now fails red where it previously passed, then restored and confirmed green against the actual fix. Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com>
Make all retention parameters nullable (exportRetentionInDays, ingestionRetentionInMonths, dataExplorerRawRetentionInDays, dataExplorerFinalRetentionInMonths). A deployment that omits one keeps the value stored in config/settings.json; explicit values, including lower ones, are applied. New hubs still get the documented defaults. - Replace the "never change a stored value" guard with the nullable pattern. - Settings script returns the effective raw retention, which Data Explorer applies; remove the softdelete = 0d step that ran on every deploy. - Deploy-FinOpsHub only passes retention parameters that are specified. - Portal keeps visible defaults and only sends values changed from the default, so a portal redeploy doesn't reset an existing hub. Fixes #2206 Fixes #2348 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plies Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
584c2b4 to
da1387e
Compare
|
Hubs + ADX (managed) deployed to |
Roland Krummenacher (RolandKrummenacher)
left a comment
There was a problem hiding this comment.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Hubs + ADX (managed) deployment failed. View logs. |
|
Hubs + ADX (managed) deployed to |
|
Hubs + ADX (managed) deployed to |
|
Test environments cleaned up. |
🛠️ Description
🤖 [AI] Hub redeploys and upgrades no longer reset retention settings. The fix is on the incoming values: retention parameters are optional, so leaving one out keeps the stored setting, and an explicit value always applies.
exportRetentionInDays,ingestionRetentionInMonths,dataExplorerRawRetentionInDays,dataExplorerFinalRetentionInMonths) are nowint?. Omitted = keep the value inconfig/settings.json; explicit = applied, including lower values. New hubs still get 0/13/0/13. Replaces the earlier "never change a stored value" guard.raw.days, and Data Explorer applies that value. Thesoftdelete = 0d"clear" step is gone, so raw tables never drop to 0 during a deploy.Deploy-FinOpsHubonly passes retention params you specify.null, which the template treats as "keep current". The Data Explorer section explains this.config/settings.jsonor runDeploy-FinOpsHubwith the value. Not tested in the portal sandbox: how the portal passes anulloutput (parse('null')).Fixes #2206, #2348
📋 Checklist
🔬 How did you test this change?
📦 Deploy to test?
🙋♀️ Do any of the following that apply?
📑 Did you update
docs/changelog.md?📖 Did you update documentation?
🤖 Generated with Claude Code