Spaces:
Running
Running
R14-RESUME-R1: worker-confirmed checkpoint and local epoch fencing
#56
by GuXiaopeng - opened
- core/checkpoint_store.py +183 -28
- core/generation_runner.py +24 -2
- task_executor.py +49 -16
core/checkpoint_store.py
CHANGED
|
@@ -10,9 +10,20 @@ from __future__ import annotations
|
|
| 10 |
import json
|
| 11 |
import os
|
| 12 |
import re
|
|
|
|
| 13 |
from datetime import datetime, timezone
|
| 14 |
from pathlib import Path
|
| 15 |
-
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def _atomic_json(path: Path, value: dict[str, Any]) -> None:
|
|
@@ -22,50 +33,194 @@ def _atomic_json(path: Path, value: dict[str, Any]) -> None:
|
|
| 22 |
os.replace(temporary, path)
|
| 23 |
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
class TaskCheckpointStore:
|
| 26 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
self.task_dir = Path(task_dir)
|
| 28 |
self.request_hash = str(request_hash)
|
| 29 |
self.execution_epoch = int(execution_epoch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
self.directory = self.task_dir / "checkpoints"
|
| 31 |
self.directory.mkdir(parents=True, exist_ok=True)
|
| 32 |
-
self.sequence = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
def load_latest(self) -> dict[str, Any] | None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
candidates: list[tuple[int, Path, dict[str, Any]]] = []
|
| 36 |
for path in self.directory.glob("checkpoint_*.json"):
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
if not isinstance(payload, dict):
|
| 40 |
-
continue
|
| 41 |
-
if payload.get("request_hash") != self.request_hash:
|
| 42 |
-
continue
|
| 43 |
-
seq = int(payload.get("checkpoint_seq") or 0)
|
| 44 |
-
if seq <= 0:
|
| 45 |
-
continue
|
| 46 |
-
candidates.append((seq, path, payload))
|
| 47 |
-
except Exception:
|
| 48 |
continue
|
|
|
|
|
|
|
| 49 |
if not candidates:
|
| 50 |
return None
|
| 51 |
seq, _, payload = max(candidates, key=lambda item: item[0])
|
| 52 |
self.sequence = seq
|
| 53 |
return payload
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def save(self, stage: str, state: dict[str, Any]) -> tuple[int, str]:
|
| 56 |
-
self.sequence += 1
|
| 57 |
stage_name = re.sub(r"[^A-Za-z0-9_.-]", "_", str(stage or "checkpoint"))[:80]
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
return self.sequence, stage_name
|
|
|
|
| 10 |
import json
|
| 11 |
import os
|
| 12 |
import re
|
| 13 |
+
from contextlib import contextmanager
|
| 14 |
from datetime import datetime, timezone
|
| 15 |
from pathlib import Path
|
| 16 |
+
from typing import Any, Iterator
|
| 17 |
+
|
| 18 |
+
try: # Hugging Face production runs on Linux.
|
| 19 |
+
import fcntl # type: ignore
|
| 20 |
+
except ImportError: # pragma: no cover - Windows-only local development fallback.
|
| 21 |
+
fcntl = None
|
| 22 |
+
import msvcrt # type: ignore
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
CLAIM_HANDSHAKE_FILENAME = "executor_claim.json"
|
| 26 |
+
AUTHORITY_LOCK_FILENAME = ".checkpoint_authority.lock"
|
| 27 |
|
| 28 |
|
| 29 |
def _atomic_json(path: Path, value: dict[str, Any]) -> None:
|
|
|
|
| 33 |
os.replace(temporary, path)
|
| 34 |
|
| 35 |
|
| 36 |
+
@contextmanager
|
| 37 |
+
def checkpoint_authority_lock(task_dir: Path) -> Iterator[None]:
|
| 38 |
+
"""Serialize local claim-marker replacement and checkpoint writes.
|
| 39 |
+
|
| 40 |
+
Worker/D1 remains the global authority. This small process lock only closes
|
| 41 |
+
the local race where a superseded executor could otherwise validate an old
|
| 42 |
+
marker immediately before a newer epoch replaces it, then write afterwards.
|
| 43 |
+
"""
|
| 44 |
+
lock_path = Path(task_dir) / AUTHORITY_LOCK_FILENAME
|
| 45 |
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
| 46 |
+
handle = lock_path.open("a+b")
|
| 47 |
+
try:
|
| 48 |
+
if fcntl is not None:
|
| 49 |
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
| 50 |
+
else: # pragma: no cover - Windows-only local development fallback.
|
| 51 |
+
handle.seek(0, os.SEEK_END)
|
| 52 |
+
if handle.tell() == 0:
|
| 53 |
+
handle.write(b"0")
|
| 54 |
+
handle.flush()
|
| 55 |
+
handle.seek(0)
|
| 56 |
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
| 57 |
+
yield
|
| 58 |
+
finally:
|
| 59 |
+
try:
|
| 60 |
+
if fcntl is not None:
|
| 61 |
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
| 62 |
+
else: # pragma: no cover - Windows-only local development fallback.
|
| 63 |
+
handle.seek(0)
|
| 64 |
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
| 65 |
+
finally:
|
| 66 |
+
handle.close()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
class TaskCheckpointStore:
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
task_dir: Path,
|
| 73 |
+
request_hash: str,
|
| 74 |
+
execution_epoch: int,
|
| 75 |
+
*,
|
| 76 |
+
task_id: str = "",
|
| 77 |
+
executor_instance_id: str = "",
|
| 78 |
+
confirmed_checkpoint_seq: int | None = None,
|
| 79 |
+
confirmed_checkpoint_stage: str = "",
|
| 80 |
+
):
|
| 81 |
self.task_dir = Path(task_dir)
|
| 82 |
self.request_hash = str(request_hash)
|
| 83 |
self.execution_epoch = int(execution_epoch)
|
| 84 |
+
self.task_id = str(task_id or "")
|
| 85 |
+
self.executor_instance_id = str(executor_instance_id or "")
|
| 86 |
+
if bool(self.task_id) != bool(self.executor_instance_id):
|
| 87 |
+
raise ValueError("checkpoint_authority_identity_incomplete")
|
| 88 |
+
self.confirmed_checkpoint_seq = (
|
| 89 |
+
None if confirmed_checkpoint_seq is None else max(0, int(confirmed_checkpoint_seq))
|
| 90 |
+
)
|
| 91 |
+
self.confirmed_checkpoint_stage = str(confirmed_checkpoint_stage or "")[:120]
|
| 92 |
self.directory = self.task_dir / "checkpoints"
|
| 93 |
self.directory.mkdir(parents=True, exist_ok=True)
|
| 94 |
+
self.sequence = int(self.confirmed_checkpoint_seq or 0)
|
| 95 |
+
|
| 96 |
+
def _read_payload(self, path: Path) -> dict[str, Any] | None:
|
| 97 |
+
try:
|
| 98 |
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 99 |
+
except Exception:
|
| 100 |
+
return None
|
| 101 |
+
if not isinstance(payload, dict):
|
| 102 |
+
return None
|
| 103 |
+
if payload.get("request_hash") != self.request_hash:
|
| 104 |
+
return None
|
| 105 |
+
try:
|
| 106 |
+
seq = int(payload.get("checkpoint_seq") or 0)
|
| 107 |
+
writer_epoch = int(payload.get("checkpoint_execution_epoch") or 0)
|
| 108 |
+
except (TypeError, ValueError):
|
| 109 |
+
return None
|
| 110 |
+
if seq <= 0 or writer_epoch <= 0 or writer_epoch > self.execution_epoch:
|
| 111 |
+
return None
|
| 112 |
+
return payload
|
| 113 |
+
|
| 114 |
+
def load_confirmed(
|
| 115 |
+
self,
|
| 116 |
+
checkpoint_seq: int | None = None,
|
| 117 |
+
checkpoint_stage: str | None = None,
|
| 118 |
+
) -> dict[str, Any] | None:
|
| 119 |
+
"""Load only the exact checkpoint sequence acknowledged by Worker/D1.
|
| 120 |
+
|
| 121 |
+
Unacknowledged local snapshots from an older network-partitioned epoch
|
| 122 |
+
are intentionally ignored even when they have a larger sequence number.
|
| 123 |
+
"""
|
| 124 |
+
seq = self.confirmed_checkpoint_seq if checkpoint_seq is None else max(0, int(checkpoint_seq))
|
| 125 |
+
stage = self.confirmed_checkpoint_stage if checkpoint_stage is None else str(checkpoint_stage or "")[:120]
|
| 126 |
+
if seq is None:
|
| 127 |
+
raise ValueError("confirmed_checkpoint_seq_required")
|
| 128 |
+
self.sequence = int(seq)
|
| 129 |
+
if seq <= 0:
|
| 130 |
+
return None
|
| 131 |
+
|
| 132 |
+
candidates: list[dict[str, Any]] = []
|
| 133 |
+
for path in self.directory.glob(f"checkpoint_{seq:05d}_*.json"):
|
| 134 |
+
payload = self._read_payload(path)
|
| 135 |
+
if payload is None:
|
| 136 |
+
continue
|
| 137 |
+
if int(payload.get("checkpoint_seq") or 0) != seq:
|
| 138 |
+
continue
|
| 139 |
+
if stage and str(payload.get("checkpoint_stage") or "") != stage:
|
| 140 |
+
continue
|
| 141 |
+
candidates.append(payload)
|
| 142 |
+
if len(candidates) != 1:
|
| 143 |
+
return None
|
| 144 |
+
return candidates[0]
|
| 145 |
|
| 146 |
def load_latest(self) -> dict[str, Any] | None:
|
| 147 |
+
# Authoritative production callers always pin recovery to the exact
|
| 148 |
+
# Worker-confirmed checkpoint. Keep the legacy scan only for isolated
|
| 149 |
+
# compatibility/tests that do not yet supply confirmed metadata.
|
| 150 |
+
if self.confirmed_checkpoint_seq is not None:
|
| 151 |
+
return self.load_confirmed()
|
| 152 |
+
|
| 153 |
candidates: list[tuple[int, Path, dict[str, Any]]] = []
|
| 154 |
for path in self.directory.glob("checkpoint_*.json"):
|
| 155 |
+
payload = self._read_payload(path)
|
| 156 |
+
if payload is None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
continue
|
| 158 |
+
seq = int(payload.get("checkpoint_seq") or 0)
|
| 159 |
+
candidates.append((seq, path, payload))
|
| 160 |
if not candidates:
|
| 161 |
return None
|
| 162 |
seq, _, payload = max(candidates, key=lambda item: item[0])
|
| 163 |
self.sequence = seq
|
| 164 |
return payload
|
| 165 |
|
| 166 |
+
def _assert_local_authority_locked(self) -> None:
|
| 167 |
+
if not self.task_id:
|
| 168 |
+
return
|
| 169 |
+
marker_path = self.task_dir / CLAIM_HANDSHAKE_FILENAME
|
| 170 |
+
try:
|
| 171 |
+
marker = json.loads(marker_path.read_text(encoding="utf-8"))
|
| 172 |
+
except Exception as exc:
|
| 173 |
+
raise RuntimeError("checkpoint_authority_marker_unavailable") from exc
|
| 174 |
+
if not isinstance(marker, dict):
|
| 175 |
+
raise RuntimeError("checkpoint_authority_marker_invalid")
|
| 176 |
+
|
| 177 |
+
expected = {
|
| 178 |
+
"task_id": self.task_id,
|
| 179 |
+
"request_hash": self.request_hash,
|
| 180 |
+
"execution_epoch": self.execution_epoch,
|
| 181 |
+
"executor_instance_id": self.executor_instance_id,
|
| 182 |
+
}
|
| 183 |
+
for key, value in expected.items():
|
| 184 |
+
actual = marker.get(key)
|
| 185 |
+
if key == "execution_epoch":
|
| 186 |
+
try:
|
| 187 |
+
actual = int(actual or 0)
|
| 188 |
+
except (TypeError, ValueError):
|
| 189 |
+
actual = 0
|
| 190 |
+
else:
|
| 191 |
+
actual = str(actual or "")
|
| 192 |
+
if actual != value:
|
| 193 |
+
raise RuntimeError("checkpoint_authority_lost")
|
| 194 |
+
|
| 195 |
+
if self.confirmed_checkpoint_seq is not None:
|
| 196 |
+
try:
|
| 197 |
+
marker_base_seq = int(marker.get("worker_checkpoint_seq") or 0)
|
| 198 |
+
except (TypeError, ValueError):
|
| 199 |
+
marker_base_seq = -1
|
| 200 |
+
marker_base_stage = str(marker.get("worker_checkpoint_stage") or "")[:120]
|
| 201 |
+
if marker_base_seq != int(self.confirmed_checkpoint_seq):
|
| 202 |
+
raise RuntimeError("checkpoint_authority_base_mismatch")
|
| 203 |
+
if self.confirmed_checkpoint_stage and marker_base_stage != self.confirmed_checkpoint_stage:
|
| 204 |
+
raise RuntimeError("checkpoint_authority_base_mismatch")
|
| 205 |
+
|
| 206 |
def save(self, stage: str, state: dict[str, Any]) -> tuple[int, str]:
|
|
|
|
| 207 |
stage_name = re.sub(r"[^A-Za-z0-9_.-]", "_", str(stage or "checkpoint"))[:80]
|
| 208 |
+
with checkpoint_authority_lock(self.task_dir):
|
| 209 |
+
self._assert_local_authority_locked()
|
| 210 |
+
next_sequence = self.sequence + 1
|
| 211 |
+
payload = dict(state or {})
|
| 212 |
+
payload.update(
|
| 213 |
+
{
|
| 214 |
+
"request_hash": self.request_hash,
|
| 215 |
+
"checkpoint_seq": next_sequence,
|
| 216 |
+
"checkpoint_stage": stage_name,
|
| 217 |
+
"checkpoint_execution_epoch": self.execution_epoch,
|
| 218 |
+
"checkpoint_executor_instance_id": self.executor_instance_id,
|
| 219 |
+
"checkpoint_saved_at": datetime.now(timezone.utc).isoformat(),
|
| 220 |
+
}
|
| 221 |
+
)
|
| 222 |
+
path = self.directory / f"checkpoint_{next_sequence:05d}_{stage_name}.json"
|
| 223 |
+
_atomic_json(path, payload)
|
| 224 |
+
_atomic_json(self.directory / "latest.json", payload)
|
| 225 |
+
self.sequence = next_sequence
|
| 226 |
return self.sequence, stage_name
|
core/generation_runner.py
CHANGED
|
@@ -146,12 +146,20 @@ class GenerationRunner:
|
|
| 146 |
task_dir: str | os.PathLike,
|
| 147 |
request_data: dict[str, Any],
|
| 148 |
execution_epoch: int,
|
|
|
|
|
|
|
|
|
|
| 149 |
progress_callback: ProgressCallback | None = None,
|
| 150 |
cancel_checker: Callable[[], bool] | None = None,
|
| 151 |
):
|
| 152 |
self.task_dir = Path(task_dir)
|
| 153 |
self.request = dict(request_data)
|
| 154 |
self.execution_epoch = int(execution_epoch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
self.progress_callback = progress_callback or (lambda **_: None)
|
| 156 |
self.cancel_checker = cancel_checker or (lambda: False)
|
| 157 |
expected_hash = str(self.request.get("request_hash") or "")
|
|
@@ -159,7 +167,15 @@ class GenerationRunner:
|
|
| 159 |
if not expected_hash or expected_hash != actual_hash:
|
| 160 |
raise RuntimeError("request_hash_mismatch")
|
| 161 |
self.request_hash = expected_hash
|
| 162 |
-
self.checkpoints = TaskCheckpointStore(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
self.output_dir = self.task_dir / "outputs"
|
| 164 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 165 |
self.agent = None
|
|
@@ -1516,7 +1532,13 @@ class GenerationRunner:
|
|
| 1516 |
quant_engine = get_quantitative_engine()
|
| 1517 |
qual_engine = get_qualitative_engine(agent)
|
| 1518 |
|
| 1519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1520 |
quality_diagnostics = list(state.get("quality_diagnostics") or [])
|
| 1521 |
if state:
|
| 1522 |
self._preview_english_text = str(state.get('full_draft_en') or '')
|
|
|
|
| 146 |
task_dir: str | os.PathLike,
|
| 147 |
request_data: dict[str, Any],
|
| 148 |
execution_epoch: int,
|
| 149 |
+
executor_instance_id: str = "",
|
| 150 |
+
confirmed_checkpoint_seq: int | None = None,
|
| 151 |
+
confirmed_checkpoint_stage: str = "",
|
| 152 |
progress_callback: ProgressCallback | None = None,
|
| 153 |
cancel_checker: Callable[[], bool] | None = None,
|
| 154 |
):
|
| 155 |
self.task_dir = Path(task_dir)
|
| 156 |
self.request = dict(request_data)
|
| 157 |
self.execution_epoch = int(execution_epoch)
|
| 158 |
+
self.executor_instance_id = str(executor_instance_id or "")
|
| 159 |
+
self.confirmed_checkpoint_seq = (
|
| 160 |
+
None if confirmed_checkpoint_seq is None else max(0, int(confirmed_checkpoint_seq))
|
| 161 |
+
)
|
| 162 |
+
self.confirmed_checkpoint_stage = str(confirmed_checkpoint_stage or "")[:120]
|
| 163 |
self.progress_callback = progress_callback or (lambda **_: None)
|
| 164 |
self.cancel_checker = cancel_checker or (lambda: False)
|
| 165 |
expected_hash = str(self.request.get("request_hash") or "")
|
|
|
|
| 167 |
if not expected_hash or expected_hash != actual_hash:
|
| 168 |
raise RuntimeError("request_hash_mismatch")
|
| 169 |
self.request_hash = expected_hash
|
| 170 |
+
self.checkpoints = TaskCheckpointStore(
|
| 171 |
+
self.task_dir,
|
| 172 |
+
self.request_hash,
|
| 173 |
+
self.execution_epoch,
|
| 174 |
+
task_id=str(self.request.get("task_id") or "") if self.executor_instance_id else "",
|
| 175 |
+
executor_instance_id=self.executor_instance_id,
|
| 176 |
+
confirmed_checkpoint_seq=self.confirmed_checkpoint_seq,
|
| 177 |
+
confirmed_checkpoint_stage=self.confirmed_checkpoint_stage,
|
| 178 |
+
)
|
| 179 |
self.output_dir = self.task_dir / "outputs"
|
| 180 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 181 |
self.agent = None
|
|
|
|
| 1532 |
quant_engine = get_quantitative_engine()
|
| 1533 |
qual_engine = get_qualitative_engine(agent)
|
| 1534 |
|
| 1535 |
+
if self.confirmed_checkpoint_seq is None:
|
| 1536 |
+
# Compatibility path for isolated unit helpers that do not represent
|
| 1537 |
+
# a claimed production executor. Production always supplies Worker
|
| 1538 |
+
# checkpoint metadata through task_executor.
|
| 1539 |
+
state = self.checkpoints.load_latest() or {}
|
| 1540 |
+
else:
|
| 1541 |
+
state = self.checkpoints.load_confirmed() or {}
|
| 1542 |
quality_diagnostics = list(state.get("quality_diagnostics") or [])
|
| 1543 |
if state:
|
| 1544 |
self._preview_english_text = str(state.get('full_draft_en') or '')
|
task_executor.py
CHANGED
|
@@ -27,6 +27,7 @@ from urllib.parse import urlsplit
|
|
| 27 |
import requests
|
| 28 |
|
| 29 |
from config import APP_CONFIG
|
|
|
|
| 30 |
|
| 31 |
_log_handlers = [logging.StreamHandler(sys.stdout)]
|
| 32 |
_executor_log_path = str(os.environ.get("SMARTPAPER_EXECUTOR_LOG_PATH") or "").strip()
|
|
@@ -119,6 +120,10 @@ class ExecutorApiError(RuntimeError):
|
|
| 119 |
self.payload = dict(payload or {})
|
| 120 |
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
def _is_recoverable_generation_failure(exc: Exception) -> bool:
|
| 123 |
"""Classify only post-claim execution failures.
|
| 124 |
|
|
@@ -432,18 +437,21 @@ def _bootstrap_claim(*, task_dir: Path, task_id: str, user_id: int, request_data
|
|
| 432 |
if int(task.get("target_words") or 0) != int(request_data.get("target_words") or 0):
|
| 433 |
raise RuntimeError("target_words_claim_mismatch")
|
| 434 |
try:
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
| 447 |
except Exception as exc:
|
| 448 |
try:
|
| 449 |
api.finish(
|
|
@@ -503,13 +511,29 @@ def main() -> int:
|
|
| 503 |
request_data=request_data,
|
| 504 |
api=api,
|
| 505 |
)
|
|
|
|
|
|
|
| 506 |
state = ExecutionState(task_id=args.task_id, user_id=args.user_id, epoch=epoch, instance_id=instance_id, api=api)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
state.start_heartbeat()
|
| 508 |
logging.info("[SP_EXECUTOR] claimed task_id=%s epoch=%s instance=%s", args.task_id, epoch, instance_id[:12])
|
| 509 |
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
try:
|
| 514 |
api.finish(
|
| 515 |
{
|
|
@@ -519,7 +543,7 @@ def main() -> int:
|
|
| 519 |
"executor_instance_id": instance_id,
|
| 520 |
"status": "failed",
|
| 521 |
"safe_error_code": "E_CHECKPOINT_MISSING",
|
| 522 |
-
"safe_error_detail": "
|
| 523 |
"current_node": "checkpoint_missing",
|
| 524 |
}
|
| 525 |
)
|
|
@@ -537,6 +561,9 @@ def main() -> int:
|
|
| 537 |
task_dir=task_dir,
|
| 538 |
request_data=request_data,
|
| 539 |
execution_epoch=epoch,
|
|
|
|
|
|
|
|
|
|
| 540 |
progress_callback=state.touch,
|
| 541 |
cancel_checker=state.cancelled,
|
| 542 |
)
|
|
@@ -570,6 +597,12 @@ def main() -> int:
|
|
| 570 |
else:
|
| 571 |
logging.warning("[SP_EXECUTOR] stopped_non_authoritative task_id=%s epoch=%s reason=%s", args.task_id, epoch, reason or "unknown")
|
| 572 |
return 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
if isinstance(exc, (ImportError, ModuleNotFoundError)):
|
| 574 |
logging.exception("[SP_EXECUTOR] bootstrap_import_failed task_id=%s epoch=%s error=%s", args.task_id, epoch, _safe(exc, 240))
|
| 575 |
_finish_bootstrap_failure(api, task_id=args.task_id, user_id=args.user_id, epoch=epoch, instance_id=instance_id, exc=exc)
|
|
|
|
| 27 |
import requests
|
| 28 |
|
| 29 |
from config import APP_CONFIG
|
| 30 |
+
from core.checkpoint_store import TaskCheckpointStore, checkpoint_authority_lock
|
| 31 |
|
| 32 |
_log_handlers = [logging.StreamHandler(sys.stdout)]
|
| 33 |
_executor_log_path = str(os.environ.get("SMARTPAPER_EXECUTOR_LOG_PATH") or "").strip()
|
|
|
|
| 120 |
self.payload = dict(payload or {})
|
| 121 |
|
| 122 |
|
| 123 |
+
def _is_local_checkpoint_authority_failure(exc: Exception) -> bool:
|
| 124 |
+
return isinstance(exc, RuntimeError) and str(exc or "").startswith("checkpoint_authority_")
|
| 125 |
+
|
| 126 |
+
|
| 127 |
def _is_recoverable_generation_failure(exc: Exception) -> bool:
|
| 128 |
"""Classify only post-claim execution failures.
|
| 129 |
|
|
|
|
| 437 |
if int(task.get("target_words") or 0) != int(request_data.get("target_words") or 0):
|
| 438 |
raise RuntimeError("target_words_claim_mismatch")
|
| 439 |
try:
|
| 440 |
+
with checkpoint_authority_lock(task_dir):
|
| 441 |
+
_atomic_json(
|
| 442 |
+
_claim_marker_path(task_dir),
|
| 443 |
+
{
|
| 444 |
+
"task_id": task_id,
|
| 445 |
+
"user_id": int(user_id),
|
| 446 |
+
"request_hash": request_hash,
|
| 447 |
+
"pid": os.getpid(),
|
| 448 |
+
"execution_epoch": epoch,
|
| 449 |
+
"executor_instance_id": instance_id,
|
| 450 |
+
"worker_checkpoint_seq": int(task.get("checkpoint_seq") or 0),
|
| 451 |
+
"worker_checkpoint_stage": str(task.get("checkpoint_stage") or "")[:120],
|
| 452 |
+
"claimed_at": datetime.now(timezone.utc).isoformat(),
|
| 453 |
+
},
|
| 454 |
+
)
|
| 455 |
except Exception as exc:
|
| 456 |
try:
|
| 457 |
api.finish(
|
|
|
|
| 511 |
request_data=request_data,
|
| 512 |
api=api,
|
| 513 |
)
|
| 514 |
+
remote_checkpoint_seq = int(task.get("checkpoint_seq") or 0)
|
| 515 |
+
remote_checkpoint_stage = str(task.get("checkpoint_stage") or "")[:120]
|
| 516 |
state = ExecutionState(task_id=args.task_id, user_id=args.user_id, epoch=epoch, instance_id=instance_id, api=api)
|
| 517 |
+
if remote_checkpoint_seq > 0:
|
| 518 |
+
state.touch(
|
| 519 |
+
checkpoint_seq=remote_checkpoint_seq,
|
| 520 |
+
checkpoint_stage=remote_checkpoint_stage,
|
| 521 |
+
current_node=remote_checkpoint_stage or "executor_claimed",
|
| 522 |
+
)
|
| 523 |
state.start_heartbeat()
|
| 524 |
logging.info("[SP_EXECUTOR] claimed task_id=%s epoch=%s instance=%s", args.task_id, epoch, instance_id[:12])
|
| 525 |
|
| 526 |
+
confirmed_store = TaskCheckpointStore(
|
| 527 |
+
task_dir,
|
| 528 |
+
str(request_data.get("request_hash") or ""),
|
| 529 |
+
epoch,
|
| 530 |
+
task_id=args.task_id,
|
| 531 |
+
executor_instance_id=instance_id,
|
| 532 |
+
confirmed_checkpoint_seq=remote_checkpoint_seq,
|
| 533 |
+
confirmed_checkpoint_stage=remote_checkpoint_stage,
|
| 534 |
+
)
|
| 535 |
+
confirmed_checkpoint = confirmed_store.load_confirmed()
|
| 536 |
+
if remote_checkpoint_seq > 0 and confirmed_checkpoint is None:
|
| 537 |
try:
|
| 538 |
api.finish(
|
| 539 |
{
|
|
|
|
| 543 |
"executor_instance_id": instance_id,
|
| 544 |
"status": "failed",
|
| 545 |
"safe_error_code": "E_CHECKPOINT_MISSING",
|
| 546 |
+
"safe_error_detail": "Worker-confirmed checkpoint metadata does not match available local checkpoint material; quota released.",
|
| 547 |
"current_node": "checkpoint_missing",
|
| 548 |
}
|
| 549 |
)
|
|
|
|
| 561 |
task_dir=task_dir,
|
| 562 |
request_data=request_data,
|
| 563 |
execution_epoch=epoch,
|
| 564 |
+
executor_instance_id=instance_id,
|
| 565 |
+
confirmed_checkpoint_seq=remote_checkpoint_seq,
|
| 566 |
+
confirmed_checkpoint_stage=remote_checkpoint_stage,
|
| 567 |
progress_callback=state.touch,
|
| 568 |
cancel_checker=state.cancelled,
|
| 569 |
)
|
|
|
|
| 597 |
else:
|
| 598 |
logging.warning("[SP_EXECUTOR] stopped_non_authoritative task_id=%s epoch=%s reason=%s", args.task_id, epoch, reason or "unknown")
|
| 599 |
return 3
|
| 600 |
+
if _is_local_checkpoint_authority_failure(exc):
|
| 601 |
+
logging.warning(
|
| 602 |
+
"[SP_EXECUTOR] stopped_non_authoritative task_id=%s epoch=%s reason=local_checkpoint_authority_lost error=%s",
|
| 603 |
+
args.task_id, epoch, _safe(exc, 180),
|
| 604 |
+
)
|
| 605 |
+
return 3
|
| 606 |
if isinstance(exc, (ImportError, ModuleNotFoundError)):
|
| 607 |
logging.exception("[SP_EXECUTOR] bootstrap_import_failed task_id=%s epoch=%s error=%s", args.task_id, epoch, _safe(exc, 240))
|
| 608 |
_finish_bootstrap_failure(api, task_id=args.task_id, user_id=args.user_id, epoch=epoch, instance_id=instance_id, exc=exc)
|