fix(history): back off retryable source failures
This commit is contained in:
parent
5f21e685ef
commit
575e7aaffd
3 changed files with 31 additions and 0 deletions
|
|
@ -10,6 +10,7 @@
|
||||||
"notion_batch_size": 600,
|
"notion_batch_size": 600,
|
||||||
"git_batch_size": 500,
|
"git_batch_size": 500,
|
||||||
"review_queue_backfill_batch_size": 500,
|
"review_queue_backfill_batch_size": 500,
|
||||||
|
"source_retry_backoff_seconds": 1800,
|
||||||
"semantic_review_endpoint": "http://127.0.0.1:8077/v1/broadcast",
|
"semantic_review_endpoint": "http://127.0.0.1:8077/v1/broadcast",
|
||||||
"semantic_review_interval_seconds": 600,
|
"semantic_review_interval_seconds": 600,
|
||||||
"semantic_review_batch_size": 8,
|
"semantic_review_batch_size": 8,
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,20 @@ def blocks_later_history(status: str) -> bool:
|
||||||
return status != "COMPLETE" and status not in SOURCE_WAITING_STATUSES
|
return status != "COMPLETE" and status not in SOURCE_WAITING_STATUSES
|
||||||
|
|
||||||
|
|
||||||
|
def retry_backoff_elapsed(
|
||||||
|
updated_at: str | None, backoff_seconds: int, now_unix: float | None = None
|
||||||
|
) -> bool:
|
||||||
|
"""Keep retryable sources alive without writing one failure per runtime cycle."""
|
||||||
|
if not updated_at:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
last_attempt = datetime.fromisoformat(updated_at).timestamp()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return True
|
||||||
|
current = time.time() if now_unix is None else now_unix
|
||||||
|
return current - last_attempt >= backoff_seconds
|
||||||
|
|
||||||
|
|
||||||
def redact_semantic_excerpt(text: str) -> str:
|
def redact_semantic_excerpt(text: str) -> str:
|
||||||
text = re.sub(
|
text = re.sub(
|
||||||
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
|
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
|
||||||
|
|
@ -1016,6 +1030,12 @@ class Runtime:
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_source(self, source: dict) -> None:
|
def process_source(self, source: dict) -> None:
|
||||||
|
state = self.store.state(source["id"])
|
||||||
|
if state["status"] == "ERROR_RETRYABLE" and not retry_backoff_elapsed(
|
||||||
|
state["updated_at"],
|
||||||
|
int(self.config.get("source_retry_backoff_seconds", 1800)),
|
||||||
|
):
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
if source["kind"] == "gpt_export":
|
if source["kind"] == "gpt_export":
|
||||||
self.process_gpt(source)
|
self.process_gpt(source)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,16 @@ class RuntimeTests(unittest.TestCase):
|
||||||
self.assertFalse(runtime.blocks_later_history("COMPLETE"))
|
self.assertFalse(runtime.blocks_later_history("COMPLETE"))
|
||||||
self.assertFalse(runtime.blocks_later_history("WAITING_FOR_SOURCE_ACCEPTANCE"))
|
self.assertFalse(runtime.blocks_later_history("WAITING_FOR_SOURCE_ACCEPTANCE"))
|
||||||
|
|
||||||
|
def test_retryable_source_uses_bounded_backoff(self):
|
||||||
|
updated_at = "1970-01-01T00:00:00+00:00"
|
||||||
|
self.assertFalse(
|
||||||
|
runtime.retry_backoff_elapsed(updated_at, 1800, now_unix=1799)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
runtime.retry_backoff_elapsed(updated_at, 1800, now_unix=1800)
|
||||||
|
)
|
||||||
|
self.assertTrue(runtime.retry_backoff_elapsed("invalid", 1800))
|
||||||
|
|
||||||
def test_semantic_redaction_and_reality_boundary(self):
|
def test_semantic_redaction_and_reality_boundary(self):
|
||||||
redacted = runtime.redact_semantic_excerpt(
|
redacted = runtime.redact_semantic_excerpt(
|
||||||
"a@example.com token: sk-abcdefghijklmnop "
|
"a@example.com token: sk-abcdefghijklmnop "
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue