Skip to content

fix: clear NotFoundError on tag-cache miss instead of (DELETED) sentinel (BLDX-1530)#971

Merged
Aryamanz29 merged 3 commits into
mainfrom
aryaman/bldx-1530
Jul 13, 2026
Merged

fix: clear NotFoundError on tag-cache miss instead of (DELETED) sentinel (BLDX-1530)#971
Aryamanz29 merged 3 commits into
mainfrom
aryaman/bldx-1530

Conversation

@Aryamanz29

Copy link
Copy Markdown
Member

What & why

Fixes BLDX-1530 (Multiplex Sev1, ZD#126376). When a tag name can't be resolved in the local tag cache, AtlanTagRetranslator substituted the literal (DELETED) sentinel into the request payload and sent it to Atlas, which rejected it with an opaque:

ATLAS-404-00-008: Given classification (DELETED) was invalid.

That names neither the tag nor the cause, and misleads customers into thinking a tag was deleted when it may simply never have existed — the repro is add_atlan_tags(["Alert: DQ"]) on a tenant without that tag.

The fix

On the asset-tag write path (classification objects: classifications / addOrUpdateClassifications / removeClassifications), an unresolvable tag name now raises a clear client-side error naming the tag:

NotFoundError: ATLAN-PYTHON-404-006 Atlan tag with name Alert: DQ does not exist.

Two behaviors are deliberately preserved:

  • (DELETED) sentinel round-trip — an asset read with an already-deleted tag surfaces (DELETED); re-serializing it must stay lossless, so a name that is the sentinel is preserved (not raised on).
  • purpose/persona policy (names) pathclassificationNames / purposeClassifications stay tolerant (they round-trip deleted references through the sentinel); the strict check applies only to the asset-tag write path where the customer-facing bug occurs.

Fixed in both the sync (retranslators.py) and async (aio/retranslators.py) retranslators.

Tests

  • Unit (tests/unit/test_retranslators.py + aio/): resolve existing tag → id; missing tag → named NotFoundError (no sentinel leak); removeClassifications missing → raises; (DELETED) sentinel round-trip preserved; names/policy path stays tolerant.
  • Integration (tests/integration/test_client.py::test_add_nonexistent_classification_raises_named_error): add_atlan_tags with a non-existent tag raises the named error end-to-end.
  • Full unit suite: 6992 passed, no regression.

Related: CSA-491 (the customer package crash), DOC-815 (same opaque symptom for playbooks).

Closes BLDX-1530.

… sentinel (BLDX-1530)

When a tag name couldn't be resolved in the local cache, AtlanTagRetranslator
substituted the literal `(DELETED)` sentinel into the request and sent it to
Atlas, which rejected it with an opaque `ATLAS-404-00-008: Given classification
(DELETED) was invalid` — naming neither the tag nor the cause, and misleading
customers into thinking a tag was deleted when it may never have existed
(Multiplex Sev1, ZD#126376: add_atlan_tags(["Alert: DQ"]) on a tenant without
that tag).

On the asset-tag write path (classification objects), an unresolvable name now
raises NotFoundError naming the tag ("Atlan tag with name 'Alert: DQ' does not
exist"). The `(DELETED)` sentinel itself is preserved so an asset read with an
already-deleted tag still round-trips losslessly; the purpose/persona policy
(names) path stays tolerant for the same round-trip reason. Fixed in both the
sync and async retranslators.

Adds unit tests (sync + async: resolve / raise-on-missing / sentinel round-trip)
and an integration test (add_atlan_tags with a non-existent tag raises the named
error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear

linear Bot commented Jul 13, 2026

Copy link
Copy Markdown

BLDX-1530

Aryamanz29 and others added 2 commits July 13, 2026 16:07
…e); consolidate tests

Refine BLDX-1530: get_id_for_name returns None for both a soft-deleted tag and a
name that never existed, but the cache tracks deleted names separately
(deleted_names). Use it so the retranslator only RAISES for a name that never
existed; a known-deleted tag (or the (DELETED) sentinel from a read round-trip)
keeps the sentinel — lossless, since the tag really existed. Applied to both the
sync and async retranslators.

Move the retranslator unit tests into tests/unit/test_atlan_tag_name.py (+ aio),
alongside the existing tag-serde tests, and drop the standalone test_retranslators
files. Adds the deleted-tag-keeps-sentinel case so both behaviours are covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g-cache-miss raise (BLDX-1530)

Live testing on a real tenant exposed that the deleted_names-based
distinction never worked: AtlanTagCacheCommon.get_id_for_name_after_refresh
adds ANY unresolved name to deleted_names on a refresh miss ("could be an
audit-log entry"), so a never-existed name is in deleted_names by the time
the retranslator checks it. The guard therefore never raised, and
add_atlan_tags(["<missing>"]) still shipped the (DELETED) sentinel to Atlas,
reproducing the opaque "ATLAS-404-00-008: Given classification (DELETED)".

The cache genuinely cannot tell a soft-deleted tag from a never-existed one
by name, so the only reliable "genuinely deleted" signal is the (DELETED)
sentinel *string* itself, produced by a read of a deleted tag. Gate on that:
preserve the sentinel (lossless round-trip); raise a clear, named
NotFoundError (ATLAN-PYTHON-404-006) for any other unresolvable name.

Verified live on all three cases: never-existed -> named 404-006 raise;
active add -> succeeds; deleted-tag read -> (DELETED) preserved. Unit test
now pins that deleted_names membership does NOT suppress the raise (regression
guard); integration test asserts the specific 404-006 message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Aryamanz29

Copy link
Copy Markdown
Member Author

Update: live testing changed the approach

Validated the earlier deleted_names-based distinction on a real tenant (kill-argo) and found it never worked, so I've corrected it.

Root cause of the miss: AtlanTagCacheCommon.get_id_for_name_after_refresh adds any unresolved name to deleted_names on a refresh miss (comment: "could be an audit-log entry"). Confirmed live — after one lookup, a never-existed name is already in deleted_names. So deleted_names conflates soft-deleted with never-existed; gating the raise on it meant the guard never fired, and add_atlan_tags(["<missing>"]) still shipped (DELETED) to Atlas → the original opaque ATLAS-404-00-008: Given classification (DELETED) was invalid.

Fix: the cache can't tell the two apart by name, so the only reliable "genuinely deleted" signal is the (DELETED) sentinel string itself (produced by a read of a deleted tag). Gate on that: preserve the sentinel (lossless round-trip), raise a clear named NotFoundError (ATLAN-PYTHON-404-006) for anything else unresolvable.

Live validation (all three cases, on a real tenant):

Case Result
add_atlan_tags(["NonExistent-…"]) ATLAN-PYTHON-404-006 Atlan tag with name NonExistent-… does not exist (named, no sentinel leak)
add_atlan_tags(["<active tag>"]) ✅ succeeds
read asset whose tag was deleted (DELETED) sentinel preserved

Unit test now pins that deleted_names membership does not suppress the raise (regression guard for this exact bug); integration test asserts the specific 404-006 message and absence of (DELETED).

@Aryamanz29 Aryamanz29 self-assigned this Jul 13, 2026
@Aryamanz29 Aryamanz29 added the bugfix Bug fix pull request label Jul 13, 2026
@Aryamanz29 Aryamanz29 merged commit cd13857 into main Jul 13, 2026
74 of 91 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant