Skip to content

Commit e133e53

Browse files
committed
feat: make WorkflowContext thread-safe with a Lock
1 parent 1b9259c commit e133e53

2 files changed

Lines changed: 92 additions & 30 deletions

File tree

gateframe/core/context.py

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import threading
34
from datetime import datetime, timezone
45
from typing import Any
56

@@ -66,59 +67,70 @@ def __init__(
6667
) -> None:
6768
self.workflow_id = workflow_id
6869
self.escalation_threshold = escalation_threshold
69-
self.confidence = initial_confidence
70+
self._confidence = initial_confidence
7071
self._initial_confidence = initial_confidence
7172
self._soft_fail_penalty = soft_fail_penalty
7273
self._silent_fail_penalty = silent_fail_penalty
7374
self._history: list[StepRecord] = []
75+
self._lock = threading.Lock()
76+
77+
@property
78+
def confidence(self) -> float:
79+
with self._lock:
80+
return self._confidence
7481

7582
@property
7683
def history(self) -> list[StepRecord]:
77-
return list(self._history)
84+
with self._lock:
85+
return list(self._history)
7886

7987
@property
8088
def step_count(self) -> int:
81-
return len(self._history)
89+
with self._lock:
90+
return len(self._history)
8291

8392
@property
8493
def threshold_breached(self) -> bool:
85-
return self.confidence < self.escalation_threshold
94+
with self._lock:
95+
return self._confidence < self.escalation_threshold
8696

8797
def update(self, result: ValidationResult) -> None:
88-
confidence_before = self.confidence
8998
penalty = self._compute_penalty(result)
90-
self.confidence = max(0.0, self.confidence - penalty)
91-
92-
record = StepRecord(
93-
step_index=len(self._history),
94-
contract_name=result.contract_name,
95-
passed=result.passed,
96-
confidence_before=confidence_before,
97-
confidence_after=self.confidence,
98-
penalty_applied=penalty,
99-
failure_modes=[f.failure_mode for f in result.failures],
100-
)
101-
self._history.append(record)
99+
with self._lock:
100+
confidence_before = self._confidence
101+
self._confidence = max(0.0, self._confidence - penalty)
102+
confidence_after = self._confidence
103+
record = StepRecord(
104+
step_index=len(self._history),
105+
contract_name=result.contract_name,
106+
passed=result.passed,
107+
confidence_before=confidence_before,
108+
confidence_after=confidence_after,
109+
penalty_applied=penalty,
110+
failure_modes=[f.failure_mode for f in result.failures],
111+
)
112+
self._history.append(record)
102113

103114
logger.info(
104115
"workflow_step",
105116
workflow_id=self.workflow_id,
106117
step_index=record.step_index,
107118
contract=result.contract_name,
108119
passed=result.passed,
109-
confidence=self.confidence,
120+
confidence=confidence_after,
110121
threshold_breached=self.threshold_breached,
111122
)
112123

113124
def reset(self) -> None:
114-
self.confidence = self._initial_confidence
115-
self._history.clear()
125+
with self._lock:
126+
self._confidence = self._initial_confidence
127+
self._history.clear()
116128

117129
logger.info(
118130
"workflow_reset",
119131
workflow_id=self.workflow_id,
120-
confidence=self.confidence,
121-
threshold_breached=self.threshold_breached,
132+
confidence=self._initial_confidence,
133+
threshold_breached=self._initial_confidence < self.escalation_threshold,
122134
)
123135

124136
def _compute_penalty(self, result: ValidationResult) -> float:
@@ -131,11 +143,12 @@ def _compute_penalty(self, result: ValidationResult) -> float:
131143
return penalty
132144

133145
def to_dict(self) -> dict[str, Any]:
134-
return {
135-
"workflow_id": self.workflow_id,
136-
"confidence": self.confidence,
137-
"escalation_threshold": self.escalation_threshold,
138-
"threshold_breached": self.threshold_breached,
139-
"step_count": self.step_count,
140-
"history": [r.to_dict() for r in self._history],
141-
}
146+
with self._lock:
147+
return {
148+
"workflow_id": self.workflow_id,
149+
"confidence": self._confidence,
150+
"escalation_threshold": self.escalation_threshold,
151+
"threshold_breached": self._confidence < self.escalation_threshold,
152+
"step_count": len(self._history),
153+
"history": [r.to_dict() for r in self._history],
154+
}

tests/core/test_context.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import threading
12
from typing import Any
23

34
import pytest
@@ -194,6 +195,54 @@ def test_reset_restores_initial_confidence_and_clears_history(self) -> None:
194195
assert ctx.threshold_breached is False
195196

196197

198+
def test_concurrent_updates_do_not_corrupt_confidence(self) -> None:
199+
ctx = WorkflowContext("wf_concurrent", soft_fail_penalty=0.01)
200+
result = _make_result(passed=False, failures=[_soft_failure()])
201+
202+
def do_updates() -> None:
203+
for _ in range(50):
204+
ctx.update(result)
205+
206+
threads = [threading.Thread(target=do_updates) for _ in range(4)]
207+
for t in threads:
208+
t.start()
209+
for t in threads:
210+
t.join()
211+
212+
assert ctx.step_count == 200
213+
assert 0.0 <= ctx.confidence <= 1.0
214+
215+
def test_concurrent_update_and_reset(self) -> None:
216+
ctx = WorkflowContext("wf_reset_race")
217+
result = _make_result(passed=False, failures=[_soft_failure()])
218+
219+
errors: list[Exception] = []
220+
221+
def do_updates() -> None:
222+
try:
223+
for _ in range(30):
224+
ctx.update(result)
225+
except Exception as exc: # noqa: BLE001
226+
errors.append(exc)
227+
228+
def do_resets() -> None:
229+
try:
230+
for _ in range(10):
231+
ctx.reset()
232+
except Exception as exc: # noqa: BLE001
233+
errors.append(exc)
234+
235+
threads = [threading.Thread(target=do_updates) for _ in range(3)]
236+
threads += [threading.Thread(target=do_resets) for _ in range(2)]
237+
for t in threads:
238+
t.start()
239+
for t in threads:
240+
t.join()
241+
242+
assert errors == []
243+
assert 0.0 <= ctx.confidence <= 1.0
244+
245+
197246
class TestStepRecord:
198247
def test_to_dict(self) -> None:
199248
record = StepRecord(

0 commit comments

Comments
 (0)