Make projection cycle planning scale-safe
This commit is contained in:
parent
f1fabaf0ca
commit
5b43c44dd7
2 changed files with 52 additions and 32 deletions
|
|
@ -24,47 +24,51 @@ def _cycles(node_ids: tuple[str, ...], edges: tuple[Edge, ...]) -> list[list[str
|
|||
"""Return deterministic strongly connected components that represent cycles."""
|
||||
|
||||
adjacency: dict[str, list[str]] = {node_id: [] for node_id in node_ids}
|
||||
reverse_adjacency: dict[str, list[str]] = {node_id: [] for node_id in node_ids}
|
||||
for edge in edges:
|
||||
adjacency[edge.source_id].append(edge.target_id)
|
||||
for targets in adjacency.values():
|
||||
reverse_adjacency[edge.target_id].append(edge.source_id)
|
||||
for targets in (*adjacency.values(), *reverse_adjacency.values()):
|
||||
targets.sort()
|
||||
|
||||
index = 0
|
||||
indexes: dict[str, int] = {}
|
||||
lowlinks: dict[str, int] = {}
|
||||
stack: list[str] = []
|
||||
on_stack: set[str] = set()
|
||||
components: list[list[str]] = []
|
||||
visited: set[str] = set()
|
||||
finished: list[str] = []
|
||||
for node_id in node_ids:
|
||||
if node_id in visited:
|
||||
continue
|
||||
visited.add(node_id)
|
||||
traversal: list[tuple[str, int]] = [(node_id, 0)]
|
||||
while traversal:
|
||||
current, position = traversal[-1]
|
||||
targets = adjacency[current]
|
||||
if position < len(targets):
|
||||
target = targets[position]
|
||||
traversal[-1] = (current, position + 1)
|
||||
if target not in visited:
|
||||
visited.add(target)
|
||||
traversal.append((target, 0))
|
||||
continue
|
||||
finished.append(current)
|
||||
traversal.pop()
|
||||
|
||||
def visit(node_id: str) -> None:
|
||||
nonlocal index
|
||||
indexes[node_id] = index
|
||||
lowlinks[node_id] = index
|
||||
index += 1
|
||||
stack.append(node_id)
|
||||
on_stack.add(node_id)
|
||||
for target_id in adjacency[node_id]:
|
||||
if target_id not in indexes:
|
||||
visit(target_id)
|
||||
lowlinks[node_id] = min(lowlinks[node_id], lowlinks[target_id])
|
||||
elif target_id in on_stack:
|
||||
lowlinks[node_id] = min(lowlinks[node_id], indexes[target_id])
|
||||
if lowlinks[node_id] != indexes[node_id]:
|
||||
return
|
||||
assigned: set[str] = set()
|
||||
components: list[list[str]] = []
|
||||
for node_id in reversed(finished):
|
||||
if node_id in assigned:
|
||||
continue
|
||||
assigned.add(node_id)
|
||||
component: list[str] = []
|
||||
while stack:
|
||||
member = stack.pop()
|
||||
on_stack.remove(member)
|
||||
component.append(member)
|
||||
if member == node_id:
|
||||
break
|
||||
component_stack = [node_id]
|
||||
while component_stack:
|
||||
current = component_stack.pop()
|
||||
component.append(current)
|
||||
for target in reversed(reverse_adjacency[current]):
|
||||
if target not in assigned:
|
||||
assigned.add(target)
|
||||
component_stack.append(target)
|
||||
component.sort()
|
||||
if len(component) > 1 or component[0] in adjacency[component[0]]:
|
||||
components.append(component)
|
||||
|
||||
for node_id in node_ids:
|
||||
if node_id not in indexes:
|
||||
visit(node_id)
|
||||
return sorted(components)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from unittest import mock
|
|||
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.manual_projection import (
|
||||
_cycles,
|
||||
build_manual_projection_package,
|
||||
build_manual_render_plan,
|
||||
)
|
||||
|
|
@ -390,6 +391,21 @@ class ProjectionContractTests(unittest.TestCase):
|
|||
workflow["cross_references"],
|
||||
)
|
||||
|
||||
def test_cycle_detection_handles_the_maximum_deep_graph_iteratively(self) -> None:
|
||||
node_ids = tuple(f"node.{index:05d}" for index in range(10_000))
|
||||
chain = tuple(
|
||||
Edge(node_ids[index], "depends_on", node_ids[index + 1])
|
||||
for index in range(len(node_ids) - 1)
|
||||
)
|
||||
self.assertEqual([], _cycles(node_ids, chain))
|
||||
self.assertEqual(
|
||||
[list(node_ids)],
|
||||
_cycles(
|
||||
node_ids,
|
||||
(*chain, Edge(node_ids[-1], "depends_on", node_ids[0])),
|
||||
),
|
||||
)
|
||||
|
||||
def test_alpha_compatibility_shim_preserves_legacy_identity_and_bytes(self) -> None:
|
||||
renderer = GenericHtmlRenderer()
|
||||
self.assertEqual(ALPHA_RENDERER_VERSION, renderer.renderer_version)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue