c4f71e930d
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 21s
CI / quality (pull_request) Successful in 21s
CI / build (pull_request) Successful in 23s
CI / typecheck (pull_request) Successful in 39s
CI / security (pull_request) Successful in 50s
CI / integration_tests (pull_request) Successful in 4m4s
CI / unit_tests (pull_request) Successful in 9m15s
CI / docker (pull_request) Successful in 1m0s
CI / benchmark-regression (pull_request) Successful in 23m41s
CI / coverage (pull_request) Successful in 43m24s
CI / lint (push) Successful in 22s
CI / build (push) Successful in 24s
CI / quality (push) Successful in 27s
CI / typecheck (push) Successful in 44s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 55s
CI / integration_tests (push) Successful in 4m16s
CI / benchmark-publish (push) Successful in 14m39s
CI / unit_tests (push) Successful in 24m24s
CI / docker (push) Successful in 1m1s
CI / coverage (push) Successful in 45m36s
Implement step-level progress persistence and plan resume with graceful shutdown handling. - Add ResumeCheckpoint, ResumeMetadata, ResumeEligibility, ResumeSummary domain models (resume.py) - Add PlanResumeService with validate_eligibility(), build_resume_summary(), resume_plan(dry_run), record_step_checkpoint(), record_shutdown() - Add last_completed_step and last_checkpoint_id fields to Plan model - Add 'plan resume' CLI command with --dry-run flag - Update plan lifecycle docs (ADR-006) with resume behavior section - Add Behave tests (24 scenarios in plan_resume.feature) - Add Robot Framework integration tests (10 tests) - Add ASV benchmarks for resume overhead Closes #328
160 lines
4.6 KiB
Python
160 lines
4.6 KiB
Python
"""ASV benchmarks for plan resume overhead baseline.
|
|
|
|
Measures the cost of:
|
|
- Resume eligibility validation
|
|
- Step checkpoint recording
|
|
- Resume summary building (dry-run)
|
|
- Live resume state transitions
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ulid import ULID
|
|
|
|
from cleveragents.application.services.plan_lifecycle_service import (
|
|
PlanLifecycleService,
|
|
)
|
|
from cleveragents.application.services.plan_resume_service import (
|
|
PlanResumeService,
|
|
)
|
|
from cleveragents.config.settings import Settings
|
|
from cleveragents.domain.models.core.plan import (
|
|
ProjectLink,
|
|
)
|
|
from cleveragents.domain.models.core.resume import ResumeMetadata
|
|
|
|
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
_bench_counter = 9000
|
|
|
|
|
|
def _bench_ulid() -> str:
|
|
global _bench_counter
|
|
_bench_counter += 1
|
|
n = _bench_counter
|
|
suffix = ""
|
|
for _ in range(8):
|
|
suffix = _CB32[n % 32] + suffix
|
|
n //= 32
|
|
return f"01HGZ6FE0AQDYTR4BX{suffix}"
|
|
|
|
|
|
def _make_services() -> tuple[PlanLifecycleService, PlanResumeService]:
|
|
settings = Settings()
|
|
lifecycle = PlanLifecycleService(settings=settings)
|
|
resume = PlanResumeService(lifecycle_service=lifecycle)
|
|
return lifecycle, resume
|
|
|
|
|
|
def _create_action(lifecycle: PlanLifecycleService, suffix: str) -> str:
|
|
name = f"local/bench-resume-{suffix}"
|
|
lifecycle.create_action(
|
|
name=name,
|
|
description="Benchmark resume action",
|
|
definition_of_done="Step 1\nStep 2\nStep 3",
|
|
strategy_actor="local/stub-strategy",
|
|
execution_actor="local/stub-execute",
|
|
)
|
|
return name
|
|
|
|
|
|
def _make_errored_plan(lifecycle: PlanLifecycleService, action_name: str) -> str:
|
|
plan = lifecycle.use_action(
|
|
action_name=action_name,
|
|
project_links=[ProjectLink(project_name="local/bench-proj")],
|
|
)
|
|
pid = plan.identity.plan_id
|
|
lifecycle.start_strategize(pid)
|
|
p = lifecycle.get_plan(pid)
|
|
p.decision_root_id = str(ULID())
|
|
lifecycle._commit_plan(p)
|
|
lifecycle.complete_strategize(pid)
|
|
lifecycle.execute_plan(pid)
|
|
lifecycle.start_execute(pid)
|
|
lifecycle.fail_execute(pid, "bench error")
|
|
return pid
|
|
|
|
|
|
class TimeResumeEligibility:
|
|
"""Benchmark resume eligibility validation."""
|
|
|
|
timeout = 30.0
|
|
|
|
def setup(self) -> None:
|
|
self.lifecycle, self.resume = _make_services()
|
|
action = _create_action(self.lifecycle, _bench_ulid())
|
|
self.plan_id = _make_errored_plan(self.lifecycle, action)
|
|
|
|
def time_validate_eligibility(self) -> None:
|
|
self.resume.validate_eligibility(self.plan_id)
|
|
|
|
|
|
class TimeCheckpointRecording:
|
|
"""Benchmark step checkpoint recording."""
|
|
|
|
timeout = 30.0
|
|
|
|
def setup(self) -> None:
|
|
self.lifecycle, self.resume = _make_services()
|
|
action = _create_action(self.lifecycle, _bench_ulid())
|
|
plan = self.lifecycle.use_action(
|
|
action_name=action,
|
|
project_links=[ProjectLink(project_name="local/bench-proj")],
|
|
)
|
|
pid = plan.identity.plan_id
|
|
self.lifecycle.start_strategize(pid)
|
|
p = self.lifecycle.get_plan(pid)
|
|
p.decision_root_id = str(ULID())
|
|
self.lifecycle._commit_plan(p)
|
|
self.lifecycle.complete_strategize(pid)
|
|
self.lifecycle.execute_plan(pid)
|
|
self.lifecycle.start_execute(pid)
|
|
self.plan_id = pid
|
|
self.resume.set_total_steps(pid, 100)
|
|
self.step_counter = 0
|
|
|
|
def time_record_checkpoint(self) -> None:
|
|
idx = self.step_counter
|
|
self.step_counter += 1
|
|
if idx >= 100:
|
|
return
|
|
self.resume.record_step_checkpoint(
|
|
self.plan_id, idx, f"DEC-{idx}", f"Step {idx}"
|
|
)
|
|
|
|
|
|
class TimeDryRunResume:
|
|
"""Benchmark dry-run resume summary building."""
|
|
|
|
timeout = 30.0
|
|
|
|
def setup(self) -> None:
|
|
self.lifecycle, self.resume = _make_services()
|
|
action = _create_action(self.lifecycle, _bench_ulid())
|
|
self.plan_id = _make_errored_plan(self.lifecycle, action)
|
|
self.resume.set_total_steps(self.plan_id, 10)
|
|
self.resume.record_step_checkpoint(self.plan_id, 5, "DEC-5", "Step 5")
|
|
|
|
def time_dry_run_resume(self) -> None:
|
|
self.resume.resume_plan(self.plan_id, dry_run=True)
|
|
|
|
|
|
class TimeResumeMetadataProperties:
|
|
"""Benchmark ResumeMetadata property access."""
|
|
|
|
timeout = 30.0
|
|
|
|
def setup(self) -> None:
|
|
self.metadata = ResumeMetadata(
|
|
last_completed_step=42,
|
|
total_steps=100,
|
|
)
|
|
|
|
def time_has_progress(self) -> None:
|
|
_ = self.metadata.has_progress
|
|
|
|
def time_next_step_index(self) -> None:
|
|
_ = self.metadata.next_step_index
|
|
|
|
def time_is_complete(self) -> None:
|
|
_ = self.metadata.is_complete
|