Skip to content

Commit f5b0ef0

Browse files
committed
Merge branch 'feature/DBC22-5164-2' of https://github.com/bcgov/DriveBC.ca into feature/DBC22-5164-2
2 parents 7344a73 + f4637b3 commit f5b0ef0

1 file changed

Lines changed: 17 additions & 169 deletions

File tree

src/backend/apps/consumer/processor.py

Lines changed: 17 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@
6464
EXCHANGE_NAME = os.getenv("RABBITMQ_EXCHANGE_NAME")
6565
CAMERA_CACHE_REFRESH_SECONDS = int(os.getenv("CAMERA_CACHE_REFRESH_SECONDS", "60"))
6666

67-
RABBITMQ_HEARTBEAT = int(os.getenv("RABBITMQ_HEARTBEAT", "60"))
68-
RABBITMQ_TIMEOUT = int(os.getenv("RABBITMQ_TIMEOUT", "30"))
69-
RABBITMQ_RECONNECT_INTERVAL = int(os.getenv("RABBITMQ_RECONNECT_INTERVAL", "5"))
70-
7167

7268
#boto3.set_stream_logger('botocore', logging.DEBUG)
7369

@@ -100,141 +96,28 @@
10096
last_camera_refresh = {}
10197
image_invalid = False
10298

103-
last_activity = 0.0 # Track last message processing time
104-
health_check_interval = 60 # seconds
105-
max_idle_time = 300 # 5 minutes before warning
106-
107-
tz_pst = 'America/Vancouver'
108-
109-
async def consume_from(rb_url: str):
99+
async def run_consumer():
110100
"""
111-
Consumer instance bound to a single RabbitMQ URL.
101+
Long-running RabbitMQ consumer that processes image messages and watermarks them.
102+
Shuts down cleanly when stop_event is set.
112103
"""
113-
try:
114-
db_data = await parse_rows(rb_url)
115-
except Exception as exc:
116-
logger.error("Failed to initialize consumer for %s: %s", rb_url, exc)
117-
return
104+
rb_url = os.getenv("RABBITMQ_URL")
105+
if not rb_url:
106+
raise RuntimeError("RABBITMQ_URL environment variable is not set.")
118107

119-
async def on_reconnect(conn):
120-
logger.info("RabbitMQ connection re-established")
121-
global last_activity
122-
last_activity = time.time()
123-
124-
async def on_close(conn, exc=None):
125-
logger.warning(f"RabbitMQ connection closed: {exc}")
126-
127-
async def on_channel_close(ch, exc=None):
128-
logger.warning(f"RabbitMQ channel closed: {exc}")
129-
130-
131-
async def on_message(message: aio_pika.IncomingMessage):
132-
"""Process incoming RabbitMQ message."""
133-
if stop_event.is_set():
134-
logger.info("Stop requested. Dropping message.")
135-
136-
async with message.process(ignore_processed=True):
137-
filename = message.headers.get("filename", "unknown.jpg")
138-
camera_id = filename.split("_")[0].split(".")[0]
139-
timestamp_utc = message.headers.get("timestamp", "unknown")
140-
camera_status = calculate_camera_status(camera_id, timestamp_utc)
141-
142-
try:
143-
timestamp_local = await sync_to_async(safe_db_call)(
144-
generate_local_timestamp, db_data, camera_id, timestamp_utc
145-
)
146-
try:
147-
await asyncio.wait_for(
148-
handle_image_message(camera_id, message.body, timestamp_local, camera_status),
149-
timeout=120
150-
)
151-
except asyncio.TimeoutError:
152-
logger.error(f"Processing timeout for camera {camera_id}")
153-
154-
logger.info("Processed message for camera %s.", camera_id)
155-
except Exception as e:
156-
logger.exception("Failed processing message (camera %s): %s", camera_id, e)
157-
await asyncio.sleep(2)
158-
159-
async def wait_for_timeout_or_signal(timeout_seconds: int):
160-
"""Wait for timeout or stop signal."""
161-
try:
162-
await asyncio.wait_for(stop_event.wait(), timeout=timeout_seconds)
163-
return True # Stop signal received
164-
except asyncio.TimeoutError:
165-
return False # Timeout reached
108+
rows = await sync_to_async(get_all_from_db)()
166109

