nebu 0.1.91__py3-none-any.whl → 0.1.93__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nebu/cache.py +15 -11
- nebu/containers/container.py +13 -10
- nebu/data.py +112 -92
- nebu/logging.py +33 -0
- nebu/namespaces/namespace.py +7 -4
- nebu/processors/consumer.py +184 -152
- nebu/processors/consumer_process_worker.py +179 -96
- nebu/processors/decorate.py +226 -223
- nebu/processors/processor.py +38 -28
- {nebu-0.1.91.dist-info → nebu-0.1.93.dist-info}/METADATA +2 -1
- nebu-0.1.93.dist-info/RECORD +28 -0
- nebu/containers/decorator.py +0 -93
- nebu/containers/server.py +0 -70
- nebu/processors/remote.py +0 -47
- nebu-0.1.91.dist-info/RECORD +0 -30
- {nebu-0.1.91.dist-info → nebu-0.1.93.dist-info}/WHEEL +0 -0
- {nebu-0.1.91.dist-info → nebu-0.1.93.dist-info}/licenses/LICENSE +0 -0
- {nebu-0.1.91.dist-info → nebu-0.1.93.dist-info}/top_level.txt +0 -0
nebu/processors/consumer.py
CHANGED
@@ -17,6 +17,7 @@ import socks
|
|
17
17
|
from redis import ConnectionError, ResponseError
|
18
18
|
|
19
19
|
from nebu.errors import RetriableError
|
20
|
+
from nebu.logging import logger
|
20
21
|
|
21
22
|
# Define TypeVar for generic models
|
22
23
|
T = TypeVar("T")
|
@@ -38,12 +39,12 @@ NEBU_EXECUTION_MODE = os.environ.get("NEBU_EXECUTION_MODE", "inline").lower()
|
|
38
39
|
execution_mode = NEBU_EXECUTION_MODE
|
39
40
|
|
40
41
|
if execution_mode not in ["inline", "subprocess"]:
|
41
|
-
|
42
|
+
logger.warning(
|
42
43
|
f"Invalid NEBU_EXECUTION_MODE: {NEBU_EXECUTION_MODE}. Must be 'inline' or 'subprocess'. Defaulting to 'inline'."
|
43
44
|
)
|
44
45
|
execution_mode = "inline"
|
45
46
|
|
46
|
-
|
47
|
+
logger.info(f"Execution mode: {execution_mode}")
|
47
48
|
|
48
49
|
|
49
50
|
# --- Function to Load/Reload User Code ---
|
@@ -69,16 +70,18 @@ def load_or_reload_user_code(
|
|
69
70
|
loaded_module = None
|
70
71
|
exec_namespace: Dict[str, Any] = {} # Use a local namespace for this load attempt
|
71
72
|
|
72
|
-
|
73
|
+
logger.info(f"[Code Loader] Attempting to load/reload module: '{module_path}'")
|
73
74
|
os.environ[_NEBU_INSIDE_CONSUMER_ENV_VAR] = "1" # Set guard *before* import/reload
|
74
|
-
|
75
|
+
logger.debug(
|
76
|
+
f"[Code Loader] Set environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}=1"
|
77
|
+
)
|
75
78
|
|
76
79
|
try:
|
77
80
|
current_mtime = os.path.getmtime(entrypoint_abs_path)
|
78
81
|
|
79
82
|
# Execute included object sources FIRST (if any)
|
80
83
|
if included_object_sources:
|
81
|
-
|
84
|
+
logger.debug("[Code Loader] Executing @include object sources...")
|
82
85
|
# Include necessary imports for the exec context
|
83
86
|
exec("from pydantic import BaseModel, Field", exec_namespace)
|
84
87
|
exec(
|
@@ -92,28 +95,34 @@ def load_or_reload_user_code(
|
|
92
95
|
for i, (obj_source, args_sources) in enumerate(included_object_sources):
|
93
96
|
try:
|
94
97
|
exec(obj_source, exec_namespace)
|
95
|
-
|
98
|
+
logger.debug(
|
96
99
|
f"[Code Loader] Successfully executed included object {i} base source"
|
97
100
|
)
|
98
101
|
for j, arg_source in enumerate(args_sources):
|
99
102
|
try:
|
100
103
|
exec(arg_source, exec_namespace)
|
101
|
-
|
104
|
+
logger.debug(
|
102
105
|
f"[Code Loader] Successfully executed included object {i} arg {j} source"
|
103
106
|
)
|
104
107
|
except Exception as e_arg:
|
105
|
-
|
108
|
+
logger.error(
|
106
109
|
f"Error executing included object {i} arg {j} source: {e_arg}"
|
107
110
|
)
|
108
|
-
|
111
|
+
logger.exception(
|
112
|
+
f"Traceback for included object {i} arg {j} source error:"
|
113
|
+
)
|
109
114
|
except Exception as e_base:
|
110
|
-
|
111
|
-
|
112
|
-
|
115
|
+
logger.error(
|
116
|
+
f"Error executing included object {i} base source: {e_base}"
|
117
|
+
)
|
118
|
+
logger.exception(
|
119
|
+
f"Traceback for included object {i} base source error:"
|
120
|
+
)
|
121
|
+
logger.debug("[Code Loader] Finished executing included object sources.")
|
113
122
|
|
114
123
|
# Check if module is already loaded and needs reload
|
115
124
|
if module_path in sys.modules:
|
116
|
-
|
125
|
+
logger.info(
|
117
126
|
f"[Code Loader] Module '{module_path}' already imported. Reloading..."
|
118
127
|
)
|
119
128
|
# Pass the exec_namespace as globals? Usually reload works within its own context.
|
@@ -121,32 +130,34 @@ def load_or_reload_user_code(
|
|
121
130
|
# reload might not pick that up easily. Might need a fresh import instead.
|
122
131
|
# Let's try reload first.
|
123
132
|
loaded_module = importlib.reload(sys.modules[module_path])
|
124
|
-
|
133
|
+
logger.info(f"[Code Loader] Successfully reloaded module: {module_path}")
|
125
134
|
else:
|
126
135
|
# Import the main module
|
127
136
|
loaded_module = importlib.import_module(module_path)
|
128
|
-
|
137
|
+
logger.info(
|
129
138
|
f"[Code Loader] Successfully imported module for the first time: {module_path}"
|
130
139
|
)
|
131
140
|
|
132
141
|
# Get the target function from the loaded/reloaded module
|
133
142
|
loaded_target_func = getattr(loaded_module, function_name)
|
134
|
-
|
143
|
+
logger.info(
|
135
144
|
f"[Code Loader] Successfully loaded function '{function_name}' from module '{module_path}'"
|
136
145
|
)
|
137
146
|
|
138
147
|
# Get the init function if specified
|
139
148
|
if init_func_name:
|
140
149
|
loaded_init_func = getattr(loaded_module, init_func_name)
|
141
|
-
|
150
|
+
logger.info(
|
142
151
|
f"[Code Loader] Successfully loaded init function '{init_func_name}' from module '{module_path}'"
|
143
152
|
)
|
144
153
|
# Execute init_func
|
145
|
-
|
154
|
+
logger.info(f"[Code Loader] Executing init_func: {init_func_name}...")
|
146
155
|
loaded_init_func() # Call the function
|
147
|
-
|
156
|
+
logger.info(
|
157
|
+
f"[Code Loader] Successfully executed init_func: {init_func_name}"
|
158
|
+
)
|
148
159
|
|
149
|
-
|
160
|
+
logger.info("[Code Loader] Code load/reload successful.")
|
150
161
|
return (
|
151
162
|
loaded_target_func,
|
152
163
|
loaded_init_func,
|
@@ -156,37 +167,39 @@ def load_or_reload_user_code(
|
|
156
167
|
)
|
157
168
|
|
158
169
|
except FileNotFoundError:
|
159
|
-
|
170
|
+
logger.error(
|
160
171
|
f"[Code Loader] Error: Entrypoint file not found at '{entrypoint_abs_path}'. Cannot load/reload."
|
161
172
|
)
|
162
173
|
return None, None, None, {}, 0.0 # Indicate failure
|
163
174
|
except ImportError as e:
|
164
|
-
|
165
|
-
|
175
|
+
logger.error(
|
176
|
+
f"[Code Loader] Error importing/reloading module '{module_path}': {e}"
|
177
|
+
)
|
178
|
+
logger.exception("Import/Reload Error Traceback:")
|
166
179
|
return None, None, None, {}, 0.0 # Indicate failure
|
167
180
|
except AttributeError as e:
|
168
|
-
|
181
|
+
logger.error(
|
169
182
|
f"[Code Loader] Error accessing function '{function_name}' or '{init_func_name}' in module '{module_path}': {e}"
|
170
183
|
)
|
171
|
-
|
184
|
+
logger.exception("Attribute Error Traceback:")
|
172
185
|
return None, None, None, {}, 0.0 # Indicate failure
|
173
186
|
except Exception as e:
|
174
|
-
|
175
|
-
|
187
|
+
logger.error(f"[Code Loader] Unexpected error during code load/reload: {e}")
|
188
|
+
logger.exception("Unexpected Code Load/Reload Error Traceback:")
|
176
189
|
return None, None, None, {}, 0.0 # Indicate failure
|
177
190
|
finally:
|
178
191
|
# Unset the guard environment variable
|
179
192
|
os.environ.pop(_NEBU_INSIDE_CONSUMER_ENV_VAR, None)
|
180
|
-
|
193
|
+
logger.debug(
|
181
194
|
f"[Code Loader] Unset environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}"
|
182
195
|
)
|
183
196
|
|
184
197
|
|
185
198
|
# Print all environment variables before starting
|
186
|
-
|
199
|
+
logger.debug("===== ENVIRONMENT VARIABLES =====")
|
187
200
|
for key, value in sorted(os.environ.items()):
|
188
|
-
|
189
|
-
|
201
|
+
logger.debug(f"{key}={value}")
|
202
|
+
logger.debug("=================================")
|
190
203
|
|
191
204
|
# --- Get Environment Variables ---
|
192
205
|
try:
|
@@ -224,7 +237,7 @@ try:
|
|
224
237
|
break
|
225
238
|
|
226
239
|
if not _function_name or not _entrypoint_rel_path:
|
227
|
-
|
240
|
+
logger.critical(
|
228
241
|
"FATAL: FUNCTION_NAME or NEBU_ENTRYPOINT_MODULE_PATH environment variables not set"
|
229
242
|
)
|
230
243
|
sys.exit(1)
|
@@ -242,12 +255,12 @@ try:
|
|
242
255
|
if os.path.exists(potential_path):
|
243
256
|
entrypoint_abs_path = potential_path
|
244
257
|
found_path = True
|
245
|
-
|
258
|
+
logger.info(
|
246
259
|
f"[Consumer] Found entrypoint absolute path via PYTHONPATH: {entrypoint_abs_path}"
|
247
260
|
)
|
248
261
|
break
|
249
262
|
if not found_path:
|
250
|
-
|
263
|
+
logger.critical(
|
251
264
|
f"FATAL: Could not find entrypoint file via relative path '{_entrypoint_rel_path}' or in PYTHONPATH."
|
252
265
|
)
|
253
266
|
# Attempting abspath anyway for the error message in load function
|
@@ -260,17 +273,17 @@ try:
|
|
260
273
|
if _module_path.endswith(".__init__"):
|
261
274
|
_module_path = _module_path[: -len(".__init__")]
|
262
275
|
elif _module_path == "__init__":
|
263
|
-
|
276
|
+
logger.critical(
|
264
277
|
f"FATAL: Entrypoint '{_entrypoint_rel_path}' resolves to ambiguous top-level __init__. Please use a named file or package."
|
265
278
|
)
|
266
279
|
sys.exit(1)
|
267
280
|
if not _module_path:
|
268
|
-
|
281
|
+
logger.critical(
|
269
282
|
f"FATAL: Could not derive a valid module path from entrypoint '{_entrypoint_rel_path}'"
|
270
283
|
)
|
271
284
|
sys.exit(1)
|
272
285
|
|
273
|
-
|
286
|
+
logger.info(
|
274
287
|
f"[Consumer] Initializing. Entrypoint: '{_entrypoint_rel_path}', Module: '{_module_path}', Function: '{_function_name}', Init: '{_init_func_name}'"
|
275
288
|
)
|
276
289
|
|
@@ -290,30 +303,30 @@ try:
|
|
290
303
|
)
|
291
304
|
|
292
305
|
if target_function is None or imported_module is None:
|
293
|
-
|
306
|
+
logger.critical("FATAL: Initial load of user code failed. Exiting.")
|
294
307
|
sys.exit(1)
|
295
|
-
|
308
|
+
logger.info(
|
296
309
|
f"[Consumer] Initial code load successful. Last modified time: {last_load_mtime}"
|
297
310
|
)
|
298
311
|
|
299
312
|
|
300
313
|
except Exception as e:
|
301
|
-
|
302
|
-
|
314
|
+
logger.critical(f"FATAL: Error during initial environment setup or code load: {e}")
|
315
|
+
logger.exception("Initial Setup/Load Error Traceback:")
|
303
316
|
sys.exit(1)
|
304
317
|
|
305
318
|
# Get Redis connection parameters from environment
|
306
319
|
REDIS_URL = os.environ.get("REDIS_URL", "")
|
307
320
|
|
308
321
|
if not all([REDIS_URL, REDIS_CONSUMER_GROUP, REDIS_STREAM]):
|
309
|
-
|
322
|
+
logger.critical("Missing required Redis environment variables")
|
310
323
|
sys.exit(1)
|
311
324
|
|
312
325
|
# Configure SOCKS proxy before connecting to Redis
|
313
326
|
# Use the proxy settings provided by tailscaled
|
314
327
|
socks.set_default_proxy(socks.SOCKS5, "localhost", 1055)
|
315
328
|
socket.socket = socks.socksocket
|
316
|
-
|
329
|
+
logger.info("Configured SOCKS5 proxy for socket connections via localhost:1055")
|
317
330
|
|
318
331
|
# Connect to Redis
|
319
332
|
try:
|
@@ -324,10 +337,10 @@ try:
|
|
324
337
|
) # Added decode_responses for convenience
|
325
338
|
r.ping() # Test connection
|
326
339
|
redis_info = REDIS_URL.split("@")[-1] if "@" in REDIS_URL else REDIS_URL
|
327
|
-
|
340
|
+
logger.info(f"Connected to Redis via SOCKS proxy at {redis_info}")
|
328
341
|
except Exception as e:
|
329
|
-
|
330
|
-
|
342
|
+
logger.critical(f"Failed to connect to Redis via SOCKS proxy: {e}")
|
343
|
+
logger.exception("Redis Connection Error Traceback:")
|
331
344
|
sys.exit(1)
|
332
345
|
|
333
346
|
# Create consumer group if it doesn't exist
|
@@ -336,13 +349,15 @@ try:
|
|
336
349
|
assert isinstance(REDIS_STREAM, str)
|
337
350
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
338
351
|
r.xgroup_create(REDIS_STREAM, REDIS_CONSUMER_GROUP, id="0", mkstream=True)
|
339
|
-
|
352
|
+
logger.info(
|
353
|
+
f"Created consumer group {REDIS_CONSUMER_GROUP} for stream {REDIS_STREAM}"
|
354
|
+
)
|
340
355
|
except ResponseError as e:
|
341
356
|
if "BUSYGROUP" in str(e):
|
342
|
-
|
357
|
+
logger.info(f"Consumer group {REDIS_CONSUMER_GROUP} already exists")
|
343
358
|
else:
|
344
|
-
|
345
|
-
|
359
|
+
logger.error(f"Error creating consumer group: {e}")
|
360
|
+
logger.exception("Consumer Group Creation Error Traceback:")
|
346
361
|
|
347
362
|
|
348
363
|
# Function to process messages
|
@@ -353,16 +368,16 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
353
368
|
|
354
369
|
# --- Subprocess Execution Path ---
|
355
370
|
if execution_mode == "subprocess":
|
356
|
-
|
371
|
+
logger.info(f"Processing message {message_id} in subprocess...")
|
357
372
|
process = None # Initialize process variable
|
358
373
|
|
359
374
|
# Helper function to read and print stream lines
|
360
375
|
def stream_reader(stream: IO[str], prefix: str):
|
361
376
|
try:
|
362
377
|
for line in iter(stream.readline, ""):
|
363
|
-
|
378
|
+
logger.debug(f"{prefix}: {line.strip()}")
|
364
379
|
except Exception as e:
|
365
|
-
|
380
|
+
logger.error(f"Error reading stream {prefix}: {e}")
|
366
381
|
finally:
|
367
382
|
stream.close()
|
368
383
|
|
@@ -410,12 +425,12 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
410
425
|
process.stdin.close() # Signal end of input
|
411
426
|
except (BrokenPipeError, OSError) as e:
|
412
427
|
# Handle cases where the process might have exited early
|
413
|
-
|
428
|
+
logger.warning(
|
414
429
|
f"Warning: Failed to write full input to subprocess {message_id}: {e}. It might have exited prematurely."
|
415
430
|
)
|
416
431
|
# Continue to wait and check return code
|
417
432
|
else:
|
418
|
-
|
433
|
+
logger.error(
|
419
434
|
f"Error: Subprocess stdin stream not available for {message_id}. Cannot send input."
|
420
435
|
)
|
421
436
|
# Handle this case - perhaps terminate and report error?
|
@@ -431,19 +446,19 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
431
446
|
stderr_thread.join()
|
432
447
|
|
433
448
|
if return_code == 0:
|
434
|
-
|
449
|
+
logger.info(
|
435
450
|
f"Subprocess for {message_id} completed successfully (return code 0)."
|
436
451
|
)
|
437
452
|
# Assume success handling (ack/response) was done by the worker
|
438
453
|
elif return_code == 3:
|
439
|
-
|
454
|
+
logger.warning(
|
440
455
|
f"Subprocess for {message_id} reported a retriable error (exit code 3). Message will not be acknowledged."
|
441
456
|
)
|
442
457
|
# Optionally send an error response here, though the worker already did.
|
443
458
|
# _send_error_response(...)
|
444
459
|
# DO NOT Acknowledge the message here, let it be retried.
|
445
460
|
else:
|
446
|
-
|
461
|
+
logger.error(
|
447
462
|
f"Subprocess for {message_id} failed with exit code {return_code}."
|
448
463
|
)
|
449
464
|
# Worker likely failed, send generic error and ACK here
|
@@ -459,14 +474,14 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
459
474
|
assert isinstance(REDIS_STREAM, str)
|
460
475
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
461
476
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
462
|
-
|
477
|
+
logger.info(f"Acknowledged failed subprocess message {message_id}")
|
463
478
|
except Exception as e_ack:
|
464
|
-
|
479
|
+
logger.critical(
|
465
480
|
f"CRITICAL: Failed to acknowledge failed subprocess message {message_id}: {e_ack}"
|
466
481
|
)
|
467
482
|
|
468
483
|
except FileNotFoundError:
|
469
|
-
|
484
|
+
logger.critical(
|
470
485
|
"FATAL: Worker script 'nebu.processors.consumer_process_worker' not found. Check PYTHONPATH."
|
471
486
|
)
|
472
487
|
# Send error and ack if possible
|
@@ -481,19 +496,19 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
481
496
|
assert isinstance(REDIS_STREAM, str)
|
482
497
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
483
498
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
484
|
-
|
499
|
+
logger.info(
|
485
500
|
f"Acknowledged message {message_id} after worker script not found failure"
|
486
501
|
)
|
487
502
|
except Exception as e_ack:
|
488
|
-
|
503
|
+
logger.critical(
|
489
504
|
f"CRITICAL: Failed to acknowledge message {message_id} after worker script not found failure: {e_ack}"
|
490
505
|
)
|
491
506
|
|
492
507
|
except Exception as e:
|
493
|
-
|
508
|
+
logger.error(
|
494
509
|
f"Error launching or managing subprocess for message {message_id}: {e}"
|
495
510
|
)
|
496
|
-
|
511
|
+
logger.exception("Subprocess Launch/Manage Error Traceback:")
|
497
512
|
# Also send an error and acknowledge
|
498
513
|
_send_error_response(
|
499
514
|
message_id,
|
@@ -506,22 +521,22 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
506
521
|
assert isinstance(REDIS_STREAM, str)
|
507
522
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
508
523
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
509
|
-
|
524
|
+
logger.info(
|
510
525
|
f"Acknowledged message {message_id} after subprocess launch/manage failure"
|
511
526
|
)
|
512
527
|
except Exception as e_ack:
|
513
|
-
|
528
|
+
logger.critical(
|
514
529
|
f"CRITICAL: Failed to acknowledge message {message_id} after subprocess launch/manage failure: {e_ack}"
|
515
530
|
)
|
516
531
|
# Ensure process is terminated if it's still running after an error
|
517
532
|
if process and process.poll() is None:
|
518
|
-
|
533
|
+
logger.warning(
|
519
534
|
f"Terminating potentially lingering subprocess for {message_id}..."
|
520
535
|
)
|
521
536
|
process.terminate()
|
522
537
|
process.wait(timeout=5) # Give it a moment to terminate
|
523
538
|
if process.poll() is None:
|
524
|
-
|
539
|
+
logger.warning(
|
525
540
|
f"Subprocess for {message_id} did not terminate gracefully, killing."
|
526
541
|
)
|
527
542
|
process.kill()
|
@@ -549,7 +564,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
549
564
|
|
550
565
|
# --- Inline Execution Path (Original Logic) ---
|
551
566
|
if target_function is None or imported_module is None:
|
552
|
-
|
567
|
+
logger.error(
|
553
568
|
f"Error processing message {message_id}: User code (target_function or module) is not loaded. Skipping."
|
554
569
|
)
|
555
570
|
_send_error_response(
|
@@ -564,9 +579,11 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
564
579
|
assert isinstance(REDIS_STREAM, str)
|
565
580
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
566
581
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
567
|
-
|
582
|
+
logger.warning(
|
583
|
+
f"Acknowledged message {message_id} due to code load failure."
|
584
|
+
)
|
568
585
|
except Exception as e_ack:
|
569
|
-
|
586
|
+
logger.critical(
|
570
587
|
f"CRITICAL: Failed to acknowledge message {message_id} after code load failure: {e_ack}"
|
571
588
|
)
|
572
589
|
return # Skip processing
|
@@ -590,7 +607,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
590
607
|
f"Expected parsed payload to be a dictionary, but got {type(raw_payload)}"
|
591
608
|
)
|
592
609
|
|
593
|
-
|
610
|
+
logger.debug(f">> Raw payload: {raw_payload}")
|
594
611
|
|
595
612
|
kind = raw_payload.get("kind", "")
|
596
613
|
msg_id = raw_payload.get("id", "")
|
@@ -613,11 +630,11 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
613
630
|
handle = raw_payload.get("handle")
|
614
631
|
adapter = raw_payload.get("adapter")
|
615
632
|
api_key = raw_payload.get("api_key")
|
616
|
-
|
633
|
+
logger.debug(f">> Extracted API key length: {len(api_key) if api_key else 0}")
|
617
634
|
|
618
635
|
# --- Health Check Logic (Keep as is) ---
|
619
636
|
if kind == "HealthCheck":
|
620
|
-
|
637
|
+
logger.info(f"Received HealthCheck message {message_id}")
|
621
638
|
health_response = {
|
622
639
|
"kind": "StreamResponseMessage", # Respond with a standard message kind
|
623
640
|
"id": message_id,
|
@@ -630,13 +647,13 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
630
647
|
# Assert type again closer to usage for type checker clarity
|
631
648
|
assert isinstance(return_stream, str)
|
632
649
|
r.xadd(return_stream, {"data": json.dumps(health_response)})
|
633
|
-
|
650
|
+
logger.info(f"Sent health check response to {return_stream}")
|
634
651
|
|
635
652
|
# Assert types again closer to usage for type checker clarity
|
636
653
|
assert isinstance(REDIS_STREAM, str)
|
637
654
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
638
655
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
639
|
-
|
656
|
+
logger.info(f"Acknowledged HealthCheck message {message_id}")
|
640
657
|
return # Exit early for health checks
|
641
658
|
# --- End Health Check Logic ---
|
642
659
|
|
@@ -649,7 +666,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
649
666
|
else:
|
650
667
|
content = content_raw
|
651
668
|
|
652
|
-
print(f"Content: {content}")
|
669
|
+
# print(f"Content: {content}")
|
653
670
|
|
654
671
|
# --- Construct Input Object using Imported Types ---
|
655
672
|
input_obj: Any = None
|
@@ -677,24 +694,26 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
677
694
|
# Check in local_namespace from included objects as fallback
|
678
695
|
content_model_class = local_namespace.get(content_type_name)
|
679
696
|
if content_model_class is None:
|
680
|
-
|
697
|
+
logger.warning(
|
681
698
|
f"Warning: Content type class '{content_type_name}' not found in imported module or includes."
|
682
699
|
)
|
683
700
|
else:
|
684
|
-
|
701
|
+
logger.debug(
|
702
|
+
f"Found content model class: {content_model_class}"
|
703
|
+
)
|
685
704
|
except AttributeError:
|
686
|
-
|
705
|
+
logger.warning(
|
687
706
|
f"Warning: Content type class '{content_type_name}' not found in imported module."
|
688
707
|
)
|
689
708
|
except Exception as e:
|
690
|
-
|
709
|
+
logger.warning(
|
691
710
|
f"Warning: Error resolving content type class '{content_type_name}': {e}"
|
692
711
|
)
|
693
712
|
|
694
713
|
if content_model_class:
|
695
714
|
try:
|
696
715
|
content_model = content_model_class.model_validate(content)
|
697
|
-
print(f"Validated content model: {content_model}")
|
716
|
+
# print(f"Validated content model: {content_model}")
|
698
717
|
input_obj = message_class(
|
699
718
|
kind=kind,
|
700
719
|
id=msg_id,
|
@@ -708,7 +727,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
708
727
|
api_key=api_key,
|
709
728
|
)
|
710
729
|
except Exception as e:
|
711
|
-
|
730
|
+
logger.error(
|
712
731
|
f"Error validating/creating content model '{content_type_name}': {e}. Falling back."
|
713
732
|
)
|
714
733
|
# Fallback to raw content in Message
|
@@ -754,53 +773,54 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
754
773
|
input_type_class = local_namespace.get(param_type_name)
|
755
774
|
if input_type_class is None:
|
756
775
|
if param_type_name: # Only warn if a name was expected
|
757
|
-
|
776
|
+
logger.warning(
|
758
777
|
f"Warning: Input type class '{param_type_name}' not found. Passing raw content."
|
759
778
|
)
|
760
779
|
input_obj = content
|
761
780
|
else:
|
762
|
-
|
781
|
+
logger.debug(f"Found input model class: {input_type_class}")
|
763
782
|
input_obj = input_type_class.model_validate(content)
|
764
|
-
|
783
|
+
logger.debug(f"Validated input model: {input_obj}")
|
765
784
|
except AttributeError:
|
766
|
-
|
785
|
+
logger.warning(
|
767
786
|
f"Warning: Input type class '{param_type_name}' not found in imported module."
|
768
787
|
)
|
769
788
|
input_obj = content
|
770
789
|
except Exception as e:
|
771
|
-
|
790
|
+
logger.error(
|
772
791
|
f"Error resolving/validating input type '{param_type_name}': {e}. Passing raw content."
|
773
792
|
)
|
774
793
|
input_obj = content
|
775
794
|
|
776
795
|
except NameError as e:
|
777
|
-
|
796
|
+
logger.error(
|
778
797
|
f"Error: Required class (e.g., Message or parameter type) not found. Import failed? {e}"
|
779
798
|
)
|
780
799
|
# Can't proceed without types, re-raise or handle error response
|
781
800
|
raise RuntimeError(f"Required class not found: {e}") from e
|
782
801
|
except Exception as e:
|
783
|
-
|
802
|
+
logger.error(f"Error constructing input object: {e}")
|
784
803
|
raise # Re-raise unexpected errors during input construction
|
785
804
|
|
786
805
|
# print(f"Input object: {input_obj}") # Reduce verbosity
|
806
|
+
# logger.debug(f"Input object: {input_obj}") # Could use logger.debug if needed
|
787
807
|
|
788
808
|
# Execute the function
|
789
|
-
|
809
|
+
logger.info("Executing function...")
|
790
810
|
result = target_function(input_obj)
|
791
|
-
#
|
811
|
+
# logger.debug(f"Raw Result: {result}") # Debugging
|
792
812
|
|
793
813
|
result_content = None # Default to None
|
794
814
|
if result is not None: # Only process if there's a result
|
795
815
|
try:
|
796
816
|
if hasattr(result, "model_dump"):
|
797
|
-
|
817
|
+
logger.debug("[Consumer] Result has model_dump, using it.")
|
798
818
|
# Use 'json' mode to ensure serializability where possible
|
799
819
|
result_content = result.model_dump(mode="json")
|
800
|
-
#
|
820
|
+
# logger.debug(f"[Consumer] Result after model_dump: {result_content}") # Debugging
|
801
821
|
else:
|
802
822
|
# Try standard json.dumps as a fallback to check serializability
|
803
|
-
|
823
|
+
logger.debug(
|
804
824
|
"[Consumer] Result has no model_dump, attempting json.dumps check."
|
805
825
|
)
|
806
826
|
try:
|
@@ -808,9 +828,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
808
828
|
json.dumps(result)
|
809
829
|
# If the above line doesn't raise TypeError, assign the original result
|
810
830
|
result_content = result
|
811
|
-
#
|
831
|
+
# logger.debug(f"[Consumer] Result assigned directly after json.dumps check passed: {result_content}") # Debugging
|
812
832
|
except TypeError as e:
|
813
|
-
|
833
|
+
logger.warning(
|
814
834
|
f"[Consumer] Warning: Result is not JSON serializable: {e}. Discarding result."
|
815
835
|
)
|
816
836
|
result_content = None # Explicitly set to None on failure
|
@@ -818,10 +838,10 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
818
838
|
except (
|
819
839
|
Exception
|
820
840
|
) as e: # Catch other potential model_dump errors or unexpected issues
|
821
|
-
|
841
|
+
logger.warning(
|
822
842
|
f"[Consumer] Warning: Unexpected error during result processing/serialization: {e}. Discarding result."
|
823
843
|
)
|
824
|
-
|
844
|
+
logger.exception("Result Processing/Serialization Error Traceback:")
|
825
845
|
result_content = None
|
826
846
|
|
827
847
|
# Prepare the response (ensure 'content' key exists even if None)
|
@@ -840,7 +860,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
840
860
|
if return_stream:
|
841
861
|
assert isinstance(return_stream, str)
|
842
862
|
r.xadd(return_stream, {"data": json.dumps(response)})
|
843
|
-
|
863
|
+
logger.info(
|
864
|
+
f"Processed message {message_id}, result sent to {return_stream}"
|
865
|
+
)
|
844
866
|
|
845
867
|
# Acknowledge the message
|
846
868
|
assert isinstance(REDIS_STREAM, str)
|
@@ -848,17 +870,17 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
848
870
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
849
871
|
|
850
872
|
except RetriableError as e:
|
851
|
-
|
852
|
-
|
873
|
+
logger.warning(f"Retriable error processing message {message_id}: {e}")
|
874
|
+
logger.exception("Retriable Error Traceback:")
|
853
875
|
_send_error_response(
|
854
876
|
message_id, str(e), traceback.format_exc(), return_stream, user_id
|
855
877
|
)
|
856
878
|
# DO NOT Acknowledge the message for retriable errors
|
857
|
-
|
879
|
+
logger.info(f"Message {message_id} will be retried later.")
|
858
880
|
|
859
881
|
except Exception as e:
|
860
|
-
|
861
|
-
|
882
|
+
logger.error(f"Error processing message {message_id}: {e}")
|
883
|
+
logger.exception("Message Processing Error Traceback:")
|
862
884
|
_send_error_response(
|
863
885
|
message_id, str(e), traceback.format_exc(), return_stream, user_id
|
864
886
|
)
|
@@ -868,9 +890,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
868
890
|
assert isinstance(REDIS_STREAM, str)
|
869
891
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
870
892
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
871
|
-
|
893
|
+
logger.info(f"Acknowledged failed message {message_id}")
|
872
894
|
except Exception as e_ack:
|
873
|
-
|
895
|
+
logger.critical(
|
874
896
|
f"CRITICAL: Failed to acknowledge failed message {message_id}: {e_ack}"
|
875
897
|
)
|
876
898
|
|
@@ -905,16 +927,20 @@ def _send_error_response(
|
|
905
927
|
try:
|
906
928
|
assert isinstance(error_destination, str)
|
907
929
|
r.xadd(error_destination, {"data": json.dumps(error_response)})
|
908
|
-
|
930
|
+
logger.info(
|
931
|
+
f"Sent error response for message {message_id} to {error_destination}"
|
932
|
+
)
|
909
933
|
except Exception as e_redis:
|
910
|
-
|
934
|
+
logger.critical(
|
911
935
|
f"CRITICAL: Failed to send error response for {message_id} to Redis: {e_redis}"
|
912
936
|
)
|
913
|
-
|
937
|
+
logger.exception("Redis Error Response Send Error Traceback:")
|
914
938
|
|
915
939
|
|
916
940
|
# Main loop
|
917
|
-
|
941
|
+
logger.info(
|
942
|
+
f"Starting consumer for stream {REDIS_STREAM} in group {REDIS_CONSUMER_GROUP}"
|
943
|
+
)
|
918
944
|
consumer_name = f"consumer-{os.getpid()}-{socket.gethostname()}" # More unique name
|
919
945
|
MIN_IDLE_TIME_MS = 60000 # Minimum idle time in milliseconds (e.g., 60 seconds)
|
920
946
|
CLAIM_COUNT = 10 # Max messages to claim at once
|
@@ -924,25 +950,25 @@ disable_hot_reload = os.environ.get("NEBU_DISABLE_HOT_RELOAD", "0").lower() in [
|
|
924
950
|
"1",
|
925
951
|
"true",
|
926
952
|
]
|
927
|
-
|
953
|
+
logger.info(
|
928
954
|
f"[Consumer] Hot code reloading is {'DISABLED' if disable_hot_reload else 'ENABLED'}."
|
929
955
|
)
|
930
956
|
|
931
957
|
try:
|
932
958
|
while True:
|
933
|
-
|
959
|
+
logger.debug(
|
934
960
|
f"[{datetime.now(timezone.utc).isoformat()}] --- Top of main loop ---"
|
935
961
|
) # Added log
|
936
962
|
# --- Check for Code Updates ---
|
937
963
|
if not disable_hot_reload:
|
938
|
-
|
964
|
+
logger.debug(
|
939
965
|
f"[{datetime.now(timezone.utc).isoformat()}] Checking for code updates..."
|
940
966
|
) # Added log
|
941
967
|
if entrypoint_abs_path: # Should always be set after init
|
942
968
|
try:
|
943
969
|
current_mtime = os.path.getmtime(entrypoint_abs_path)
|
944
970
|
if current_mtime > last_load_mtime:
|
945
|
-
|
971
|
+
logger.info(
|
946
972
|
f"[Consumer] Detected change in entrypoint file: {entrypoint_abs_path}. Reloading code..."
|
947
973
|
)
|
948
974
|
(
|
@@ -963,7 +989,7 @@ try:
|
|
963
989
|
reloaded_target_func is not None
|
964
990
|
and reloaded_module is not None
|
965
991
|
):
|
966
|
-
|
992
|
+
logger.info(
|
967
993
|
"[Consumer] Code reload successful. Updating functions."
|
968
994
|
)
|
969
995
|
target_function = reloaded_target_func
|
@@ -974,13 +1000,13 @@ try:
|
|
974
1000
|
)
|
975
1001
|
last_load_mtime = new_mtime
|
976
1002
|
else:
|
977
|
-
|
1003
|
+
logger.warning(
|
978
1004
|
"[Consumer] Code reload failed. Continuing with previously loaded code."
|
979
1005
|
)
|
980
1006
|
# Optionally: Send an alert/log prominently that reload failed
|
981
1007
|
|
982
1008
|
except FileNotFoundError:
|
983
|
-
|
1009
|
+
logger.error(
|
984
1010
|
f"[Consumer] Error: Entrypoint file '{entrypoint_abs_path}' not found during check. Cannot reload."
|
985
1011
|
)
|
986
1012
|
# Mark as non-runnable? Or just log?
|
@@ -988,23 +1014,25 @@ try:
|
|
988
1014
|
imported_module = None
|
989
1015
|
last_load_mtime = 0 # Reset mtime to force check next time
|
990
1016
|
except Exception as e_reload_check:
|
991
|
-
|
992
|
-
|
1017
|
+
logger.error(
|
1018
|
+
f"[Consumer] Error checking/reloading code: {e_reload_check}"
|
1019
|
+
)
|
1020
|
+
logger.exception("Code Reload Check Error Traceback:")
|
993
1021
|
else:
|
994
|
-
|
1022
|
+
logger.warning(
|
995
1023
|
"[Consumer] Warning: Entrypoint absolute path not set, cannot check for code updates."
|
996
1024
|
)
|
997
|
-
|
1025
|
+
logger.debug(
|
998
1026
|
f"[{datetime.now(timezone.utc).isoformat()}] Finished checking for code updates."
|
999
1027
|
) # Added log
|
1000
1028
|
else:
|
1001
1029
|
# Log that hot reload is skipped if it's disabled
|
1002
|
-
|
1030
|
+
logger.debug(
|
1003
1031
|
f"[{datetime.now(timezone.utc).isoformat()}] Hot reload check skipped (NEBU_DISABLE_HOT_RELOAD=1)."
|
1004
1032
|
)
|
1005
1033
|
|
1006
1034
|
# --- Claim Old Pending Messages ---
|
1007
|
-
|
1035
|
+
logger.debug(
|
1008
1036
|
f"[{datetime.now(timezone.utc).isoformat()}] Checking for pending messages to claim..."
|
1009
1037
|
) # Added log
|
1010
1038
|
try:
|
@@ -1063,67 +1091,71 @@ try:
|
|
1063
1091
|
claimed_messages = [(REDIS_STREAM, claimed_messages_list)]
|
1064
1092
|
|
1065
1093
|
if claimed_messages:
|
1066
|
-
print(
|
1067
|
-
f"[{datetime.now(timezone.utc).isoformat()}] Claimed {claimed_messages} pending message(s). Processing..."
|
1068
|
-
)
|
1069
1094
|
# Process claimed messages immediately
|
1070
1095
|
# Cast messages to expected type to satisfy type checker
|
1071
1096
|
typed_messages = cast(
|
1072
1097
|
List[Tuple[str, List[Tuple[str, Dict[str, str]]]]],
|
1073
1098
|
claimed_messages,
|
1074
1099
|
)
|
1100
|
+
# Log after casting and before processing
|
1101
|
+
num_claimed = len(typed_messages[0][1]) if typed_messages else 0
|
1102
|
+
logger.info(
|
1103
|
+
f"[{datetime.now(timezone.utc).isoformat()}] Claimed {num_claimed} pending message(s). Processing..."
|
1104
|
+
)
|
1075
1105
|
stream_name_str, stream_messages = typed_messages[0]
|
1076
1106
|
for (
|
1077
1107
|
message_id_str,
|
1078
1108
|
message_data_str_dict,
|
1079
1109
|
) in stream_messages:
|
1080
|
-
|
1110
|
+
logger.info(
|
1111
|
+
f"[Consumer] Processing claimed message {message_id_str}"
|
1112
|
+
)
|
1081
1113
|
process_message(message_id_str, message_data_str_dict)
|
1082
1114
|
# After processing claimed messages, loop back to check for more potentially
|
1083
1115
|
# This avoids immediately blocking on XREADGROUP if there were claimed messages
|
1084
1116
|
continue
|
1085
1117
|
else: # Added log
|
1086
|
-
|
1118
|
+
logger.debug(
|
1087
1119
|
f"[{datetime.now(timezone.utc).isoformat()}] No pending messages claimed."
|
1088
1120
|
) # Added log
|
1089
1121
|
|
1090
1122
|
except ResponseError as e_claim:
|
1091
1123
|
# Handle specific errors like NOGROUP gracefully if needed
|
1092
1124
|
if "NOGROUP" in str(e_claim):
|
1093
|
-
|
1125
|
+
logger.critical(
|
1094
1126
|
f"Consumer group {REDIS_CONSUMER_GROUP} not found during xautoclaim. Exiting."
|
1095
1127
|
)
|
1096
1128
|
sys.exit(1)
|
1097
1129
|
else:
|
1098
|
-
|
1130
|
+
logger.error(f"[Consumer] Error during XAUTOCLAIM: {e_claim}")
|
1099
1131
|
# Decide if this is fatal or recoverable
|
1100
|
-
|
1132
|
+
logger.error(
|
1101
1133
|
f"[{datetime.now(timezone.utc).isoformat()}] Error during XAUTOCLAIM: {e_claim}"
|
1102
1134
|
) # Added log
|
1103
1135
|
time.sleep(5) # Wait before retrying claim
|
1104
1136
|
except ConnectionError as e_claim_conn:
|
1105
|
-
|
1137
|
+
logger.error(
|
1106
1138
|
f"Redis connection error during XAUTOCLAIM: {e_claim_conn}. Will attempt reconnect in main loop."
|
1107
1139
|
)
|
1108
1140
|
# Let the main ConnectionError handler below deal with reconnection
|
1109
|
-
|
1141
|
+
logger.error(
|
1110
1142
|
f"[{datetime.now(timezone.utc).isoformat()}] Redis connection error during XAUTOCLAIM: {e_claim_conn}. Will attempt reconnect."
|
1111
1143
|
) # Added log
|
1112
1144
|
time.sleep(5) # Avoid tight loop on connection errors during claim
|
1113
1145
|
except Exception as e_claim_other:
|
1114
|
-
|
1146
|
+
logger.error(
|
1115
1147
|
f"[Consumer] Unexpected error during XAUTOCLAIM/processing claimed messages: {e_claim_other}"
|
1116
1148
|
)
|
1117
|
-
|
1149
|
+
logger.error(
|
1118
1150
|
f"[{datetime.now(timezone.utc).isoformat()}] Unexpected error during XAUTOCLAIM/processing claimed: {e_claim_other}"
|
1119
1151
|
) # Added log
|
1120
|
-
|
1152
|
+
logger.exception("XAUTOCLAIM/Processing Error Traceback:")
|
1121
1153
|
time.sleep(5) # Wait before retrying
|
1122
1154
|
|
1123
1155
|
# --- Read New Messages from Redis Stream ---
|
1124
1156
|
if target_function is None:
|
1125
1157
|
# If code failed to load initially or during reload, wait before retrying
|
1126
|
-
|
1158
|
+
logger.warning(
|
1127
1159
|
"[Consumer] Target function not loaded, waiting 5s before checking again..."
|
1128
1160
|
)
|
1129
1161
|
time.sleep(5)
|
@@ -1135,7 +1167,7 @@ try:
|
|
1135
1167
|
streams_arg: Dict[str, str] = {REDIS_STREAM: ">"}
|
1136
1168
|
|
1137
1169
|
# With decode_responses=True, redis-py expects str types here
|
1138
|
-
|
1170
|
+
logger.debug(
|
1139
1171
|
f"[{datetime.now(timezone.utc).isoformat()}] Calling xreadgroup (block=5000ms)..."
|
1140
1172
|
) # Added log
|
1141
1173
|
messages = r.xreadgroup(
|
@@ -1147,10 +1179,10 @@ try:
|
|
1147
1179
|
)
|
1148
1180
|
|
1149
1181
|
if not messages:
|
1150
|
-
|
1182
|
+
logger.trace(
|
1151
1183
|
f"[{datetime.now(timezone.utc).isoformat()}] xreadgroup timed out (no new messages)."
|
1152
1184
|
) # Added log
|
1153
|
-
#
|
1185
|
+
# logger.debug("[Consumer] No new messages.") # Reduce verbosity
|
1154
1186
|
continue
|
1155
1187
|
# Removed the else block here
|
1156
1188
|
|
@@ -1166,7 +1198,7 @@ try:
|
|
1166
1198
|
num_msgs = len(stream_messages)
|
1167
1199
|
|
1168
1200
|
# Log reception and count before processing
|
1169
|
-
|
1201
|
+
logger.info(
|
1170
1202
|
f"[{datetime.now(timezone.utc).isoformat()}] xreadgroup returned {num_msgs} message(s). Processing..."
|
1171
1203
|
) # Moved and combined log
|
1172
1204
|
|
@@ -1185,32 +1217,32 @@ try:
|
|
1185
1217
|
process_message(message_id_str, message_data_str_dict)
|
1186
1218
|
|
1187
1219
|
except ConnectionError as e:
|
1188
|
-
|
1220
|
+
logger.error(f"Redis connection error: {e}. Reconnecting in 5s...")
|
1189
1221
|
time.sleep(5)
|
1190
1222
|
# Attempt to reconnect explicitly
|
1191
1223
|
try:
|
1192
|
-
|
1224
|
+
logger.info("Attempting Redis reconnection...")
|
1193
1225
|
# Close existing potentially broken connection? `r.close()` if available
|
1194
1226
|
r = redis.from_url(REDIS_URL, decode_responses=True)
|
1195
1227
|
r.ping()
|
1196
|
-
|
1228
|
+
logger.info("Reconnected to Redis.")
|
1197
1229
|
except Exception as recon_e:
|
1198
|
-
|
1230
|
+
logger.error(f"Failed to reconnect to Redis: {recon_e}")
|
1199
1231
|
# Keep waiting
|
1200
1232
|
|
1201
1233
|
except ResponseError as e:
|
1202
|
-
|
1234
|
+
logger.error(f"Redis command error: {e}")
|
1203
1235
|
# Should we exit or retry?
|
1204
1236
|
if "NOGROUP" in str(e):
|
1205
|
-
|
1237
|
+
logger.critical("Consumer group seems to have disappeared. Exiting.")
|
1206
1238
|
sys.exit(1)
|
1207
1239
|
time.sleep(1)
|
1208
1240
|
|
1209
1241
|
except Exception as e:
|
1210
|
-
|
1211
|
-
|
1242
|
+
logger.error(f"Unexpected error in main loop: {e}")
|
1243
|
+
logger.exception("Main Loop Error Traceback:")
|
1212
1244
|
time.sleep(1)
|
1213
1245
|
|
1214
1246
|
finally:
|
1215
|
-
|
1247
|
+
logger.info("Consumer loop exited.")
|
1216
1248
|
# Any other cleanup needed?
|