Repository navigation
feat(video): normalize async job resource state #912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
85dd7b8
1cca76b
5e30a37
ed02361
f0f0372
9dd3a1f
c127b23
45fe4a8
1c52fc8
74bbe1e
fd8d456
a891aa5
d534af5
71f569a
7c32be8
769bcc1
3cdfff7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| name: Video job registry atomicity RED | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - feat/normalized-video-job-resource-20260829 | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| red: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 | ||
| with: | ||
| python-version: "3.12" | ||
| - name: Set up uv | ||
| uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e | ||
| with: | ||
| version: "0.12.5" | ||
| - name: Run atomicity regressions | ||
| run: >- | ||
| uv run --locked --extra api --extra db --extra queue --group dev | ||
| python -m pytest -q tests/test_video_job_registry_atomicity.py |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| from dataclasses import dataclass | ||
| import hashlib | ||
| import json | ||
| import threading | ||
| import time | ||
| from typing import Any | ||
| import uuid | ||
|
|
@@ -14,9 +15,30 @@ class VideoJobContractError(RuntimeError): | |
| """Raised when a provider submission cannot become a trackable video job.""" | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class VideoJobRecord: | ||
| """Persist the immutable ownership tuple for one gateway video job.""" | ||
|
|
||
| gateway_job_id: str | ||
| provider_job_id: str | ||
| agent_id: str | ||
| submitted_at: int | ||
| owner_id: str | ||
| agent_affinity_key: str = "" | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class VideoJobUsage: | ||
| """Persist the first complete provider usage report separately.""" | ||
|
|
||
| prompt_tokens: int | ||
| completion_tokens: int | ||
| observed_at: int | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class VideoJobOwner: | ||
| """Bind one gateway video job to the exact provider agent that accepted it.""" | ||
| """Combine an ownership record with its optional measured usage.""" | ||
|
|
||
| gateway_job_id: str | ||
| provider_job_id: str | ||
|
|
@@ -44,9 +66,19 @@ class VideoJobRegistry: | |
| """Store video-job ownership in the configured shared job registry.""" | ||
|
|
||
| def __init__(self, job_registry: Any) -> None: | ||
| self._owners = job_registry.mapping( | ||
| # Keep immutable ownership and mutable provider observations in separate | ||
| # records. The old mapping remains a compatibility path for jobs | ||
| # created before this normalized layout existed. | ||
| self._records = job_registry.mapping( | ||
| "video_job_records", decode=lambda raw: VideoJobRecord(**raw) | ||
| ) | ||
| self._usages = job_registry.mapping( | ||
| "video_job_usages", decode=lambda raw: VideoJobUsage(**raw) | ||
| ) | ||
| self._legacy_owners = job_registry.mapping( | ||
| "video_job_owners", decode=lambda raw: VideoJobOwner(**raw) | ||
| ) | ||
| self._usage_lock = threading.Lock() | ||
|
|
||
| def register( | ||
| self, | ||
|
|
@@ -65,20 +97,29 @@ def register( | |
| if not isinstance(owner_id, str) or not owner_id: | ||
| raise VideoJobContractError("video job owner is unavailable") | ||
| gateway_job_id = f"videojob_{uuid.uuid4().hex}" | ||
| reported_usage = self._provider_usage(provider_result) | ||
| self._owners[gateway_job_id] = VideoJobOwner( | ||
| submitted_at = int(time.time()) | ||
| record = VideoJobRecord( | ||
| gateway_job_id=gateway_job_id, | ||
| provider_job_id=provider_job_id, | ||
| agent_id=agent_id, | ||
| submitted_at=int(time.time()), | ||
| submitted_at=submitted_at, | ||
| owner_id=owner_id, | ||
| usage_measurement_status=( | ||
| "measured" if reported_usage is not None else "unavailable" | ||
| ), | ||
| provider_usage=reported_usage, | ||
| agent_affinity_key=agent_affinity_key, | ||
| ) | ||
| return self.public_response(provider_result, self._owners[gateway_job_id]) | ||
| reported_usage = self._provider_usage(provider_result) | ||
| # Core ownership is written first: a failure after provider acceptance | ||
| # must never leave the job without a gateway-addressable owner. | ||
| self._records[gateway_job_id] = record | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Rolling upgrades hide new video jobs During mixed-version deployments, Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| if reported_usage is not None: | ||
| self._store_usage_if_absent( | ||
| gateway_job_id, | ||
| VideoJobUsage( | ||
| prompt_tokens=reported_usage["prompt_tokens"], | ||
| completion_tokens=reported_usage["completion_tokens"], | ||
| observed_at=submitted_at, | ||
| ), | ||
| ) | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
|
||
| return self.public_response(provider_result, self._owner_from_record(record)) | ||
|
|
||
| @staticmethod | ||
| def public_response( | ||
|
|
@@ -111,30 +152,97 @@ def replace(value: Any) -> Any: | |
|
|
||
| def owner(self, gateway_job_id: str, owner_id: str) -> VideoJobOwner: | ||
| """Return a principal-owned record, hiding unknown and foreign identities alike.""" | ||
| owner = self._owners[gateway_job_id] | ||
| if not owner_id or owner.owner_id != owner_id: | ||
| record, legacy = self._record(gateway_job_id) | ||
| stored_owner_id = record.owner_id | ||
| if not owner_id or stored_owner_id != owner_id: | ||
| raise KeyError(gateway_job_id) | ||
| return owner | ||
| if legacy: | ||
| assert isinstance(record, VideoJobOwner) | ||
| return record | ||
| assert isinstance(record, VideoJobRecord) | ||
| return self._owner_from_record(record) | ||
|
|
||
| def _record(self, gateway_job_id: str) -> tuple[VideoJobRecord | VideoJobOwner, bool]: | ||
| """Load the normalized record, falling back to the legacy map.""" | ||
| try: | ||
| return self._records[gateway_job_id], False | ||
| except KeyError: | ||
| return self._legacy_owners[gateway_job_id], True | ||
|
|
||
| def _owner_from_record(self, record: VideoJobRecord) -> VideoJobOwner: | ||
| """Join one normalized ownership record to its optional usage row.""" | ||
| usage = self._usages.get(record.gateway_job_id) | ||
| provider_usage = ( | ||
| None | ||
| if usage is None | ||
| else { | ||
| "prompt_tokens": usage.prompt_tokens, | ||
| "completion_tokens": usage.completion_tokens, | ||
| } | ||
| ) | ||
| return VideoJobOwner( | ||
| gateway_job_id=record.gateway_job_id, | ||
| provider_job_id=record.provider_job_id, | ||
| agent_id=record.agent_id, | ||
| submitted_at=record.submitted_at, | ||
| owner_id=record.owner_id, | ||
| usage_measurement_status=( | ||
| "measured" if provider_usage is not None else "unavailable" | ||
| ), | ||
| provider_usage=provider_usage, | ||
| agent_affinity_key=record.agent_affinity_key, | ||
| ) | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
|
|
||
| def observe_provider_result( | ||
| self, owner: VideoJobOwner, provider_result: dict[str, Any] | ||
| ) -> VideoJobOwner: | ||
| """Persist provider-reported counts while retaining honest unknown state.""" | ||
| usage = self._provider_usage(provider_result) | ||
| if usage is None or owner.provider_usage is not None: | ||
| return owner | ||
| updated = VideoJobOwner( | ||
| gateway_job_id=owner.gateway_job_id, | ||
| provider_job_id=owner.provider_job_id, | ||
| agent_id=owner.agent_id, | ||
| submitted_at=owner.submitted_at, | ||
| owner_id=owner.owner_id, | ||
| usage_measurement_status="measured", | ||
| provider_usage=usage, | ||
| agent_affinity_key=owner.agent_affinity_key, | ||
| ) | ||
| self._owners[owner.gateway_job_id] = updated | ||
| return updated | ||
| record, legacy = self._record(owner.gateway_job_id) | ||
| if legacy: | ||
| if usage is None or owner.provider_usage is not None: | ||
| return owner | ||
| # Preserve the old payload shape until its configured retention | ||
| # expires; new submissions never write this denormalized mapping. | ||
| with self._usage_lock: | ||
| current = self._legacy_owners[owner.gateway_job_id] | ||
| if current.provider_usage is not None: | ||
| return current | ||
| updated = VideoJobOwner( | ||
| gateway_job_id=current.gateway_job_id, | ||
| provider_job_id=current.provider_job_id, | ||
| agent_id=current.agent_id, | ||
| submitted_at=current.submitted_at, | ||
| owner_id=current.owner_id, | ||
| usage_measurement_status="measured", | ||
| provider_usage=usage, | ||
| agent_affinity_key=current.agent_affinity_key, | ||
| ) | ||
| self._legacy_owners[owner.gateway_job_id] = updated | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
|
||
| return updated | ||
| assert isinstance(record, VideoJobRecord) | ||
| if usage is not None and owner.provider_usage is None: | ||
| self._store_usage_if_absent( | ||
| owner.gateway_job_id, | ||
| VideoJobUsage( | ||
| prompt_tokens=usage["prompt_tokens"], | ||
| completion_tokens=usage["completion_tokens"], | ||
| observed_at=int(time.time()), | ||
| ), | ||
| ) | ||
| return self._owner_from_record(record) | ||
|
seonghobae marked this conversation as resolved.
Resolved
Comment on lines
+260
to
+269
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Failed billing write never retries If Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| def _store_usage_if_absent(self, gateway_job_id: str, usage: VideoJobUsage) -> None: | ||
| """Keep the first complete report when replicas observe stale owners.""" | ||
| setter = getattr(self._usages, "set_if_absent", None) | ||
| if callable(setter): | ||
| setter(gateway_job_id, usage) | ||
| return | ||
| # ponytail: plain dict fallback has no compare-and-set; this process-local | ||
| # lock serializes its threaded request path. Valkey uses hsetnx above. | ||
| with self._usage_lock: | ||
| if self._usages.get(gateway_job_id) is None: | ||
| self._usages[gateway_job_id] = usage | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
|
|
||
| @staticmethod | ||
| def _provider_usage(document: dict[str, Any]) -> dict[str, int] | None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Video job resource contract | ||
|
|
||
| The gateway treats video generation as an asynchronous resource. Submission | ||
| returns an opaque gateway identifier; later status and content requests resolve | ||
| the stored provider owner rather than selecting a model again. The provider job | ||
| identifier remains internal, and a provider lifecycle value is copied only as | ||
| observed data. | ||
|
|
||
| New registry entries keep immutable ownership in `video_job_records` and the | ||
| first complete token report in `video_job_usages`. Missing usage remains | ||
| unavailable, repeated polling cannot revise the first measured report, and | ||
| provider status is returned only as observed by the current provider response. | ||
| Standalone process-local storage makes no durability claim. Valkey is required | ||
| when restart or replica continuity matters. | ||
|
|
||
| The contract follows the HTTP asynchronous-resource model: the upstream video | ||
| API accepts work and exposes a polling resource, while HTTP semantics require | ||
| the gateway to describe acceptance without claiming completion. The gateway's | ||
| opaque id also prevents a provider-specific resource identifier from becoming | ||
| the public ownership key. | ||
|
|
||
| ## References | ||
|
|
||
| OpenRouter. (2026). *Video generation*. OpenRouter documentation. | ||
| https://openrouter.ai/docs/guides/overview/multimodal/video-generation | ||
|
|
||
| Fielding, R. T., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC | ||
| 9110). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc9110 | ||
|
seonghobae marked this conversation as resolved.
Resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.