rememberstack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
"""Transactional operations on the D67 work ledger (processing_state + cost_ledger).
|
|
2
|
+
|
|
3
|
+
The ledger is the sole work-truth (D12/D67): enqueue is idempotent on the
|
|
4
|
+
(deployment, target, stage, component version) key; claiming uses SKIP LOCKED
|
|
5
|
+
over the (deployment, stage, lane) route; attempts count handler executions that
|
|
6
|
+
actually began; failures keep their full traceback; the DLQ is
|
|
7
|
+
status='dead_letter' rows; billed calls copy their attribution from the locked
|
|
8
|
+
running row and callers can never supply it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from datetime import timedelta
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from typing import cast
|
|
16
|
+
from typing import Self
|
|
17
|
+
from uuid import UUID
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
from pydantic import model_validator
|
|
21
|
+
from pydantic_settings import BaseSettings
|
|
22
|
+
from pydantic_settings import SettingsConfigDict
|
|
23
|
+
from sqlalchemy import bindparam
|
|
24
|
+
from sqlalchemy import Connection
|
|
25
|
+
from sqlalchemy import DateTime
|
|
26
|
+
from sqlalchemy import JSON
|
|
27
|
+
from sqlalchemy import text
|
|
28
|
+
from sqlalchemy.engine import Engine
|
|
29
|
+
from sqlalchemy.engine import RowMapping
|
|
30
|
+
|
|
31
|
+
from rememberstack.model import BudgetParked
|
|
32
|
+
from rememberstack.model import ClaimedWork
|
|
33
|
+
from rememberstack.model import CostBudget
|
|
34
|
+
from rememberstack.model import CostBudgetStatus
|
|
35
|
+
from rememberstack.model import CostTierSpend
|
|
36
|
+
from rememberstack.model import DeadLetterReplayResult
|
|
37
|
+
from rememberstack.model import EnqueueOutcome
|
|
38
|
+
from rememberstack.model import EnqueueWork
|
|
39
|
+
from rememberstack.model import ForgetInProgressError
|
|
40
|
+
from rememberstack.model import LaneRouteError
|
|
41
|
+
from rememberstack.model import PipelineStage
|
|
42
|
+
from rememberstack.model import ProcessingLane
|
|
43
|
+
from rememberstack.model import QueueRoute
|
|
44
|
+
from rememberstack.model import RecordCall
|
|
45
|
+
from rememberstack.model import WorkNotDeadLetterError
|
|
46
|
+
from rememberstack.model import WorkNotFoundError
|
|
47
|
+
from rememberstack.model import WorkNotRunningError
|
|
48
|
+
from rememberstack.spine.admission import active_forget_id_on
|
|
49
|
+
from rememberstack.spine.catalog_contract import lane_is_valid
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class WorkLedgerSettings(BaseSettings):
|
|
53
|
+
"""Retry backoff plus explicit route budgets for one worker deployment."""
|
|
54
|
+
|
|
55
|
+
model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_WORK_", extra="ignore")
|
|
56
|
+
|
|
57
|
+
retry_backoff_base_s: float = 2.0
|
|
58
|
+
retry_backoff_max_s: float = 60.0
|
|
59
|
+
budgets: tuple[CostBudget, ...] = ()
|
|
60
|
+
|
|
61
|
+
@model_validator(mode="after")
|
|
62
|
+
def require_unique_valid_budget_routes(self) -> Self:
|
|
63
|
+
"""Reject ambiguous ceilings and stage/lane pairs that cannot be queued."""
|
|
64
|
+
routes: set[tuple[UUID, PipelineStage, ProcessingLane | None]] = set()
|
|
65
|
+
for budget in self.budgets:
|
|
66
|
+
if not lane_is_valid(
|
|
67
|
+
stage=budget.stage,
|
|
68
|
+
lane=None if budget.lane is None else budget.lane.value,
|
|
69
|
+
):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"stage {budget.stage} does not accept budget lane {budget.lane!r}"
|
|
72
|
+
)
|
|
73
|
+
route = (budget.deployment_id, budget.stage, budget.lane)
|
|
74
|
+
if route in routes:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"only one cost budget may be configured per deployment, stage, and lane"
|
|
77
|
+
)
|
|
78
|
+
routes.add(route)
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class _BudgetWindowSpend:
|
|
84
|
+
"""The database-clock window and deduplicated spend used by one pre-flight."""
|
|
85
|
+
|
|
86
|
+
started_at: datetime
|
|
87
|
+
ends_at: datetime
|
|
88
|
+
spent_usd: Decimal
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class WorkLedger:
|
|
92
|
+
"""The spine's typed gateway to processing_state and cost_ledger (D12/D67)."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, *, engine: Engine, settings: WorkLedgerSettings) -> None:
|
|
95
|
+
"""Bind the ledger to an explicit engine and explicit settings."""
|
|
96
|
+
self._engine = engine
|
|
97
|
+
self._settings = settings
|
|
98
|
+
|
|
99
|
+
def enqueue(self, *, work: EnqueueWork) -> EnqueueOutcome:
|
|
100
|
+
"""Insert one unit of work idempotently and return what happened.
|
|
101
|
+
|
|
102
|
+
A duplicate of an existing (deployment, target, stage, version) key never
|
|
103
|
+
creates a second unit of work. A steady enqueue promotes a pending/failed
|
|
104
|
+
backfill duplicate to the steady lane (live work keeps its freshness
|
|
105
|
+
guarantee); a backfill enqueue can never demote steady work (D67).
|
|
106
|
+
"""
|
|
107
|
+
_require_valid_lane(stage=work.stage, lane=work.lane)
|
|
108
|
+
with self._engine.begin() as connection:
|
|
109
|
+
return enqueue_on(connection=connection, work=work)
|
|
110
|
+
|
|
111
|
+
def claim_one(
|
|
112
|
+
self, *, deployment_id: UUID, stage: PipelineStage, lane: ProcessingLane | None
|
|
113
|
+
) -> ClaimedWork | BudgetParked | None:
|
|
114
|
+
"""Claim, budget-park, or find no due row on one route.
|
|
115
|
+
|
|
116
|
+
The due row is locked before the current route-window spend is checked.
|
|
117
|
+
Exhaustion durably parks it until the aligned window rolls, without
|
|
118
|
+
consuming an attempt or touching its last error. Otherwise claiming
|
|
119
|
+
clears any defer reason, moves it to running, and increments attempts
|
|
120
|
+
exactly once immediately before the handler begins.
|
|
121
|
+
"""
|
|
122
|
+
_require_valid_lane(stage=stage, lane=lane)
|
|
123
|
+
with self._engine.begin() as connection:
|
|
124
|
+
if stage is not PipelineStage.HARD_FORGET:
|
|
125
|
+
active_forget = active_forget_id_on(
|
|
126
|
+
connection=connection, deployment_id=deployment_id
|
|
127
|
+
)
|
|
128
|
+
if active_forget is not None:
|
|
129
|
+
raise ForgetInProgressError(
|
|
130
|
+
f"deployment {deployment_id} is honoring forget_id"
|
|
131
|
+
f" {active_forget}"
|
|
132
|
+
)
|
|
133
|
+
row = (
|
|
134
|
+
connection.execute(
|
|
135
|
+
_CLAIM_SELECT,
|
|
136
|
+
{"deployment_id": deployment_id, "stage": stage, "lane": lane},
|
|
137
|
+
)
|
|
138
|
+
.mappings()
|
|
139
|
+
.first()
|
|
140
|
+
)
|
|
141
|
+
if row is None:
|
|
142
|
+
return None
|
|
143
|
+
budget = self._budget_for(
|
|
144
|
+
deployment_id=deployment_id, stage=stage, lane=lane
|
|
145
|
+
)
|
|
146
|
+
if budget is not None:
|
|
147
|
+
spend = _budget_window_spend(connection=connection, budget=budget)
|
|
148
|
+
if spend.spent_usd >= budget.ceiling_usd:
|
|
149
|
+
connection.execute(
|
|
150
|
+
_PARK_BUDGET,
|
|
151
|
+
{
|
|
152
|
+
"processing_id": row["processing_id"],
|
|
153
|
+
"resume_at": spend.ends_at,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
return BudgetParked(
|
|
157
|
+
processing_id=row["processing_id"],
|
|
158
|
+
resume_at=spend.ends_at,
|
|
159
|
+
spent_usd=spend.spent_usd,
|
|
160
|
+
ceiling_usd=budget.ceiling_usd,
|
|
161
|
+
)
|
|
162
|
+
started = (
|
|
163
|
+
connection.execute(
|
|
164
|
+
_CLAIM_START, {"processing_id": row["processing_id"]}
|
|
165
|
+
)
|
|
166
|
+
.mappings()
|
|
167
|
+
.one()
|
|
168
|
+
)
|
|
169
|
+
return _claimed_work(row=started)
|
|
170
|
+
|
|
171
|
+
def budget_status(self, *, deployment_id: UUID) -> tuple[CostBudgetStatus, ...]:
|
|
172
|
+
"""Return current spend and parked work for every configured deployment budget."""
|
|
173
|
+
statuses: list[CostBudgetStatus] = []
|
|
174
|
+
with self._engine.connect() as connection:
|
|
175
|
+
for budget in self._settings.budgets:
|
|
176
|
+
if budget.deployment_id != deployment_id:
|
|
177
|
+
continue
|
|
178
|
+
spend = _budget_window_spend(connection=connection, budget=budget)
|
|
179
|
+
tier_rows = connection.execute(
|
|
180
|
+
_BUDGET_TIER_SPEND,
|
|
181
|
+
{
|
|
182
|
+
"deployment_id": budget.deployment_id,
|
|
183
|
+
"stage": budget.stage,
|
|
184
|
+
"lane": budget.lane,
|
|
185
|
+
"window_started_at": spend.started_at,
|
|
186
|
+
"window_ends_at": spend.ends_at,
|
|
187
|
+
},
|
|
188
|
+
).mappings()
|
|
189
|
+
tiers = tuple(
|
|
190
|
+
CostTierSpend(
|
|
191
|
+
tier=cast(str | None, row["tier"]),
|
|
192
|
+
cost_usd=_decimal(row["cost_usd"]),
|
|
193
|
+
)
|
|
194
|
+
for row in tier_rows
|
|
195
|
+
)
|
|
196
|
+
parked_work = int(
|
|
197
|
+
connection.execute(
|
|
198
|
+
_BUDGET_PARKED_COUNT,
|
|
199
|
+
{
|
|
200
|
+
"deployment_id": budget.deployment_id,
|
|
201
|
+
"stage": budget.stage,
|
|
202
|
+
"lane": budget.lane,
|
|
203
|
+
},
|
|
204
|
+
).scalar_one()
|
|
205
|
+
)
|
|
206
|
+
remaining = max(Decimal(0), budget.ceiling_usd - spend.spent_usd)
|
|
207
|
+
statuses.append(
|
|
208
|
+
CostBudgetStatus(
|
|
209
|
+
deployment_id=budget.deployment_id,
|
|
210
|
+
stage=budget.stage,
|
|
211
|
+
lane=budget.lane,
|
|
212
|
+
window_seconds=budget.window_seconds,
|
|
213
|
+
window_started_at=spend.started_at,
|
|
214
|
+
window_ends_at=spend.ends_at,
|
|
215
|
+
ceiling_usd=budget.ceiling_usd,
|
|
216
|
+
spent_usd=spend.spent_usd,
|
|
217
|
+
remaining_usd=remaining,
|
|
218
|
+
exhausted=spend.spent_usd >= budget.ceiling_usd,
|
|
219
|
+
parked_work=parked_work,
|
|
220
|
+
tiers=tiers,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
return tuple(statuses)
|
|
224
|
+
|
|
225
|
+
def complete(
|
|
226
|
+
self, *, processing_id: UUID, follow_up: tuple[EnqueueWork, ...] = ()
|
|
227
|
+
) -> tuple[EnqueueOutcome, ...]:
|
|
228
|
+
"""Mark a running attempt succeeded and enqueue its chain follow-ups atomically.
|
|
229
|
+
|
|
230
|
+
The chain rule (a completing stage enqueues the next stage for its target)
|
|
231
|
+
commits in the same transaction as the success mark, so a crash can never
|
|
232
|
+
record success without the follow-up work existing.
|
|
233
|
+
"""
|
|
234
|
+
for work in follow_up:
|
|
235
|
+
_require_valid_lane(stage=work.stage, lane=work.lane)
|
|
236
|
+
with self._engine.begin() as connection:
|
|
237
|
+
updated = connection.execute(
|
|
238
|
+
_COMPLETE, {"processing_id": processing_id}
|
|
239
|
+
).rowcount
|
|
240
|
+
if updated == 0:
|
|
241
|
+
raise WorkNotRunningError(
|
|
242
|
+
f"processing row {processing_id} is not running; cannot complete"
|
|
243
|
+
)
|
|
244
|
+
return tuple(
|
|
245
|
+
enqueue_on(connection=connection, work=work) for work in follow_up
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def fail(
|
|
249
|
+
self, *, processing_id: UUID, error: str, retryable: bool
|
|
250
|
+
) -> datetime | None:
|
|
251
|
+
"""Record a failed attempt with its full traceback; never bury it (core value 6).
|
|
252
|
+
|
|
253
|
+
A retryable failure with attempts remaining schedules a retry backoff
|
|
254
|
+
(status failed, defer_reason retry_backoff, not_before in the future) and
|
|
255
|
+
returns the scheduled time — the caller re-announces it through the
|
|
256
|
+
queue port (packaging §3: retry paths call the port in both profiles). A
|
|
257
|
+
failure at the attempt limit, or a non-retryable one, dead-letters the
|
|
258
|
+
row and returns None.
|
|
259
|
+
"""
|
|
260
|
+
with self._engine.begin() as connection:
|
|
261
|
+
row = (
|
|
262
|
+
connection.execute(_SELECT_FOR_FAIL, {"processing_id": processing_id})
|
|
263
|
+
.mappings()
|
|
264
|
+
.first()
|
|
265
|
+
)
|
|
266
|
+
if row is None:
|
|
267
|
+
raise WorkNotFoundError(
|
|
268
|
+
f"processing row {processing_id} does not exist"
|
|
269
|
+
)
|
|
270
|
+
if row["status"] != "running":
|
|
271
|
+
raise WorkNotRunningError(
|
|
272
|
+
f"processing row {processing_id} is not running; cannot fail it"
|
|
273
|
+
)
|
|
274
|
+
attempts, max_attempts = int(row["attempts"]), int(row["max_attempts"])
|
|
275
|
+
if retryable and attempts < max_attempts:
|
|
276
|
+
backoff_s = min(
|
|
277
|
+
self._settings.retry_backoff_base_s * 2 ** (attempts - 1),
|
|
278
|
+
self._settings.retry_backoff_max_s,
|
|
279
|
+
)
|
|
280
|
+
scheduled = connection.execute(
|
|
281
|
+
_FAIL_RETRY,
|
|
282
|
+
{
|
|
283
|
+
"processing_id": processing_id,
|
|
284
|
+
"error": error,
|
|
285
|
+
"backoff_s": backoff_s,
|
|
286
|
+
},
|
|
287
|
+
).scalar_one()
|
|
288
|
+
return scheduled
|
|
289
|
+
connection.execute(
|
|
290
|
+
_FAIL_DEAD_LETTER, {"processing_id": processing_id, "error": error}
|
|
291
|
+
)
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def park_for_budget(self, *, processing_id: UUID, resume_at: datetime) -> None:
|
|
295
|
+
"""Park queued work until its budget window rolls (D67).
|
|
296
|
+
|
|
297
|
+
Parking happens at claim-time pre-flight, before an attempt starts: it
|
|
298
|
+
applies only to pending/failed rows (a running attempt is never parked — that
|
|
299
|
+
would allow a second concurrent claim), sets defer_reason budget with a
|
|
300
|
+
future not_before, consumes no attempt, and touches no error state, so
|
|
301
|
+
it can never cause dead-lettering.
|
|
302
|
+
"""
|
|
303
|
+
with self._engine.begin() as connection:
|
|
304
|
+
updated = connection.execute(
|
|
305
|
+
_PARK_BUDGET, {"processing_id": processing_id, "resume_at": resume_at}
|
|
306
|
+
).rowcount
|
|
307
|
+
if updated == 0:
|
|
308
|
+
raise WorkNotRunningError(
|
|
309
|
+
f"processing row {processing_id} is not queued; only queued "
|
|
310
|
+
"work can be budget-parked"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def wake(self, *, processing_id: UUID) -> None:
|
|
314
|
+
"""Announce an existing committed row on the self-host wake channel.
|
|
315
|
+
|
|
316
|
+
The initial wake after enqueue is the schema-owned insert trigger; this
|
|
317
|
+
primitive re-announces for retry, replay, and janitor paths. It never
|
|
318
|
+
creates or mutates work state (the port contract), and SQL stays in the
|
|
319
|
+
spine — adapters call this, never NOTIFY directly.
|
|
320
|
+
"""
|
|
321
|
+
with self._engine.begin() as connection:
|
|
322
|
+
connection.execute(_WAKE, {"processing_id": str(processing_id)})
|
|
323
|
+
|
|
324
|
+
def replay_dead_letter(
|
|
325
|
+
self,
|
|
326
|
+
*,
|
|
327
|
+
deployment_id: UUID,
|
|
328
|
+
processing_id: UUID,
|
|
329
|
+
attempt_allowance: int = 1,
|
|
330
|
+
lane: ProcessingLane | None = None,
|
|
331
|
+
not_before: datetime | None = None,
|
|
332
|
+
) -> DeadLetterReplayResult:
|
|
333
|
+
"""Reopen exactly one deployment-owned dead letter for explicit replay.
|
|
334
|
+
|
|
335
|
+
The transition preserves the attempts already consumed and the complete
|
|
336
|
+
previous error. It grants only the requested additional attempt budget,
|
|
337
|
+
defaults to due now, and may reroute only to a lane accepted by the
|
|
338
|
+
row's immutable stage. Delivery is announced separately after commit.
|
|
339
|
+
"""
|
|
340
|
+
if attempt_allowance < 1:
|
|
341
|
+
raise ValueError("dead-letter replay requires at least one new attempt")
|
|
342
|
+
if not_before is not None and (
|
|
343
|
+
not_before.tzinfo is None or not_before.utcoffset() != timedelta(0)
|
|
344
|
+
):
|
|
345
|
+
raise ValueError("dead-letter replay not_before must be UTC")
|
|
346
|
+
with self._engine.begin() as connection:
|
|
347
|
+
existing = (
|
|
348
|
+
connection.execute(
|
|
349
|
+
_SELECT_DEAD_LETTER_FOR_REPLAY,
|
|
350
|
+
{"deployment_id": deployment_id, "processing_id": processing_id},
|
|
351
|
+
)
|
|
352
|
+
.mappings()
|
|
353
|
+
.one_or_none()
|
|
354
|
+
)
|
|
355
|
+
if existing is None:
|
|
356
|
+
raise WorkNotFoundError(
|
|
357
|
+
f"processing row {processing_id} does not exist in deployment "
|
|
358
|
+
f"{deployment_id}"
|
|
359
|
+
)
|
|
360
|
+
if existing["status"] != "dead_letter":
|
|
361
|
+
raise WorkNotDeadLetterError(
|
|
362
|
+
f"processing row {processing_id} has status {existing['status']}; "
|
|
363
|
+
"only dead-letter rows can be replayed"
|
|
364
|
+
)
|
|
365
|
+
stage = PipelineStage(existing["stage"])
|
|
366
|
+
replay_lane = (
|
|
367
|
+
None
|
|
368
|
+
if existing["lane"] is None and lane is None
|
|
369
|
+
else lane or ProcessingLane(existing["lane"])
|
|
370
|
+
)
|
|
371
|
+
_require_valid_lane(stage=stage, lane=replay_lane)
|
|
372
|
+
attempts = int(existing["attempts"])
|
|
373
|
+
if attempts + attempt_allowance > 32_767:
|
|
374
|
+
raise ValueError("dead-letter replay attempt budget exceeds smallint")
|
|
375
|
+
replayed = (
|
|
376
|
+
connection.execute(
|
|
377
|
+
_REPLAY_DEAD_LETTER,
|
|
378
|
+
{
|
|
379
|
+
"processing_id": processing_id,
|
|
380
|
+
"lane": replay_lane,
|
|
381
|
+
"attempt_allowance": attempt_allowance,
|
|
382
|
+
"not_before": not_before,
|
|
383
|
+
},
|
|
384
|
+
)
|
|
385
|
+
.mappings()
|
|
386
|
+
.one()
|
|
387
|
+
)
|
|
388
|
+
return DeadLetterReplayResult(
|
|
389
|
+
processing_id=processing_id,
|
|
390
|
+
route=QueueRoute(
|
|
391
|
+
deployment_id=deployment_id, stage=stage, lane=replay_lane
|
|
392
|
+
),
|
|
393
|
+
not_before=replayed["not_before"],
|
|
394
|
+
attempts=int(replayed["attempts"]),
|
|
395
|
+
max_attempts=int(replayed["max_attempts"]),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def record_call(self, *, call: RecordCall) -> bool:
|
|
399
|
+
"""Attribute one billed call to the running attempt; idempotent per call key.
|
|
400
|
+
|
|
401
|
+
Stage, lane, attempt, and target attribution are copied from the locked
|
|
402
|
+
running row (D67) — a caller or delivery envelope can never choose them.
|
|
403
|
+
Returns False when the (processing, attempt, call_key) row already exists,
|
|
404
|
+
so an acknowledged-late retry cannot double-bill.
|
|
405
|
+
"""
|
|
406
|
+
with self._engine.begin() as connection:
|
|
407
|
+
row = (
|
|
408
|
+
connection.execute(
|
|
409
|
+
_SELECT_FOR_COST, {"processing_id": call.processing_id}
|
|
410
|
+
)
|
|
411
|
+
.mappings()
|
|
412
|
+
.first()
|
|
413
|
+
)
|
|
414
|
+
if row is None:
|
|
415
|
+
raise WorkNotFoundError(
|
|
416
|
+
f"processing row {call.processing_id} does not exist"
|
|
417
|
+
)
|
|
418
|
+
if row["status"] != "running":
|
|
419
|
+
raise WorkNotRunningError(
|
|
420
|
+
f"processing row {call.processing_id} is not running; "
|
|
421
|
+
"cost attribution requires a running attempt"
|
|
422
|
+
)
|
|
423
|
+
inserted = connection.execute(
|
|
424
|
+
_INSERT_COST,
|
|
425
|
+
{
|
|
426
|
+
"cost_id": uuid4(),
|
|
427
|
+
"deployment_id": row["deployment_id"],
|
|
428
|
+
"processing_id": call.processing_id,
|
|
429
|
+
"stage": row["stage"],
|
|
430
|
+
"lane": row["lane"],
|
|
431
|
+
"target_kind": row["target_kind"],
|
|
432
|
+
"target_id": row["target_id"],
|
|
433
|
+
"component_version": row["component_version"],
|
|
434
|
+
"attempt": row["attempts"],
|
|
435
|
+
"call_key": call.call_key,
|
|
436
|
+
"model_name": call.model_name,
|
|
437
|
+
"tier": call.tier,
|
|
438
|
+
"tokens_in": call.tokens_in,
|
|
439
|
+
"tokens_out": call.tokens_out,
|
|
440
|
+
"cost_usd": call.cost_usd,
|
|
441
|
+
"latency_ms": call.latency_ms,
|
|
442
|
+
},
|
|
443
|
+
).rowcount
|
|
444
|
+
return inserted == 1
|
|
445
|
+
|
|
446
|
+
def _budget_for(
|
|
447
|
+
self, *, deployment_id: UUID, stage: PipelineStage, lane: ProcessingLane | None
|
|
448
|
+
) -> CostBudget | None:
|
|
449
|
+
"""Return the one validated ceiling for a route, if the operator configured it."""
|
|
450
|
+
return next(
|
|
451
|
+
(
|
|
452
|
+
budget
|
|
453
|
+
for budget in self._settings.budgets
|
|
454
|
+
if budget.deployment_id == deployment_id
|
|
455
|
+
and budget.stage == stage
|
|
456
|
+
and budget.lane == lane
|
|
457
|
+
),
|
|
458
|
+
None,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _budget_window_spend(
|
|
463
|
+
*, connection: Connection, budget: CostBudget
|
|
464
|
+
) -> _BudgetWindowSpend:
|
|
465
|
+
"""Read one aligned window and its deduplicated cost using the database clock."""
|
|
466
|
+
row = (
|
|
467
|
+
connection.execute(
|
|
468
|
+
_BUDGET_WINDOW_SPEND,
|
|
469
|
+
{
|
|
470
|
+
"deployment_id": budget.deployment_id,
|
|
471
|
+
"stage": budget.stage,
|
|
472
|
+
"lane": budget.lane,
|
|
473
|
+
"window_seconds": budget.window_seconds,
|
|
474
|
+
},
|
|
475
|
+
)
|
|
476
|
+
.mappings()
|
|
477
|
+
.one()
|
|
478
|
+
)
|
|
479
|
+
return _BudgetWindowSpend(
|
|
480
|
+
started_at=cast(datetime, row["window_started_at"]),
|
|
481
|
+
ends_at=cast(datetime, row["window_ends_at"]),
|
|
482
|
+
spent_usd=_decimal(row["spent_usd"]),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _decimal(value: object) -> Decimal:
|
|
487
|
+
"""Normalize a PostgreSQL numeric aggregate without introducing float rounding."""
|
|
488
|
+
return value if isinstance(value, Decimal) else Decimal(str(value))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _require_valid_lane(*, stage: PipelineStage, lane: ProcessingLane | None) -> None:
|
|
492
|
+
"""Reject a lane value that is illegal for the stage's route (D67 pairing)."""
|
|
493
|
+
if not lane_is_valid(stage=stage, lane=None if lane is None else lane.value):
|
|
494
|
+
raise LaneRouteError(
|
|
495
|
+
f"stage {stage} does not accept lane {lane!r}: plane-E stages require "
|
|
496
|
+
"steady or backfill; scheduled K/P stages must be unlaned"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def enqueue_on(*, connection: Connection, work: EnqueueWork) -> EnqueueOutcome:
|
|
501
|
+
"""Run the idempotent insert (+ steady-promotion rule) on an open transaction.
|
|
502
|
+
|
|
503
|
+
Public for spine services whose own row writes must commit atomically with
|
|
504
|
+
the work they chain (e.g. document ingest enqueueing convert): the caller
|
|
505
|
+
owns the transaction; the initial-wake trigger fires on its commit.
|
|
506
|
+
"""
|
|
507
|
+
_require_valid_lane(stage=work.stage, lane=work.lane)
|
|
508
|
+
inserted = (
|
|
509
|
+
connection.execute(
|
|
510
|
+
_INSERT_WORK,
|
|
511
|
+
{
|
|
512
|
+
"processing_id": uuid4(),
|
|
513
|
+
"deployment_id": work.deployment_id,
|
|
514
|
+
"target_kind": work.target_kind,
|
|
515
|
+
"target_id": work.target_id,
|
|
516
|
+
"stage": work.stage,
|
|
517
|
+
"component_version": work.component_version,
|
|
518
|
+
"content_hash": work.content_hash,
|
|
519
|
+
"lane": work.lane,
|
|
520
|
+
"payload": work.payload,
|
|
521
|
+
"not_before": work.not_before,
|
|
522
|
+
},
|
|
523
|
+
)
|
|
524
|
+
.mappings()
|
|
525
|
+
.first()
|
|
526
|
+
)
|
|
527
|
+
if inserted is not None:
|
|
528
|
+
return EnqueueOutcome(
|
|
529
|
+
processing_id=inserted["processing_id"],
|
|
530
|
+
created=True,
|
|
531
|
+
promoted_to_steady=False,
|
|
532
|
+
)
|
|
533
|
+
existing = (
|
|
534
|
+
connection.execute(
|
|
535
|
+
_SELECT_EXISTING,
|
|
536
|
+
{
|
|
537
|
+
"deployment_id": work.deployment_id,
|
|
538
|
+
"target_kind": work.target_kind,
|
|
539
|
+
"target_id": work.target_id,
|
|
540
|
+
"stage": work.stage,
|
|
541
|
+
"component_version": work.component_version,
|
|
542
|
+
},
|
|
543
|
+
)
|
|
544
|
+
.mappings()
|
|
545
|
+
.one()
|
|
546
|
+
)
|
|
547
|
+
promoted = False
|
|
548
|
+
if (
|
|
549
|
+
work.lane is ProcessingLane.STEADY
|
|
550
|
+
and existing["lane"] == ProcessingLane.BACKFILL.value
|
|
551
|
+
and existing["status"] in ("pending", "failed")
|
|
552
|
+
):
|
|
553
|
+
promoted = (
|
|
554
|
+
connection.execute(
|
|
555
|
+
_PROMOTE_TO_STEADY, {"processing_id": existing["processing_id"]}
|
|
556
|
+
).rowcount
|
|
557
|
+
== 1
|
|
558
|
+
)
|
|
559
|
+
if promoted:
|
|
560
|
+
# Promotion re-routes live work: wake steady listeners on commit
|
|
561
|
+
# (a backfill row parked under the backfill budget also became due).
|
|
562
|
+
connection.execute(_WAKE, {"processing_id": str(existing["processing_id"])})
|
|
563
|
+
return EnqueueOutcome(
|
|
564
|
+
processing_id=existing["processing_id"],
|
|
565
|
+
created=False,
|
|
566
|
+
promoted_to_steady=promoted,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _claimed_work(*, row: RowMapping) -> ClaimedWork:
|
|
571
|
+
"""Build the typed claimed-work record from a returned ledger row."""
|
|
572
|
+
return ClaimedWork(
|
|
573
|
+
processing_id=row["processing_id"],
|
|
574
|
+
deployment_id=row["deployment_id"],
|
|
575
|
+
target_kind=row["target_kind"],
|
|
576
|
+
target_id=row["target_id"],
|
|
577
|
+
stage=row["stage"],
|
|
578
|
+
component_version=row["component_version"],
|
|
579
|
+
content_hash=row["content_hash"],
|
|
580
|
+
lane=None if row["lane"] is None else ProcessingLane(row["lane"]),
|
|
581
|
+
attempt=int(row["attempts"]),
|
|
582
|
+
payload=row["payload"],
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
_INSERT_WORK = text(
|
|
587
|
+
"""
|
|
588
|
+
INSERT INTO processing_state (
|
|
589
|
+
processing_id, deployment_id, target_kind, target_id, stage,
|
|
590
|
+
component_version, content_hash, lane, payload, not_before
|
|
591
|
+
) VALUES (
|
|
592
|
+
:processing_id, :deployment_id, :target_kind, :target_id, :stage,
|
|
593
|
+
:component_version, :content_hash, :lane,
|
|
594
|
+
:payload, COALESCE(:not_before, now())
|
|
595
|
+
)
|
|
596
|
+
ON CONFLICT (deployment_id, target_kind, target_id, stage, component_version)
|
|
597
|
+
DO NOTHING
|
|
598
|
+
RETURNING processing_id
|
|
599
|
+
"""
|
|
600
|
+
).bindparams(bindparam("payload", type_=JSON))
|
|
601
|
+
|
|
602
|
+
_SELECT_EXISTING = text(
|
|
603
|
+
"""
|
|
604
|
+
SELECT processing_id, lane, status
|
|
605
|
+
FROM processing_state
|
|
606
|
+
WHERE deployment_id = :deployment_id
|
|
607
|
+
AND target_kind = :target_kind
|
|
608
|
+
AND target_id = :target_id
|
|
609
|
+
AND stage = :stage
|
|
610
|
+
AND component_version = :component_version
|
|
611
|
+
"""
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
_PROMOTE_TO_STEADY = text(
|
|
615
|
+
"""
|
|
616
|
+
UPDATE processing_state
|
|
617
|
+
SET lane = 'steady',
|
|
618
|
+
defer_reason = CASE WHEN defer_reason = 'budget' THEN NULL
|
|
619
|
+
ELSE defer_reason END,
|
|
620
|
+
not_before = CASE WHEN defer_reason = 'budget' THEN now()
|
|
621
|
+
ELSE not_before END
|
|
622
|
+
WHERE processing_id = :processing_id
|
|
623
|
+
AND lane = 'backfill'
|
|
624
|
+
AND status IN ('pending', 'failed')
|
|
625
|
+
"""
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
_CLAIM_SELECT = text(
|
|
629
|
+
"""
|
|
630
|
+
SELECT processing_id
|
|
631
|
+
FROM processing_state
|
|
632
|
+
WHERE deployment_id = :deployment_id
|
|
633
|
+
AND stage = :stage
|
|
634
|
+
AND lane IS NOT DISTINCT FROM :lane
|
|
635
|
+
AND status IN ('pending', 'failed')
|
|
636
|
+
AND not_before <= now()
|
|
637
|
+
AND attempts < max_attempts
|
|
638
|
+
ORDER BY not_before, enqueued_at, processing_id
|
|
639
|
+
LIMIT 1
|
|
640
|
+
FOR UPDATE SKIP LOCKED
|
|
641
|
+
"""
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
_CLAIM_START = text(
|
|
645
|
+
"""
|
|
646
|
+
UPDATE processing_state
|
|
647
|
+
SET status = 'running',
|
|
648
|
+
defer_reason = NULL,
|
|
649
|
+
attempts = attempts + 1,
|
|
650
|
+
started_at = now()
|
|
651
|
+
WHERE processing_id = :processing_id
|
|
652
|
+
RETURNING processing_id, deployment_id, target_kind, target_id, stage,
|
|
653
|
+
component_version, content_hash, lane, attempts, payload
|
|
654
|
+
"""
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
_COMPLETE = text(
|
|
658
|
+
"""
|
|
659
|
+
UPDATE processing_state
|
|
660
|
+
SET status = 'succeeded', finished_at = now()
|
|
661
|
+
WHERE processing_id = :processing_id AND status = 'running'
|
|
662
|
+
"""
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
_SELECT_FOR_FAIL = text(
|
|
666
|
+
"""
|
|
667
|
+
SELECT status, attempts, max_attempts
|
|
668
|
+
FROM processing_state
|
|
669
|
+
WHERE processing_id = :processing_id
|
|
670
|
+
FOR UPDATE
|
|
671
|
+
"""
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
_FAIL_RETRY = text(
|
|
675
|
+
"""
|
|
676
|
+
UPDATE processing_state
|
|
677
|
+
SET status = 'failed',
|
|
678
|
+
defer_reason = 'retry_backoff',
|
|
679
|
+
not_before = now() + make_interval(secs => :backoff_s),
|
|
680
|
+
last_error = :error
|
|
681
|
+
WHERE processing_id = :processing_id
|
|
682
|
+
RETURNING not_before
|
|
683
|
+
"""
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
_FAIL_DEAD_LETTER = text(
|
|
687
|
+
"""
|
|
688
|
+
UPDATE processing_state
|
|
689
|
+
SET status = 'dead_letter',
|
|
690
|
+
defer_reason = NULL,
|
|
691
|
+
last_error = :error,
|
|
692
|
+
finished_at = now()
|
|
693
|
+
WHERE processing_id = :processing_id
|
|
694
|
+
"""
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
_PARK_BUDGET = text(
|
|
698
|
+
"""
|
|
699
|
+
UPDATE processing_state
|
|
700
|
+
SET status = 'pending', defer_reason = 'budget', not_before = :resume_at
|
|
701
|
+
WHERE processing_id = :processing_id AND status IN ('pending', 'failed')
|
|
702
|
+
"""
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
_SELECT_DEAD_LETTER_FOR_REPLAY = text(
|
|
706
|
+
"""
|
|
707
|
+
SELECT status, stage, lane, attempts
|
|
708
|
+
FROM processing_state
|
|
709
|
+
WHERE deployment_id = :deployment_id
|
|
710
|
+
AND processing_id = :processing_id
|
|
711
|
+
FOR UPDATE
|
|
712
|
+
"""
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
_REPLAY_DEAD_LETTER = text(
|
|
716
|
+
"""
|
|
717
|
+
UPDATE processing_state
|
|
718
|
+
SET status = 'pending',
|
|
719
|
+
lane = :lane,
|
|
720
|
+
defer_reason = CASE
|
|
721
|
+
WHEN :not_before IS NOT NULL AND :not_before > now()
|
|
722
|
+
THEN 'scheduled'::processing_defer_reason
|
|
723
|
+
ELSE NULL
|
|
724
|
+
END,
|
|
725
|
+
not_before = COALESCE(:not_before, now()),
|
|
726
|
+
max_attempts = attempts + :attempt_allowance,
|
|
727
|
+
finished_at = NULL
|
|
728
|
+
WHERE processing_id = :processing_id
|
|
729
|
+
AND status = 'dead_letter'
|
|
730
|
+
RETURNING attempts, max_attempts, not_before
|
|
731
|
+
"""
|
|
732
|
+
).bindparams(bindparam("not_before", type_=DateTime(timezone=True)))
|
|
733
|
+
|
|
734
|
+
_BUDGET_WINDOW_SPEND = text(
|
|
735
|
+
"""
|
|
736
|
+
WITH bounds AS (
|
|
737
|
+
SELECT
|
|
738
|
+
to_timestamp(
|
|
739
|
+
floor(extract(epoch FROM now()) / :window_seconds)
|
|
740
|
+
* :window_seconds
|
|
741
|
+
) AS window_started_at,
|
|
742
|
+
to_timestamp(
|
|
743
|
+
(floor(extract(epoch FROM now()) / :window_seconds) + 1)
|
|
744
|
+
* :window_seconds
|
|
745
|
+
) AS window_ends_at
|
|
746
|
+
)
|
|
747
|
+
SELECT bounds.window_started_at,
|
|
748
|
+
bounds.window_ends_at,
|
|
749
|
+
COALESCE(sum(cost_ledger.cost_usd), 0) AS spent_usd
|
|
750
|
+
FROM bounds
|
|
751
|
+
LEFT JOIN cost_ledger
|
|
752
|
+
ON cost_ledger.deployment_id = :deployment_id
|
|
753
|
+
AND cost_ledger.stage = :stage
|
|
754
|
+
AND cost_ledger.lane IS NOT DISTINCT FROM :lane
|
|
755
|
+
AND cost_ledger.occurred_at >= bounds.window_started_at
|
|
756
|
+
AND cost_ledger.occurred_at < bounds.window_ends_at
|
|
757
|
+
GROUP BY bounds.window_started_at, bounds.window_ends_at
|
|
758
|
+
"""
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
_BUDGET_TIER_SPEND = text(
|
|
762
|
+
"""
|
|
763
|
+
SELECT tier, COALESCE(sum(cost_usd), 0) AS cost_usd
|
|
764
|
+
FROM cost_ledger
|
|
765
|
+
WHERE deployment_id = :deployment_id
|
|
766
|
+
AND stage = :stage
|
|
767
|
+
AND lane IS NOT DISTINCT FROM :lane
|
|
768
|
+
AND occurred_at >= :window_started_at
|
|
769
|
+
AND occurred_at < :window_ends_at
|
|
770
|
+
GROUP BY tier
|
|
771
|
+
ORDER BY tier NULLS FIRST
|
|
772
|
+
"""
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
_BUDGET_PARKED_COUNT = text(
|
|
776
|
+
"""
|
|
777
|
+
SELECT count(*)
|
|
778
|
+
FROM processing_state
|
|
779
|
+
WHERE deployment_id = :deployment_id
|
|
780
|
+
AND stage = :stage
|
|
781
|
+
AND lane IS NOT DISTINCT FROM :lane
|
|
782
|
+
AND status = 'pending'
|
|
783
|
+
AND defer_reason = 'budget'
|
|
784
|
+
"""
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
_SELECT_FOR_COST = text(
|
|
788
|
+
"""
|
|
789
|
+
SELECT deployment_id, status, stage, lane, attempts,
|
|
790
|
+
target_kind, target_id, component_version
|
|
791
|
+
FROM processing_state
|
|
792
|
+
WHERE processing_id = :processing_id
|
|
793
|
+
FOR UPDATE
|
|
794
|
+
"""
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
_INSERT_COST = text(
|
|
798
|
+
"""
|
|
799
|
+
INSERT INTO cost_ledger (
|
|
800
|
+
cost_id, deployment_id, processing_id, stage, lane, target_kind,
|
|
801
|
+
target_id, component_version, attempt, call_key, model_name, tier,
|
|
802
|
+
tokens_in, tokens_out, cost_usd, latency_ms
|
|
803
|
+
) VALUES (
|
|
804
|
+
:cost_id, :deployment_id, :processing_id, :stage, :lane, :target_kind,
|
|
805
|
+
:target_id, :component_version, :attempt, :call_key, :model_name, :tier,
|
|
806
|
+
:tokens_in, :tokens_out, :cost_usd, :latency_ms
|
|
807
|
+
)
|
|
808
|
+
ON CONFLICT (deployment_id, processing_id, attempt, call_key) DO NOTHING
|
|
809
|
+
"""
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
_WAKE = text(
|
|
813
|
+
"""
|
|
814
|
+
SELECT pg_notify('queue_wake', :processing_id)
|
|
815
|
+
"""
|
|
816
|
+
)
|