Skip to content

fix: reconcile state.destination on skip-without-write outcomes - #705

Merged
michael-richey merged 4 commits into
mainfrom
michael.richey/skip-reconcile-destination-state
Sep 23, 2026
Merged

michael-richey merged 4 commits into
mainfrom
michael.richey/skip-reconcile-destination-state

Conversation

@michael-richey

@michael-richey michael-richey commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Problem

state.destination (loaded from the storage bucket at run start) is the view downstream consumers trust to know "what exists on the destination org." A second, live-API view — _existing_resources_map — is used by some create_resource/update_resource methods to skip a write when a resource already exists on the destination.

When such a method raises SkipResource without writing state.destination, the two views diverge: no destination state file is persisted, so a later bucket-presence check counts the id as failed even though the resource is confirmed present via the live API. The reported sync percentage is lower than the true sync state.

Solution

In base_resource.py, the _create_resource/_update_resource wrappers now catch SkipResource and, if the resource's mapping key is present in _existing_resources_map, write the discovered destination resource into state.destination (insert-if-absent only) before re-raising. The original SkipResource re-raises, so handler accounting (counter, metrics, emit) is unchanged.

One framework change, no per-resource edits — the bug is fixed generically and future resources are protected automatically.

  • Delegate-then-update resources (users, teams, logs_metrics, logs_indexes, metric_tag_configurations, synthetics_global_variables, synthetics_tests) never raise SkipResource in the exists-path, so the framework is a pure no-op for them.
  • Opt-out resources have an empty _existing_resources_map, so nothing is ever written.
  • Genuine non-existence skips (key absent from the map) write nothing.
  • Insert-if-absent never overwrites a pre-existing state.destination entry.

Tests

11 new test files (~48 cases): framework contract and boundaries, the primary team_memberships bug fix, per-resource regression guards, no-false-positive guards, and framework safety-net proofs for all 10 mapping resources. Full unit suite: 1360 passed.

When create_resource/update_resource discovers (via _existing_resources_map)
that a resource already exists on the destination and raises SkipResource
without writing state.destination, the bucket view diverges from destination
truth: no state file is persisted, so downstream consumers that trust the
bucket count the id as failed even though the resource is confirmed present
via the live API.

Add a best-effort _reconcile_destination_if_absent helper invoked by the
_create_resource/_update_resource wrappers on SkipResource: look up the
resource's mapping key in _existing_resources_map and, if found, write the
discovered destination resource into state.destination (insert-if-absent only).
The original SkipResource re-raises so the handler's counter/metrics/emit
accounting is unchanged.

Insert-if-absent preserves delegate-then-update resources that already write
state.destination before delegating to update_resource (users, teams,
logs_metrics, logs_indexes, metric_tag_configurations,
synthetics_global_variables, synthetics_tests) -- the framework is a pure
no-op for them. Opt-out resources (skip_resource_mapping=True) have an empty
_existing_resources_map, so the lookup never matches and nothing is written.
Genuine non-existence skips (key absent from the map) write nothing.

This fixes the class of bug at its source (the wrappers) with no per-resource
edits, protecting future resources automatically. Covered by 11 new test
files: framework contract + boundaries, plus per-resource regression guards,
no-false-positive guards, and framework safety-net proofs.
@michael-richey
michael-richey requested a review from a team as a code owner September 23, 2026 16:40
Comment thread datadog_sync/utils/base_resource.py Outdated

@michael-richey michael-richey left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thorough pass complete on this PR.

What I checked:

  • Framework behavior change in datadog_sync/utils/base_resource.py for skip-path reconciliation.
  • New per-resource regression tests and boundary tests.
  • Skip accounting path in resources_handler to confirm the original SkipResource still re-raises and existing skipped metrics/counters remain intact.

Validation run:
tox -e py311 -- tests/unit/test_skip_reconcile_framework.py tests/unit/test_team_memberships_skip_reconcile.py tests/unit/test_roles_skip_reconcile.py tests/unit/test_security_monitoring_skip_reconcile.py tests/unit/test_users_skip_reconcile.py tests/unit/test_teams_skip_reconcile.py tests/unit/test_synthetics_global_variables_skip_reconcile.py tests/unit/test_synthetics_tests_skip_reconcile.py tests/unit/test_logs_indexes_skip_reconcile.py tests/unit/test_logs_metrics_skip_reconcile.py tests/unit/test_metric_tag_configurations_skip_reconcile.py

