Skip to content

[Alerting V2] [ResponseOps] Recovery delay is configurable and persisted even when it has no effect on execution - #288802

Merged
doakalexi merged 18 commits into
elastic:mainfrom
doakalexi:alerting-v2/update-validation-for-recovery-delay
Sep 11, 2026
Merged

[Alerting V2] [ResponseOps] Recovery delay is configurable and persisted even when it has no effect on execution#288802
doakalexi merged 18 commits into
elastic:mainfrom
doakalexi:alerting-v2/update-validation-for-recovery-delay

Conversation

@doakalexi

@doakalexi doakalexi commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Resolves #279941

Summary

Recovery delay (state_transition.recovering_count / state_transition.recovering_timeframe) only ever does real work when a recovery strategy is enabled. This PR updates the schema validation on create/update to not allow creating a rule with state_transition.recovering_count > 0 / state_transition.recovering_timeframe when the recovery_strategy is null or undefined.

Note: the validation does allow recovering_count: 0 it is not a delay and the episode will recover immediately. I'm happy to change this if that is confusing.

Checklist

To verify

  1. Start Kibana and enable alerting V2
  2. Go to dev tools and run the following:

Verifyrecovery_strategy: "none" + recovering_count > 0 fails

POST kbn:/api/alerting/v2/rules
{
  "kind": "alert",
  "metadata": { "name": "verify-inert-a1" },
  "schedule": { "every": "5m" },
  "query": { "format": "standalone", "breach": { "query": "FROM logs-* | LIMIT 1" } },
  "recovery_strategy": "none",
  "state_transition": { "pending_count": 0, "recovering_count": 3 }
}

Verify recovery_strategy unset + recovering_timeframe fails

POST kbn:/api/alerting/v2/rules
{
  "kind": "alert",
  "metadata": { "name": "verify-inert-a2" },
  "schedule": { "every": "5m" },
  "query": { "format": "standalone", "breach": { "query": "FROM logs-* | LIMIT 1" } },
  "state_transition": { "recovering_timeframe": "5m" }
}

Verify it still fails with no_data_strategy: "recover"

POST kbn:/api/alerting/v2/rules
{
  "kind": "alert",
  "metadata": { "name": "verify-inert-a3" },
  "schedule": { "every": "5m" },
  "query": {
    "format": "standalone",
    "breach": { "query": "FROM logs-* | LIMIT 1" },
    "no_data": { "query": "FROM logs-* | STATS c = COUNT(*)" }
  },
  "recovery_strategy": "none",
  "no_data_strategy": "recover",
  "state_transition": { "recovering_count": 2 }
}

Verify recovering_count: 0 is allowed even when recovery is disabled

POST kbn:/api/alerting/v2/rules
{
  "kind": "alert",
  "metadata": { "name": "verify-ok-b1" },
  "schedule": { "every": "5m" },
  "query": { "format": "standalone", "breach": { "query": "FROM logs-* | LIMIT 1" } },
  "recovery_strategy": "none",
  "state_transition": { "pending_count": 0, "recovering_count": 0 }
}

Verify pending-only state_transition is allowed while recovery is disabled

POST kbn:/api/alerting/v2/rules
{
  "kind": "alert",
  "metadata": { "name": "verify-ok-b2" },
  "schedule": { "every": "5m" },
  "query": { "format": "standalone", "breach": { "query": "FROM logs-* | LIMIT 1" } },
  "recovery_strategy": "none",
  "state_transition": { "pending_count": 3 }
}

Verify a recovery delay is allowed with recovery enabled

Keep the id from the response for the next section

POST kbn:/api/alerting/v2/rules
{
  "kind": "alert",
  "metadata": { "name": "verify-ok-b3" },
  "schedule": { "every": "5m" },
  "query": { "format": "standalone", "breach": { "query": "FROM logs-* | LIMIT 1" } },
  "recovery_strategy": "no_breach",
  "state_transition": { "pending_count": 0, "recovering_count": 3, "recovering_timeframe": "5m" }
}

Update the rule above to disable recovery without clearing the delay, expect 400

PATCH kbn:/api/alerting/v2/rules/<B3_RULE_ID>
{
  "recovery_strategy": "none"
}

Update the rule to disable recovery with also clearing the delay, expect 200

PATCH kbn:/api/alerting/v2/rules/<B3_RULE_ID>
{
  "recovery_strategy": "none",
  "state_transition": { "pending_count": 0 }
}

@doakalexi doakalexi changed the title Updating validation for recovery delay [Alerting V2] [ResponseOps] Recovery delay is configurable and persisted even when it has no effect on execution Sep 2, 2026
@doakalexi doakalexi added release_note:skip Skip the PR/issue when compiling release notes Team:ResponseOps Platform ResponseOps team (formerly the Cases and Alerting teams) t// backport:version Backport to applied version labels v9.6.0 labels Sep 3, 2026
@doakalexi
doakalexi marked this pull request as ready for review September 3, 2026 16:31
@doakalexi
doakalexi requested a review from a team as a code owner September 3, 2026 16:31
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

Pinging @elastic/response-ops (Team:ResponseOps)

@kibanamachine kibanamachine added the reviewer:scout Agentic PR Scout test review label Sep 3, 2026

@baileycash-elastic baileycash-elastic left a comment

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.

looks good, one rec on the naming for the isRecoveryDelayAllowed naming. I realized we'll have UI work to do so I'll raise a separate ticket and race you! :D

expect(result.success).toBe(false);
});

it('accepts recovering_count of 0 when recovery is disabled', () => {

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.

iiiinteresting

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.

I can change this if needed, it is kind of weird

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.

I was trying to minimize the disruption from adding the new validation

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.

might create some drift since I plan to hide the ui field entirely, we'll see. I want to avoid over-engineering the mapper if possible

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.

Okay yeah that makes sense, I can remove 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.

Removed in this commit, 00f662b

});

it('returns true when recovery is disabled but no recovering delay is set', () => {
expect(isRecoveryDelayAllowed({ recovery_strategy: 'none' })).toBe(true);

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.

isRecoveryDelayAllowed feels slightly misleading in this case

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.

Updated in this commit, 00f662b

@baileycash-elastic
baileycash-elastic dismissed their stale review September 4, 2026 21:31

So sorry, backend changes look good... a few naming conventions I thought were a little weird.

The UI changes are still needed, best to do here to avoid any problems. Thanks for making these changes!

@doakalexi

Copy link
Copy Markdown
Contributor Author

@baileycash-elastic I added UI changes in this commit, 9b736cc

@github-actions github-actions Bot left a comment

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.

One compatibility concern on the update path: the new isRecoveryTransitionConsistentWithStrategy invariant runs on every update against merged attributes, which can make pre-existing rules that carry an inert recovering delay un-updatable. Details inline. The rest of the change (schema refinement, form/compose mappers gating on isRecoveryEnabled, agent-builder op check, error code, and the accompanying tests) looks consistent and well-covered.

Generated by Claude Reviewer for #288802 · claude · opus · 368.7 AIC · ⌖ 22.6 AIC · ⊞ 5.5K

details: { rule_id: ruleId },
},
{
valid: isRecoveryTransitionConsistentWithStrategy(attrs),

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.

This invariant runs on every updateRule call (validateMergedRuleAttributes(id, nextAttrs) is unconditional), and nextAttrs preserves the stored state_transition/recovery_strategy when the update body doesn't touch them. That makes it a backward-compatibility concern for rules already in storage.

Before this PR, createRuleDataSchema had no recovering-vs-strategy refinement, and the old form mapper emitted recovering_count: 0 unconditionally for immediate recovery mode (see the assertions removed in rule_request_mappers.test.ts). So a rule with recovery_strategy: 'none'/unset + state_transition.recovering_count: 0 (or a positive value) was createable via both the API and the UI and can exist on disk today.

After this change, any such pre-existing rule becomes un-updatable: even an unrelated PATCH (e.g. renaming) merges to the same inert recovering delay and now throws INVALID_STATE_TRANSITION_CONFIG (400), forcing the user to clear the recovering delay in the same request before any other edit can go through.

If alerting_v2 already has persisted rules in this shape, consider normalizing the inert recovering fields out of nextAttrs on update (drop them when recovery is disabled) rather than rejecting, or adding a saved-object migration to strip them — so existing rules stay editable. If there's no such data yet (pre-GA, no migration needed), it'd be worth confirming that explicitly.

@doakalexi doakalexi Sep 9, 2026

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.

This is okay because we are experimental.

@baileycash-elastic baileycash-elastic left a comment

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.

lgtm, proposed a few areas to 🧹 clean

tysm for implementing this!


<EuiSpacer size="m" />
<RecoveryDelayField />
{/*

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: self-explanatory, comment not needed

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.

removed in this commit, e95a043

if (stateTransition?.recoveringTimeframe != null)
out.recovering_timeframe = stateTransition.recoveringTimeframe;
if (stateTransition?.recoveringCount != null)
// Recovering thresholds are only meaningful when recovery is enabled; emitting them

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: self explanatory

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.

removed in this commit, e95a043

Comment on lines +53 to +60
/**
* Recovery is enabled only for condition-based strategies (`no_breach` / `query`).
* `none`/unset means condition-based recovery never fires, so the recovering delay
* thresholds (`recovering_count` / `recovering_timeframe`) are inert and must not be
* emitted — the write API rejects them (see `isRecoveryTransitionConsistentWithStrategy`).
* `no_data_strategy: 'recover'` does NOT re-enable them: the director bypasses recovering
* gating for no-data recovery (see `count_timeframe_strategy`).
*/

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.

probably not needed

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.

removed in this commit, e95a043

Comment thread x-pack/platform/plugins/shared/alerting_v2/server/lib/errors/error_codes.ts Outdated
doakalexi and others added 4 commits September 11, 2026 11:40
@doakalexi
doakalexi enabled auto-merge (squash) September 11, 2026 18:46
@kibanamachine

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
shared-packages 4.6MB 4.6MB +496.0B
Unknown metric groups

total optimizer output size

id before after diff
all 64.0MB 64.0MB +496.0B

warm start memory

id before after diff
post forced gc heap baseline - 835791304 +835791304
post forced gc heap delta - -1943612 -1943612
post forced gc heap delta standard deviation - 2801204 +2801204
post forced gc heap target - 833847692 +833847692
tail heap delta - -31759699 -31759699
total +1638736889

History

@doakalexi
doakalexi merged commit 5395241 into elastic:main Sep 11, 2026
41 checks passed
@kibanamachine kibanamachine added backport:skip This PR does not require backporting and removed backport:version Backport to applied version labels labels Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes reviewer:scout Agentic PR Scout test review Team:ResponseOps Platform ResponseOps team (formerly the Cases and Alerting teams) t// v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Alerting v2] Recovery delay is configurable and persisted even when it has no effect on execution

3 participants