Repository navigation
fix(tracking): delete unused members before the nodes they hold - #1487
FragmentedPacket wants to merge 2 commits into
Conversation
The tracking cleanup deleted unused group members in the order the group returns them, which is creation order. A node created first was deleted before the unused node that holds it through a mandatory relationship, so the server refused the delete and the run failed with TrackingGroupCleanupError. Order the deletes from the cached schema, and from the members' peers for kinds that hold nodes of their own kind. If the order cannot be worked out, nothing is deleted and the failure stops the cleanup, keeping every member in the group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## infrahub-release-1.12 #1487 +/- ##
=========================================================
+ Coverage 87.74% 87.93% +0.18%
=========================================================
Files 155 157 +2
Lines 15539 15738 +199
Branches 2091 2123 +32
=========================================================
+ Hits 13635 13839 +204
+ Misses 1344 1339 -5
Partials 560 560
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
2 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="infrahub_sdk/query_groups.py">
<violation number="1" location="infrahub_sdk/query_groups.py:282">
P2: `order()` places members whose kinds are missing from the schema last even though their dependency direction is unknown. Treat any unknown kind as an ordering failure before issuing deletes so the reap keeps all candidates.</violation>
<violation number="2" location="infrahub_sdk/query_groups.py:299">
P2: When the peer graph within a kind is cyclic, `_order_block` falls back to group order and deletes anyway. The PR contract says undeterminable ordering should delete nothing and raise. Raise an error here instead, so `delete_unused` records it and stops the reap.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| if ordered_ids is None: | ||
| return members |
There was a problem hiding this comment.
P2: When the peer graph within a kind is cyclic, _order_block falls back to group order and deletes anyway. The PR contract says undeterminable ordering should delete nothing and raise. Raise an error here instead, so delete_unused records it and stops the reap.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At infrahub_sdk/query_groups.py, line 299:
<comment>When the peer graph within a kind is cyclic, `_order_block` falls back to group order and deletes anyway. The PR contract says undeterminable ordering should delete nothing and raise. Raise an error here instead, so `delete_unused` records it and stops the reap.</comment>
<file context>
@@ -120,6 +121,207 @@ def failure(self) -> Exception | None:
+ # A cycle of mandatory relationships between nodes cannot be stored, but keep the
+ # group order rather than fail if the peers read back describe one.
+ ordered_ids = _dependents_first(dependencies=dependencies, order=member_ids)
+ if ordered_ids is None:
+ return members
+ kind_of = {member_id: kind for kind, member_id in members}
</file context>
| if ordered_ids is None: | |
| return members | |
| if ordered_ids is None: | |
| raise DependencyCycleExistsError("Mandatory peers of unused members form a cycle; cannot order deletion") |
| ordered: list[tuple[str, str]] = [] | ||
| for key in block_order: | ||
| ordered.extend(self._order_block(block=blocks[key], member_peers=member_peers)) | ||
| ordered.extend(candidate for candidate in self.candidates if candidate[0] in self.unknown_kinds) |
There was a problem hiding this comment.
P2: order() places members whose kinds are missing from the schema last even though their dependency direction is unknown. Treat any unknown kind as an ordering failure before issuing deletes so the reap keeps all candidates.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At infrahub_sdk/query_groups.py, line 282:
<comment>`order()` places members whose kinds are missing from the schema last even though their dependency direction is unknown. Treat any unknown kind as an ordering failure before issuing deletes so the reap keeps all candidates.</comment>
<file context>
@@ -120,6 +121,207 @@ def failure(self) -> Exception | None:
+ ordered: list[tuple[str, str]] = []
+ for key in block_order:
+ ordered.extend(self._order_block(block=blocks[key], member_peers=member_peers))
+ ordered.extend(candidate for candidate in self.candidates if candidate[0] in self.unknown_kinds)
+ return ordered
+
</file context>
| ordered.extend(candidate for candidate in self.candidates if candidate[0] in self.unknown_kinds) | |
| if self.unknown_kinds: | |
| raise ValueError(f"Unable to order members with unknown kinds: {sorted(self.unknown_kinds)}") |
Add tests for a mandatory peer that is not being deleted, peers that form a cycle, and reading peers of both cardinalities. Remove a check in DeletionOrder.peer_lookups that could never be false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying infrahub-sdk-python with
|
| Latest commit: |
694c687
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://60e13a81.infrahub-sdk-python.pages.dev |
| Branch Preview URL: | https://ai-bug-pipeline-1477-trackin.infrahub-sdk-python.pages.dev |
Why
A generator, or any tracking run with
delete_unused_nodes=True, creates nodes in dependency order: the node that is held first, then the node that holds it through a mandatory relationship. For example, two IP addresses and then a BGP session whose mandatorysrc_ipanddst_ippoint at them. When a later run stops building that session, the cleanup deleted the unused members in the order the group returns them, which is creation order. The server refused to delete the IP addresses while the session still held them, so the run failed withTrackingGroupCleanupError, and only the next run deleted them.Goal: one run deletes every unused node, including unused nodes that hold each other through mandatory relationships, without sending a delete that fails only because of the order.
Non-goals:
developandstable. They do not have the refusal handling from fix(tracking): reconcile tracking groups on runs that save no nodes #1278, and this PR depends on it.Closes #1477
What changed
Behavioral changes:
TrackingGroupCleanupError.Implementation notes:
DeletionOrderininfrahub_sdk/query_groups.pybuilds a dependency graph between the candidate kinds from their relationships withoptional: false. A peer that is a generic matches every candidate kind that inherits from it. The kinds are sorted with the existingtopological_sort().client.schema.all(), which returns the cached branch schema. The cache is already filled by the group lookup, so the order between kinds costs no request.schema.get()is not used per kind, because it downloads the whole branch schema again for every kind that is not in the cache.filters(..., only=[mandatory relationships])query per kind reads the peers, and the nodes are sorted by them. In every other case no query is sent.SchemaNotFoundErrorand is recorded as a refusal, as before._record_failure()andReapResultare unchanged. They work on the ordered list.What stayed the same:
delete_unused()andupdate_group()keep their signatures.How to review
DeletionOrderand the helpers above it ininfrahub_sdk/query_groups.py: the graph, the handling of cycles and the order inside a kind._order_for_deletion()anddelete_unused()(async and sync): the ordering failure path that keeps every member.tests/unit/sdk/test_group_context.pyandtests/integration/test_tracking_zero_members.py.Worth extra scrutiny: whether every relationship with
optional: falseis one the server enforces when it deletes the peer.How to test
The new integration test
test_unused_owner_and_animal_are_deleted_in_one_run(async and sync) creates a person and then a cat that holds them through the mandatoryownerrelationship, then runs an empty tracking session. With the fix, both are deleted in one run. With the ordering switched off, it fails with the error from the issue: "Cannot delete TestingPerson … It is linked to mandatory relationship owner on node TestingCat".Impact & rollout
query_groups.pyruns inside Infrahub generators. Running Infrahub's CI against this branch before merging is worth considering.Checklist
changelog/1477.fixed.md)🤖 Generated with Claude Code