From d111157573a03e4710eda284ad2fd1d14fd8ee53 Mon Sep 17 00:00:00 2001 From: CoreRasurae Date: Mon, 16 Feb 2026 20:46:13 +0000 Subject: [PATCH 1/2] fix(bench): use unique names per iteration to avoid UNIQUE constraint failures --- benchmarks/resource_registry_migration_bench.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/benchmarks/resource_registry_migration_bench.py b/benchmarks/resource_registry_migration_bench.py index 38cbf41fd..146d2a835 100644 --- a/benchmarks/resource_registry_migration_bench.py +++ b/benchmarks/resource_registry_migration_bench.py @@ -48,14 +48,16 @@ class ResourceTypeInsert: self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(self.engine) self.session_factory = sessionmaker(bind=self.engine) + self._iter = 0 def teardown(self) -> None: self.engine.dispose() def time_insert_single_resource_type(self) -> None: + self._iter += 1 session = self.session_factory() rt = ResourceTypeModel() - rt.name = "bench/type-single" + rt.name = f"bench/type-single-{self._iter}" rt.namespace = "bench" rt.resource_kind = "physical" rt.user_addable = True @@ -66,10 +68,12 @@ class ResourceTypeInsert: session.close() def time_insert_100_resource_types(self) -> None: + self._iter += 1 + batch = self._iter session = self.session_factory() for i in range(100): rt = ResourceTypeModel() - rt.name = f"bench/type-{i}" + rt.name = f"bench/type-{batch}-{i}" rt.namespace = "bench" rt.resource_kind = "physical" if i % 2 == 0 else "virtual" rt.user_addable = i % 3 == 0 @@ -99,16 +103,20 @@ class ResourceInsert: session.add(rt) session.commit() session.close() + self._iter = 0 def teardown(self) -> None: self.engine.dispose() def time_insert_100_resources(self) -> None: + self._iter += 1 + batch = self._iter session = self.session_factory() for i in range(100): + idx = batch * 1000 + i r = ResourceModel() - r.resource_id = _make_ulid(i) - r.namespaced_name = f"bench/resource-{i}" + r.resource_id = _make_ulid(idx) + r.namespaced_name = f"bench/resource-{batch}-{i}" r.namespace = "bench" r.type_name = "bench/git-checkout" r.resource_kind = "physical" From 946933139ca11441b12d204d3d972fd1174567ce Mon Sep 17 00:00:00 2001 From: CoreRasurae Date: Mon, 16 Feb 2026 23:13:08 +0000 Subject: [PATCH 2/2] fix(bench): Benchmark tests database issues on concurrent test calls --- benchmarks/cli_format_bench.py | 8 ++--- benchmarks/plan_model_bench.py | 2 +- benchmarks/plan_phase_migration_bench.py | 31 ++++++++++++++++---- benchmarks/project_migration_bench.py | 37 +++++++++++++++++++++--- benchmarks/uow_lifecycle_bench.py | 19 ++++++++++-- 5 files changed, 79 insertions(+), 18 deletions(-) diff --git a/benchmarks/cli_format_bench.py b/benchmarks/cli_format_bench.py index 246f3d66b..2fac742d1 100644 --- a/benchmarks/cli_format_bench.py +++ b/benchmarks/cli_format_bench.py @@ -86,7 +86,7 @@ class ActionListFormatSuite: params: list[str] = ["json", "yaml", "plain", "table", "rich"] param_names: list[str] = ["format"] - def setup(self) -> None: + def setup(self, fmt: str) -> None: # noqa: ARG002 self._svc = MagicMock() self._svc.list_actions.return_value = [ _mock_action(f"local/action-{i}") for i in range(20) @@ -97,7 +97,7 @@ class ActionListFormatSuite: ) self._patcher.start() - def teardown(self) -> None: + def teardown(self, fmt: str) -> None: # noqa: ARG002 self._patcher.stop() def time_list(self, fmt: str) -> None: @@ -110,7 +110,7 @@ class PlanStatusFormatSuite: params: list[str] = ["json", "yaml", "plain", "table", "rich"] param_names: list[str] = ["format"] - def setup(self) -> None: + def setup(self, fmt: str) -> None: # noqa: ARG002 self._svc = MagicMock() self._svc.get_plan.return_value = _mock_plan() self._patcher = patch( @@ -119,7 +119,7 @@ class PlanStatusFormatSuite: ) self._patcher.start() - def teardown(self) -> None: + def teardown(self, fmt: str) -> None: # noqa: ARG002 self._patcher.stop() def time_status(self, fmt: str) -> None: diff --git a/benchmarks/plan_model_bench.py b/benchmarks/plan_model_bench.py index 4a6cb5359..4a7c4fa12 100644 --- a/benchmarks/plan_model_bench.py +++ b/benchmarks/plan_model_bench.py @@ -152,7 +152,7 @@ class PhaseTransitionSuite: def time_can_transition_invalid(self) -> None: """Time an invalid phase transition check.""" - can_transition(PlanPhase.STRATEGIZE, PlanPhase.APPLIED) + can_transition(PlanPhase.STRATEGIZE, PlanPhase.APPLY) def time_all_transitions(self) -> None: """Time checking all possible phase transition pairs.""" diff --git a/benchmarks/plan_phase_migration_bench.py b/benchmarks/plan_phase_migration_bench.py index d6c4543a3..e573098b4 100644 --- a/benchmarks/plan_phase_migration_bench.py +++ b/benchmarks/plan_phase_migration_bench.py @@ -8,6 +8,7 @@ Measures: from __future__ import annotations +import threading from datetime import UTC, datetime from sqlalchemy import create_engine @@ -19,6 +20,18 @@ from cleveragents.infrastructure.database.models import ( LifecyclePlanModel, ) +# Thread-safe counter so repeated ASV iterations never collide on IDs. +_iter_lock = threading.Lock() +_iter_counter = 0 + + +def _next_iter() -> int: + """Return a monotonically increasing iteration number.""" + global _iter_counter + with _iter_lock: + _iter_counter += 1 + return _iter_counter + def _now_iso() -> str: return datetime.now(tz=UTC).isoformat() @@ -67,13 +80,15 @@ class PlanInsertActionPhase: self.engine.dispose() def time_insert_100_action_phase_plans(self) -> None: + it = _next_iter() + base = it * 10000 session = self.session_factory() now = _now_iso() for i in range(100): plan = LifecyclePlanModel() - plan.plan_id = _make_ulid(i) + plan.plan_id = _make_ulid(base + i) plan.action_name = "bench/phase-action" - plan.namespaced_name = f"bench/plan-{i}" + plan.namespaced_name = f"bench/plan-{it}-{i}" plan.namespace = "bench" plan.phase = "action" plan.processing_state = "queued" @@ -115,13 +130,15 @@ class PlanInsertApplyTerminalStates: self.engine.dispose() def time_insert_50_applied_plans(self) -> None: + it = _next_iter() + base = it * 10000 session = self.session_factory() now = _now_iso() for i in range(50): plan = LifecyclePlanModel() - plan.plan_id = _make_ulid(1000 + i) + plan.plan_id = _make_ulid(base + i) plan.action_name = "bench/terminal-action" - plan.namespaced_name = f"bench/applied-{i}" + plan.namespaced_name = f"bench/applied-{it}-{i}" plan.namespace = "bench" plan.phase = "apply" plan.processing_state = "applied" @@ -134,13 +151,15 @@ class PlanInsertApplyTerminalStates: session.close() def time_insert_50_constrained_plans(self) -> None: + it = _next_iter() + base = it * 10000 session = self.session_factory() now = _now_iso() for i in range(50): plan = LifecyclePlanModel() - plan.plan_id = _make_ulid(2000 + i) + plan.plan_id = _make_ulid(base + i) plan.action_name = "bench/terminal-action" - plan.namespaced_name = f"bench/constrained-{i}" + plan.namespaced_name = f"bench/constrained-{it}-{i}" plan.namespace = "bench" plan.phase = "apply" plan.processing_state = "constrained" diff --git a/benchmarks/project_migration_bench.py b/benchmarks/project_migration_bench.py index 4a561d01b..2e11520ed 100644 --- a/benchmarks/project_migration_bench.py +++ b/benchmarks/project_migration_bench.py @@ -10,6 +10,7 @@ Measures: from __future__ import annotations import json +import threading from datetime import UTC, datetime from sqlalchemy import create_engine, event, text @@ -23,6 +24,18 @@ from cleveragents.infrastructure.database.models import ( ResourceTypeModel, ) +# Thread-safe counter so repeated ASV iterations never collide on names/IDs. +_iter_lock = threading.Lock() +_iter_counter = 0 + + +def _next_iter() -> int: + """Return a monotonically increasing iteration number.""" + global _iter_counter + with _iter_lock: + _iter_counter += 1 + return _iter_counter + def _now_iso() -> str: return datetime.now(tz=UTC).isoformat() @@ -54,9 +67,10 @@ class ProjectInsert: self.engine.dispose() def time_insert_single_project(self) -> None: + it = _next_iter() session = self.session_factory() proj = NamespacedProjectModel( - namespaced_name="bench/single-project", + namespaced_name=f"bench/single-project-{it}", namespace="bench", tags_json="[]", created_at=_now_iso(), @@ -67,10 +81,11 @@ class ProjectInsert: session.close() def time_insert_100_projects(self) -> None: + it = _next_iter() session = self.session_factory() for i in range(100): proj = NamespacedProjectModel( - namespaced_name=f"bench/project-{i}", + namespaced_name=f"bench/project-{it}-{i}", namespace="bench", description=f"Project {i}", tags_json=json.dumps([f"tag-{i}"]), @@ -132,11 +147,25 @@ class ProjectLinkInsert: self.engine.dispose() def time_insert_50_links(self) -> None: + it = _next_iter() + base = it * 10000 session = self.session_factory() + # Create a unique project per iteration so the composite UNIQUE + # constraint (project_name, resource_id) never collides. + proj_name = f"bench/link-project-{it}" + proj = NamespacedProjectModel( + namespaced_name=proj_name, + namespace="bench", + tags_json="[]", + created_at=_now_iso(), + updated_at=_now_iso(), + ) + session.add(proj) + session.flush() for i in range(50): link = ProjectResourceLinkModel( - link_id=_make_ulid(1000 + i), - project_name="bench/link-project", + link_id=_make_ulid(base + i), + project_name=proj_name, resource_id=_make_ulid(i), read_only=i % 2 == 0, created_at=_now_iso(), diff --git a/benchmarks/uow_lifecycle_bench.py b/benchmarks/uow_lifecycle_bench.py index 9be54a7f2..0d1fa7918 100644 --- a/benchmarks/uow_lifecycle_bench.py +++ b/benchmarks/uow_lifecycle_bench.py @@ -67,11 +67,13 @@ def _make_bench_action(name: str = "local/bench-uow-action") -> Action: def _make_bench_plan( plan_id: str | None = None, action_name: str = "local/bench-uow-action", + plan_name: str | None = None, ) -> V3Plan: now = datetime.now() + name = plan_name or f"bench-uow-plan-{_bench_ulid()}" return V3Plan( identity=PlanIdentity(plan_id=plan_id or _bench_ulid(), attempt=1), - namespaced_name=NamespacedName(namespace="local", name="bench-uow-plan"), + namespaced_name=NamespacedName(namespace="local", name=name), action_name=action_name, description="Bench plan", definition_of_done="Done", @@ -188,11 +190,22 @@ class TimeUowActionPlanRoundTrip: session.close() def time_create_action_and_plan_via_uow(self) -> None: + uid = _bench_ulid() + # NamespacedName.validate_name lowercases the name, so the action + # is stored with a lowercased namespaced_name PK. The plan's + # action_name (a plain-string FK) must match exactly. + action_name = f"local/bench-combo-{uid}".lower() + # Commit the action first so the FK reference is durable before + # the plan repository (which has its own retry/rollback logic) + # attempts the insert. session = self.sf() ctx = UnitOfWorkContext(session) - uid = _bench_ulid() - action_name = f"local/bench-combo-{uid}" ctx.actions.create(_make_bench_action(action_name)) + session.commit() + session.close() + + session = self.sf() + ctx = UnitOfWorkContext(session) ctx.lifecycle_plans.create(_make_bench_plan(action_name=action_name)) session.commit() session.close()