Result: all 47 tests passed.

I left one non-blocking inline suggestion on the reconcile check to use key is not None (instead of truthiness) so it stays fully consistent with map_existing_resources() behavior for empty-string keys.

map_existing_resources() keeps entries when key is not None, so an empty-string
key is mappable during discovery. The reconcile helper gated on truthiness
('if key and ...'), which would never reconcile an empty-string key that IS in
the map -- inconsistent with the discovery path. Switch to 'key is not None'
to match map_existing_resources() exactly.

Addresses review comment on PR #705.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Pre-action skips can still leave destination state unreconciled, preserving the false-negative behavior.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds framework-level reconciliation so skipped resources already present at the destination are persisted in destination state.

Changes:

  • Reconciles destination state in create/update skip paths.
  • Preserves existing state and skip accounting.
  • Adds framework and resource-specific regression tests.
File Review
tests/​unit/​test_users_skip_reconcile.py Covers user paths and import-skip boundaries.
tests/​unit/​test_teams_skip_reconcile.py Covers team reconciliation behavior.
tests/​unit/​test_team_memberships_skip_reconcile.py Covers membership skip reconciliation.
tests/​unit/​test_synthetics_tests_skip_reconcile.py Covers synthetic test state handling.
tests/​unit/​test_synthetics_global_variables_skip_reconcile.py Covers global-variable state handling.
tests/​unit/​test_skip_reconcile_framework.py Tests framework contracts; contains an unused mock setup that should be removed.
tests/​unit/​test_security_monitoring_skip_reconcile.py Covers security-rule skip cases and current boundaries.
tests/​unit/​test_roles_skip_reconcile.py Covers role skip paths.
tests/​unit/​test_metric_tag_configurations_skip_reconcile.py Covers metric configuration skip cases.
tests/​unit/​test_logs_metrics_skip_reconcile.py Covers logs metric behavior.
tests/​unit/​test_logs_indexes_skip_reconcile.py Covers logs index behavior.
datadog_sync/​utils/​base_resource.py Adds reconciliation, but misses pre-action SkipResource paths; debug formatting also needs correction.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread datadog_sync/utils/base_resource.py
Comment thread datadog_sync/utils/base_resource.py
michael-richey and others added 2 commits September 23, 2026 13:43
Address two review comments on PR #705:

1. Pre-hook SkipResource (e.g. security_monitoring_rules immutable/deprecated
   rules whose matching destination rule is in _existing_resources_map) was
   raised in _pre_resource_action_hook before the _create_resource/
   _update_resource wrappers, so the framework never reconciled it -- the same
   bucket-view false negative. Wrap the pre-hook call in _apply_resource_cb with
   try/except SkipResource that calls _reconcile_destination_if_absent before
   re-raising. Insert-if-absent makes this idempotent with the wrapper reconcile.

2. The reconcile debug diagnostic passed positional %s args to logger.debug,
   but the NDJSON log backend (utils/log.py Log.debug) does not interpolate
   positional args in JSON mode, so %s placeholders would be emitted literally.
   Pre-format with an f-string so the message is readable in both modes.

Tests: add pre-hook immutable/deprecated matching-map regression cases (RED
without the handler wrap, GREEN with it) and a pre-formatted-log assertion.
Full unit suite: 1363 passed.
The test_handler_accounting_unchanged_after_framework_reconcile case built
up a bare MagicMock 'r_class' with stubs (resource_config, connect_resources,
_pre_resource_action_hook, _send_action_metrics, _create_resource,
_existing_resources_map) but never used it -- the test drives a real instance
through _make_instance so the framework wrapper actually runs. Drop the dead
block (flagged by Copilot review) and the now-unused AsyncMock import.
@michael-richey
michael-richey force-pushed the michael.richey/skip-reconcile-destination-state branch from 21cf7e6 to 4bfa586 Compare September 23, 2026 19:00
@michael-richey
michael-richey merged commit 617831e into main Sep 23, 2026
10 of 11 checks passed
@michael-richey
michael-richey deleted the michael.richey/skip-reconcile-destination-state branch September 23, 2026 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants