-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclaim_manager.py
More file actions
137 lines (121 loc) · 4.35 KB
/
Copy pathclaim_manager.py
File metadata and controls
137 lines (121 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Atomic per-video claim markers for duplicate processing prevention."""
from __future__ import annotations
import json
import logging
import os
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import List, Optional
logger = logging.getLogger(__name__)
CLAIMS_SUBDIR = os.path.join("tmp", "claims")
DEFAULT_CLAIM_TTL_SEC = 3600
@dataclass
class ClaimRecord:
video_id: str
run_id: str
worker_id: str
claimed_at: str
expires_at: str
source_queue: str = "queue"
class ClaimManager:
def __init__(self, work_path: str, ttl_sec: int = DEFAULT_CLAIM_TTL_SEC):
self.claims_dir = os.path.join(work_path, CLAIMS_SUBDIR)
self.ttl_sec = ttl_sec
os.makedirs(self.claims_dir, exist_ok=True)
def _path(self, video_id: str) -> str:
safe = (video_id or "unknown").replace("/", "_")
return os.path.join(self.claims_dir, f"{safe}.json")
def _utc_iso(self, ts: float) -> str:
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def read_claim(self, video_id: str) -> Optional[ClaimRecord]:
path = self._path(video_id)
if not os.path.isfile(path):
return None
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return ClaimRecord(**data)
except (OSError, json.JSONDecodeError, TypeError):
return None
def is_claim_valid(self, video_id: str, *, run_id: Optional[str] = None) -> bool:
rec = self.read_claim(video_id)
if not rec:
return False
try:
exp = datetime.strptime(rec.expires_at, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).timestamp()
if time.time() > exp:
return False
except ValueError:
return False
if run_id and rec.run_id != run_id:
return True
return True
def try_claim(
self,
video_id: str,
run_id: str,
worker_id: str,
*,
source_queue: str = "queue",
) -> bool:
if not video_id:
return False
self.recover_stale_for_video(video_id)
existing = self.read_claim(video_id)
if existing and self.is_claim_valid(video_id) and existing.run_id != run_id:
logger.debug("Claim denied for %s (held by %s)", video_id, existing.worker_id)
return False
path = self._path(video_id)
now = time.time()
rec = ClaimRecord(
video_id=video_id,
run_id=run_id,
worker_id=worker_id,
claimed_at=self._utc_iso(now),
expires_at=self._utc_iso(now + self.ttl_sec),
source_queue=source_queue,
)
try:
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(rec.__dict__, f, ensure_ascii=False, indent=2)
return True
except FileExistsError:
return False
except OSError as exc:
logger.warning("Claim failed for %s: %s", video_id, exc)
return False
def release(self, video_id: str) -> None:
path = self._path(video_id)
try:
if os.path.isfile(path):
os.remove(path)
except OSError as exc:
logger.debug("Release claim %s: %s", video_id, exc)
def recover_stale_for_video(self, video_id: str) -> bool:
rec = self.read_claim(video_id)
if not rec:
return False
try:
exp = datetime.strptime(rec.expires_at, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).timestamp()
if time.time() <= exp:
return False
except ValueError:
pass
self.release(video_id)
logger.info("Recovered stale claim for %s", video_id)
return True
def recover_all_stale(self) -> List[str]:
recovered = []
if not os.path.isdir(self.claims_dir):
return recovered
for name in os.listdir(self.claims_dir):
if not name.endswith(".json"):
continue
vid = name[:-5]
if self.recover_stale_for_video(vid):
recovered.append(vid)
return recovered