Skip to content

fix(synthetics_global_variables): key on (name, type) to resolve 409 conflict#564

Merged
michael-richey merged 1 commit into
mainfrom
fix/synthetics-global-vars-409-composite-key
May 15, 2026
Merged

fix(synthetics_global_variables): key on (name, type) to resolve 409 conflict#564
michael-richey merged 1 commit into
mainfrom
fix/synthetics-global-vars-409-composite-key

Conversation

@michael-richey
Copy link
Copy Markdown
Collaborator

@michael-richey michael-richey commented May 14, 2026

Summary

Resolves intermittent 409 Conflict on `datadog-sync sync` for `synthetics_global_variables`:

409 — "Synthetics variable with same name and type already exists"

Root cause. `datadog_sync/model/synthetics_global_variables.py:36` keyed `_existing_resources_map` by `name` only, but the Datadog API enforces uniqueness on the `(name, type)` tuple. When the destination had two variables sharing a name across different types (e.g. `type=variable` and `type=secret_token`), only one survived the last-write-wins collision in the prefetch map. The source variable matching the other type fell through to `POST` and hit the 409.

Fix. Switch `resource_mapping_key` to a composite lambda `lambda r: f"{r['name']}:{r['type']}"`. The existing prefetch + remap path in `create_resource` then resolves conflicts before any POST is issued — no new try/except, no new API call. This mirrors the pattern `Teams` already uses at `datadog_sync/model/teams.py:34` for its `(name, handle)` uniqueness, with the key difference that Synthetics is v1-flat (lambda reads `r['name']` / `r['type']` directly, not `attributes.name` / `attributes.handle`).

Tests

Six unit tests in `tests/unit/test_synthetics_global_variables.py`:

  1. `map_existing_resources` retains both same-name variants under distinct composite keys (red→green).
  2. Routing assertion — source X/secret_token remaps to the correct destination ID and PUTs, post never called (red→green).
    3a. Composite key resolves to `"X:variable"` on full input (red→green).
    3b. Missing `type` returns `None` silently — guards `base_resource.py:111-115` silent-fail path against future refactors.
  3. Unique-name regression — green/green, no behavior change for non-colliding names.
  4. Partial-collision (the literal production bug shape) — source X/secret_token with destination only having X/variable correctly POSTs.

All 6 pass; full `tests/unit/` suite (576 tests) green; `tox -e ruff,black` clean.

Cassette note

Existing integration test `tests/integration/resources/test_synthetics_global_variable.py` is unaffected: the destination state fixture at `resources/destination/synthetics_global_variables.json` contains a single variable with a unique name, so the new composite key collapses to the same single-entry map as the old name-only key. No cassette re-record required. (The current cassettes have pre-existing playback failures on `main` unrelated to this fix.)

Out of scope

  • No 409 try/except fallback in `create_resource`. Users/roles don't have one; the prefetch path is sufficient. A future PR can layer a 409-race handler if observed in production.
  • Type change between syncs. If a source variable's `type` is manually flipped between syncs, the composite key won't match the cached entry and the code will POST a new variable. Acceptable per current semantics — `type` is part of identity at the API level.

Test plan

  • `tox -e py313 -- tests/unit/test_synthetics_global_variables.py` — 6/6 pass
  • `tox -e py313 -- tests/unit/` — 576/576 pass
  • `tox -e ruff,black` — clean

🤖 Generated with Claude Code

…conflict

The Datadog API enforces uniqueness on the (name, type) tuple — two
global variables can share a name as long as their types differ. Keying
`_existing_resources_map` by `name` alone caused last-write-wins map
collisions: when the destination had two variables with the same name
but different types, only one survived in the prefetch map and the
source matching the other type fell through to POST and hit
"409 Conflict — Synthetics variable with same name and type already
exists".

Switch `resource_mapping_key` to `lambda r: f"{r['name']}:{r['type']}"`,
mirroring the composite-key pattern Teams already uses for (name, handle).
The existing prefetch + remap path in `create_resource` then resolves
conflicts before any POST is issued.

Adds six unit tests in tests/unit/test_synthetics_global_variables.py
covering: map_existing_resources retains both same-name variants,
routing by composite key, key-resolves-to-string + missing-type-returns-None
guards, unique-name regression, and the partial-collision case that
reproduces the production bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@michael-richey michael-richey marked this pull request as ready for review May 14, 2026 19:23
@michael-richey michael-richey requested a review from a team as a code owner May 14, 2026 19:23
@michael-richey michael-richey merged commit 086e368 into main May 15, 2026
21 of 23 checks passed
@michael-richey michael-richey deleted the fix/synthetics-global-vars-409-composite-key branch May 15, 2026 14:38
michael-richey added a commit that referenced this pull request May 15, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@michael-richey michael-richey mentioned this pull request May 15, 2026
michael-richey added a commit that referenced this pull request May 15, 2026
* Update CHANGELOG

* Update CHANGELOG: add #564 to 4.4.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: dd-octo-sts[bot] <200755185+dd-octo-sts[bot]@users.noreply.github.com>
Co-authored-by: michael.richey <michael.richey@datadoghq.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants