Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
DevPlace CI / test (push) Failing after 44m45s
DevPlace CI / test (push) Failing after 44m45s
Continuous Delivery could never be turned off once a crop was planted. Add a one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant, autoreplant claim or Refactor. Autoreplant now also respects crop locks through the shared _crop_lock predicate. Plan and measurements in happyplanting.md. Also includes the pending community sync, node network and related work in the tree; the full suite passed (4999) with all of it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,11 +28,14 @@ class CircuitBreaker:
|
||||
failures: int = LIMITS.breaker_failures,
|
||||
open_seconds: float = LIMITS.breaker_open_seconds,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
wall: Callable[[], float] = time.time,
|
||||
) -> None:
|
||||
self.failures = max(1, failures)
|
||||
self.open_seconds = open_seconds
|
||||
self._clock = clock
|
||||
self._wall = wall
|
||||
self._circuits: dict[Hashable, _Circuit] = {}
|
||||
self.dirty = False
|
||||
|
||||
def state(self, key: Hashable) -> str:
|
||||
circuit = self._circuits.get(key)
|
||||
@@ -55,7 +58,8 @@ class CircuitBreaker:
|
||||
self._circuits[key].probing = True
|
||||
|
||||
def success(self, key: Hashable) -> None:
|
||||
self._circuits.pop(key, None)
|
||||
if self._circuits.pop(key, None) is not None:
|
||||
self.dirty = True
|
||||
|
||||
def failure(self, key: Hashable) -> None:
|
||||
circuit = self._circuits.setdefault(key, _Circuit())
|
||||
@@ -63,9 +67,49 @@ class CircuitBreaker:
|
||||
circuit.failures += 1
|
||||
if circuit.failures >= self.failures:
|
||||
circuit.opened_at = self._clock()
|
||||
self.dirty = True
|
||||
|
||||
def forget(self, key: Hashable) -> None:
|
||||
self._circuits.pop(key, None)
|
||||
if self._circuits.pop(key, None) is not None:
|
||||
self.dirty = True
|
||||
|
||||
def open_count(self) -> int:
|
||||
return sum(1 for key in list(self._circuits) if self.state(key) != CLOSED)
|
||||
|
||||
def snapshot(self) -> list[dict]:
|
||||
now_clock, now_wall = self._clock(), self._wall()
|
||||
rows = []
|
||||
for key, circuit in list(self._circuits.items()):
|
||||
state = self.state(key)
|
||||
rows.append(
|
||||
{
|
||||
"key": ":".join(str(part) for part in key) if isinstance(key, tuple) else str(key),
|
||||
"failures": circuit.failures,
|
||||
"state": state,
|
||||
"opened_at": now_wall - (now_clock - circuit.opened_at) if circuit.failures >= self.failures else 0.0,
|
||||
"seconds_left": max(0.0, self.open_seconds - (now_clock - circuit.opened_at))
|
||||
if state == OPEN
|
||||
else 0.0,
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda item: (-item["failures"], item["key"]))
|
||||
return rows
|
||||
|
||||
def restore(self, rows: list[dict]) -> int:
|
||||
now_clock, now_wall = self._clock(), self._wall()
|
||||
restored = 0
|
||||
for row in rows:
|
||||
key = tuple(str(row["key"]).split(":", 1))
|
||||
if len(key) != 2:
|
||||
continue
|
||||
failures = int(row.get("failures") or 0)
|
||||
opened_wall = float(row.get("opened_at") or 0.0)
|
||||
if failures < self.failures or opened_wall <= 0:
|
||||
continue
|
||||
age = max(0.0, now_wall - opened_wall)
|
||||
if age >= self.open_seconds * 2:
|
||||
continue
|
||||
circuit = _Circuit(failures=failures, opened_at=now_clock - age)
|
||||
self._circuits[key] = circuit
|
||||
restored += 1
|
||||
return restored
|
||||
|
||||
Reference in New Issue
Block a user