Skip to content

Commit 5be032e

Browse files
committed
refactor: split run state transitions
1 parent ba97db4 commit 5be032e

3 files changed

Lines changed: 104 additions & 46 deletions

File tree

src/codealmanac/services/runs/store.py

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import UTC, datetime, timedelta
44
from pathlib import Path
55

6-
from codealmanac.core.errors import ConflictError, NotFoundError
6+
from codealmanac.core.errors import NotFoundError
77
from codealmanac.core.paths import normalize_path
88
from codealmanac.database.local import connect_local_database
99
from codealmanac.services.harnesses.models import HarnessEvent, HarnessTranscriptRef
@@ -23,6 +23,13 @@
2323
RunStatus,
2424
)
2525
from codealmanac.services.runs.tables import RUN_TABLES
26+
from codealmanac.services.runs.transitions import (
27+
attach_harness_transcript,
28+
cancel_run,
29+
finish_run,
30+
queued_message,
31+
start_run,
32+
)
2633
from codealmanac.services.runs.worker_locks import RunWorkerLockStore
2734

2835

@@ -188,20 +195,10 @@ def record_event(
188195

189196
def mark_running(self, run_id: str) -> RunRecord:
190197
record = self.read(run_id)
191-
if record.status == RunStatus.RUNNING:
192-
return record
193-
if record.status != RunStatus.QUEUED:
194-
raise ConflictError(
195-
f"run {run_id} cannot start from {record.status.value}"
196-
)
197198
now = datetime.now(UTC)
198-
running = record.model_copy(
199-
update={
200-
"status": RunStatus.RUNNING,
201-
"updated_at": now,
202-
"started_at": now,
203-
}
204-
)
199+
running = start_run(record, now)
200+
if running is record:
201+
return record
205202
self.update_record_preserving_spec(running)
206203
self.event_store.append_status(run_id, now, RunStatus.RUNNING.value)
207204
return running
@@ -212,12 +209,7 @@ def record_harness_transcript(
212209
transcript: HarnessTranscriptRef,
213210
) -> RunRecord:
214211
record = self.read(run_id)
215-
updated = record.model_copy(
216-
update={
217-
"harness_transcript": transcript,
218-
"updated_at": datetime.now(UTC),
219-
}
220-
)
212+
updated = attach_harness_transcript(record, transcript, datetime.now(UTC))
221213
self.update_record_preserving_spec(updated)
222214
return updated
223215

@@ -229,36 +221,21 @@ def finish(
229221
error: str | None,
230222
) -> RunRecord:
231223
record = self.read(run_id)
232-
if record.status == RunStatus.CANCELLED:
233-
return record
234224
now = datetime.now(UTC)
235-
finished = record.model_copy(
236-
update={
237-
"status": status,
238-
"summary": summary,
239-
"error": error,
240-
"updated_at": now,
241-
"finished_at": now,
242-
}
243-
)
225+
finished = finish_run(record, status, summary, error, now)
226+
if finished is record:
227+
return record
244228
self.update_record_preserving_spec(finished)
245229
self.event_store.append_status(run_id, now, status.value)
246230
return finished
247231

248232
def cancel(self, run_id: str) -> RunCancelResult:
249233
record = self.read(run_id)
250-
if record.status in TERMINAL_RUN_STATUSES:
251-
return RunCancelResult(record=record, changed=False)
252234
now = datetime.now(UTC)
253-
cancelled = record.model_copy(
254-
update={
255-
"status": RunStatus.CANCELLED,
256-
"updated_at": now,
257-
"finished_at": now,
258-
"summary": record.summary,
259-
"error": record.error,
260-
}
261-
)
235+
result = cancel_run(record, now)
236+
if not result.changed:
237+
return result
238+
cancelled = result.record
262239
self.update_record_preserving_spec(cancelled)
263240
self.event_store.append_status(run_id, now, RunStatus.CANCELLED.value)
264241
return RunCancelResult(record=cancelled, changed=True)
@@ -307,7 +284,3 @@ def connect(self):
307284

308285
def run_record_from_json(value: str) -> RunRecord:
309286
return RunRecord.model_validate_json(value)
310-
311-
312-
def queued_message(kind: RunKind) -> str:
313-
return f"queued {kind.value}"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from datetime import datetime
2+
3+
from codealmanac.core.errors import ConflictError
4+
from codealmanac.services.harnesses.models import HarnessTranscriptRef
5+
from codealmanac.services.runs.models import (
6+
TERMINAL_RUN_STATUSES,
7+
RunCancelResult,
8+
RunKind,
9+
RunRecord,
10+
RunStatus,
11+
)
12+
13+
14+
def queued_message(kind: RunKind) -> str:
15+
return f"queued {kind.value}"
16+
17+
18+
def start_run(record: RunRecord, now: datetime) -> RunRecord:
19+
if record.status == RunStatus.RUNNING:
20+
return record
21+
if record.status != RunStatus.QUEUED:
22+
raise ConflictError(
23+
f"run {record.run_id} cannot start from {record.status.value}"
24+
)
25+
return record.model_copy(
26+
update={
27+
"status": RunStatus.RUNNING,
28+
"updated_at": now,
29+
"started_at": now,
30+
}
31+
)
32+
33+
34+
def attach_harness_transcript(
35+
record: RunRecord,
36+
transcript: HarnessTranscriptRef,
37+
now: datetime,
38+
) -> RunRecord:
39+
return record.model_copy(
40+
update={
41+
"harness_transcript": transcript,
42+
"updated_at": now,
43+
}
44+
)
45+
46+
47+
def finish_run(
48+
record: RunRecord,
49+
status: RunStatus,
50+
summary: str | None,
51+
error: str | None,
52+
now: datetime,
53+
) -> RunRecord:
54+
if record.status == RunStatus.CANCELLED:
55+
return record
56+
return record.model_copy(
57+
update={
58+
"status": status,
59+
"summary": summary,
60+
"error": error,
61+
"updated_at": now,
62+
"finished_at": now,
63+
}
64+
)
65+
66+
67+
def cancel_run(record: RunRecord, now: datetime) -> RunCancelResult:
68+
if record.status in TERMINAL_RUN_STATUSES:
69+
return RunCancelResult(record=record, changed=False)
70+
cancelled = record.model_copy(
71+
update={
72+
"status": RunStatus.CANCELLED,
73+
"updated_at": now,
74+
"finished_at": now,
75+
"summary": record.summary,
76+
"error": record.error,
77+
}
78+
)
79+
return RunCancelResult(record=cancelled, changed=True)

tests/test_architecture.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,6 +1808,7 @@ def test_run_persistence_stays_split_by_responsibility():
18081808
service_text = (runs_root / "service.py").read_text(encoding="utf-8")
18091809
streaming_text = (runs_root / "streaming.py").read_text(encoding="utf-8")
18101810
tables_text = (runs_root / "tables.py").read_text(encoding="utf-8")
1811+
transitions_text = (runs_root / "transitions.py").read_text(encoding="utf-8")
18111812
worker_locks_text = (runs_root / "worker_locks.py").read_text(encoding="utf-8")
18121813
forbidden_store_fragments = (
18131814
"write_json_atomically",
@@ -1828,6 +1829,7 @@ def test_run_persistence_stays_split_by_responsibility():
18281829
"locks.py",
18291830
"streaming.py",
18301831
"tables.py",
1832+
"transitions.py",
18311833
"worker_locks.py",
18321834
} <= module_names
18331835
assert len(store_text.splitlines()) <= 330
@@ -1837,6 +1839,10 @@ def test_run_persistence_stays_split_by_responsibility():
18371839
assert "def new_run_record(" in factory_text
18381840
assert "uuid4" in factory_text
18391841
assert "strftime" in factory_text
1842+
assert "def start_run(" in transitions_text
1843+
assert "def finish_run(" in transitions_text
1844+
assert "def cancel_run(" in transitions_text
1845+
assert "ConflictError" in transitions_text
18401846
assert "class RunEventStore" in events_text
18411847
assert "run_events" in events_text
18421848
assert "RunWorkerLease" in locks_text

0 commit comments

Comments
 (0)