forked from vx6Fid/VectorizeDocs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpython_worker.py
More file actions
162 lines (131 loc) · 4.72 KB
/
Copy pathpython_worker.py
File metadata and controls
162 lines (131 loc) · 4.72 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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import asyncio
import json
import os
import aio_pika
import psycopg2
from aio_pika.abc import AbstractIncomingMessage
from processor import process_single_tender
from utils.config import RABBIT_URL
POSTGRES_DSN = os.getenv("POSTGRES_CONN_STRING")
MAX_ATTEMPTS = 5 # keep synced with your Go maxAttempts constant
def connect_db():
# Consider replacing with a connection pool for higher throughput.
return psycopg2.connect(POSTGRES_DSN)
def claim_job(conn, job_id):
"""
Atomically claim a job. Returns tuple (payload_json, attempts) or None.
"""
with conn.cursor() as cur:
cur.execute(
"""
UPDATE jobs
SET status='running_python', attempts = attempts + 1, updated_at=NOW()
WHERE id=%s AND status='pending_python'
RETURNING payload, attempts;
""",
(job_id,),
)
row = cur.fetchone()
conn.commit()
return row # None if not claimable
def reset_job_to_pending(conn, job_id):
with conn.cursor() as cur:
cur.execute(
"""
UPDATE jobs
SET status='pending_python', updated_at=NOW()
WHERE id=%s
""",
(job_id,),
)
conn.commit()
def complete_job(conn, job_id, python_result):
with conn.cursor() as cur:
cur.execute(
"""
UPDATE jobs
SET python_result=%s::jsonb,
status='completed',
updated_at=NOW()
WHERE id=%s;
""",
(json.dumps(python_result), job_id),
)
conn.commit()
def fail_job(conn, job_id, error_msg):
with conn.cursor() as cur:
cur.execute(
"""
UPDATE jobs
SET status='failed',
last_error=%s,
updated_at=NOW()
WHERE id=%s;
""",
(error_msg, job_id),
)
conn.commit()
async def on_message(message: AbstractIncomingMessage):
"""
NOTE: we use message.process() context manager which will ACK the message if the block
exits normally and will NACK (requeue) if an exception is raised. To get correct retry
behaviour we handle DB state explicitly inside the block and re-raise when we want the
broker to retry the message.
"""
async with message.process():
job_msg = json.loads(message.body)
job_id = job_msg["job_id"]
conn = connect_db()
claimed = claim_job(conn, job_id)
if not claimed:
conn.close()
# Nothing to do: job not pending_python, just ack and drop
return
payload_json, attempts = claimed
# Payload from Postgres may be a string (JSON). Ensure we have a dict.
if isinstance(payload_json, (bytes, str)):
try:
payload = json.loads(payload_json)
except Exception:
# invalid payload stored in DB: mark job failed and ack message
fail_job(conn, job_id, "invalid payload JSON")
conn.close()
return
else:
payload = payload_json
try:
# process_single_tender can perform blocking work; ensure it runs without blocking the event loop
python_result = await process_single_tender(payload)
complete_job(conn, job_id, python_result)
print(f"[python-worker] DONE job_id={job_id}")
conn.close()
return
except Exception as e:
# Decide whether to permanently fail or requeue for retry
err_str = str(e)
if attempts >= MAX_ATTEMPTS:
# mark failed and ACK (by returning without raising)
fail_job(conn, job_id, err_str)
conn.close()
return
else:
# reset job state to pending_python so the message will be claimable again
# then re-raise to NACK and allow RabbitMQ to redeliver after its retry/backoff
try:
reset_job_to_pending(conn, job_id)
finally:
conn.close()
# Re-raise so aio_pika will NACK the message and broker can redeliver
raise
async def start_worker():
print("Connecting to RabbitMQ...")
connection = await aio_pika.connect_robust(RABBIT_URL)
channel = await connection.channel()
await channel.set_qos(prefetch_count=1)
queue = await channel.declare_queue("jobs.python", durable=True)
print("Python async worker started. Waiting for messages...")
await queue.consume(on_message)
# Keep the worker alive forever
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(start_worker())