|
64 | 64 | EXCHANGE_NAME = os.getenv("RABBITMQ_EXCHANGE_NAME") |
65 | 65 | CAMERA_CACHE_REFRESH_SECONDS = int(os.getenv("CAMERA_CACHE_REFRESH_SECONDS", "60")) |
66 | 66 |
|
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 | | - |
71 | 67 |
|
72 | 68 | #boto3.set_stream_logger('botocore', logging.DEBUG) |
73 | 69 |
|
|
100 | 96 | last_camera_refresh = {} |
101 | 97 | image_invalid = False |
102 | 98 |
|
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(): |
110 | 100 | """ |
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. |
112 | 103 | """ |
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.") |
118 | 107 |
|
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)() |
166 | 109 |
|
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 |
233 | 116 |
|
234 | | - logger.info("Starting RabbitMQ consumer for %s", rb_url) |
| 117 | + logger.info("Starting RabbitMQ consumer.") |
235 | 118 |
|
236 | | - connection = None |
237 | | - channel = None |
| 119 | + connection: Optional[aio_pika.RobustConnection] = None |
| 120 | + channel: Optional[aio_pika.RobustChannel] = None |
238 | 121 | queue = None |
239 | 122 |
|
240 | 123 | try: |
@@ -331,42 +214,7 @@ async def run_consumer(): |
331 | 214 | if connection: |
332 | 215 | try: |
333 | 216 | 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: |
370 | 218 | logger.warning("Error closing connection: %s", e) |
371 | 219 |
|
372 | 220 | logger.info("Consumer stopped for %s.", rb_url) |
|
0 commit comments