167-
async def run_consumer():
168-
"""Long-running RabbitMQ consumer with automatic reconnection."""
169-
170-
while not stop_event.is_set():
171-
connection = None
172-
channel = None
173-
queue = None
174-
175-
try:
176-
# 1. Connect to RabbitMQ (auto-reconnects on RabbitMQ restart)
177-
connection = await aio_pika.connect_robust(
178-
RABBITMQ_URL,
179-
heartbeat=RABBITMQ_HEARTBEAT,
180-
timeout=RABBITMQ_TIMEOUT,
181-
reconnect_interval=RABBITMQ_RECONNECT_INTERVAL,
182-
fail_fast=False,
183-
)
184-
logger.info("RabbitMQ connection established.")
185-
connection.reconnect_callbacks.add(on_reconnect)
186-
connection.close_callbacks.add(on_close)
187-
188-
# 2. Setup channel and queue
189-
channel = await connection.channel()
190-
logger.info("RabbitMQ channel created.")
191-
channel.close_callbacks.add(on_channel_close)
192-
193-
exchange = await channel.declare_exchange(
194-
EXCHANGE_NAME,
195-
type=aio_pika.ExchangeType.FANOUT,
196-
durable=True,
197-
)
198-
logger.info(f"RabbitMQ exchange '{EXCHANGE_NAME}' declared.")
199-
200-
# # Delete existing queue if wrong type, then recreate as quorum
201-
# try:
202-
# await channel.queue_delete(QUEUE_NAME)
203-
# logger.info(f"Deleted existing queue '{QUEUE_NAME}' for recreation")
204-
# except Exception:
205-
# pass
206-
207-
queue = await channel.declare_queue(
208-
QUEUE_NAME,
209-
durable=True,
210-
exclusive=False,
211-
auto_delete=False,
212-
arguments={
213-
"x-max-length-bytes": QUEUE_MAX_BYTES,
214-
"x-queue-type": "quorum"
215-
},
216-
)
217-
logger.info(f"RabbitMQ queue '{QUEUE_NAME}' declared.")
218-
219-
await queue.bind(exchange)
220-
logger.info(f"RabbitMQ queue '{QUEUE_NAME}' bound to exchange '{EXCHANGE_NAME}'.")
221-
222-
logger.info("Starting message consumption...")
223-
224-
# 3. Consume messages using iterator (blocks waiting for messages)
225-
async with queue.iterator() as queue_iter:
226-
async for message in queue_iter:
227-
if stop_event.is_set():
228-
logger.info("Stop requested. Breaking consume loop.")
229-
break
230-
231-
try:
232-
await process_message(message)
110+
global db_data, last_camera_refresh
111+
db_data = process_camera_rows(rows)
112+
last_camera_refresh = time.time()
113+
if not db_data:
114+
logger.error("No camera data available for watermarking. Consumer exiting.")
115+
return
233116

234-
logger.info("Starting RabbitMQ consumer for %s", rb_url)
117+
logger.info("Starting RabbitMQ consumer.")
235118

236-
connection = None
237-
channel = None
119+
connection: Optional[aio_pika.RobustConnection] = None
120+
channel: Optional[aio_pika.RobustChannel] = None
238121
queue = None
239122

240123
try:
@@ -331,42 +214,7 @@ async def run_consumer():
331214
if connection:
332215
try:
333216
await connection.close()
334-
335-
if not stop_event.is_set():
336-
logger.info(f"Waiting {RABBITMQ_RECONNECT_INTERVAL}s before reconnecting...")
337-
await asyncio.sleep(RABBITMQ_RECONNECT_INTERVAL)
338-
339-
logger.info("Consumer stopped.")
340-
341-
async def process_message(message: aio_pika.IncomingMessage):
342-
"""Process a single message from the queue."""
343-
async with message.process(ignore_processed=True):
344-
filename = message.headers.get("filename", "unknown.jpg")
345-
camera_id = filename.split("_")[0].split(".")[0]
346-
timestamp_utc = message.headers.get("timestamp", "unknown")
347-
camera_status = await sync_to_async(calculate_camera_status)(camera_id, timestamp_utc)
348-
349-
timestamp_local = await sync_to_async(safe_db_call)(
350-
generate_local_timestamp, db_data, camera_id, timestamp_utc
351-
)
352-
353-
try:
354-
await asyncio.wait_for(
355-
handle_image_message(camera_id, message.body, timestamp_local, camera_status),
356-
timeout=120
357-
)
358-
except asyncio.TimeoutError:
359-
logger.error(f"Processing timeout for camera {camera_id}")
360-
361-
logger.info("Processed message for camera %s.", camera_id)
362-
363-
def safe_db_call(func, *args, **kwargs):
364-
"""Call a sync ORM function safely after dropping dead connections"""
365-
close_old_connections()
366-
# This forces Django to open a fresh connection if needed
367-
connection.ensure_connection()
368-
return func(*args, **kwargs)
369-
except Exception as e:
217+
except Exception as e:
370218
logger.warning("Error closing connection: %s", e)
371219

372220
logger.info("Consumer stopped for %s.", rb_url)

0 commit comments

Comments
 (0)