nebu 0.1.115__tar.gz → 0.1.117__tar.gz
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-0.1.115/src/nebu.egg-info → nebu-0.1.117}/PKG-INFO +1 -1
- {nebu-0.1.115 → nebu-0.1.117}/pyproject.toml +1 -1
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/processors/consumer.py +179 -21
- nebu-0.1.117/src/nebu/processors/consumer_health_worker.py +222 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/processors/decorate.py +7 -1
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/processors/processor.py +7 -0
- {nebu-0.1.115 → nebu-0.1.117/src/nebu.egg-info}/PKG-INFO +1 -1
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu.egg-info/SOURCES.txt +1 -0
- {nebu-0.1.115 → nebu-0.1.117}/LICENSE +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/README.md +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/setup.cfg +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/__init__.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/auth.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/builders/builder.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/builders/models.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/cache.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/config.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/containers/container.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/containers/models.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/data.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/errors.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/logging.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/meta.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/namespaces/models.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/namespaces/namespace.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/orign.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/processors/consumer_process_worker.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/processors/default.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/processors/models.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/redis/models.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu/services/service.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu.egg-info/dependency_links.txt +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu.egg-info/requires.txt +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/src/nebu.egg-info/top_level.txt +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/tests/test_bucket.py +0 -0
- {nebu-0.1.115 → nebu-0.1.117}/tests/test_containers.py +0 -0
@@ -33,11 +33,20 @@ local_namespace: Dict[str, Any] = {} # Namespace for included objects
|
|
33
33
|
last_load_mtime: float = 0.0
|
34
34
|
entrypoint_abs_path: Optional[str] = None
|
35
35
|
|
36
|
+
# Global health check subprocess
|
37
|
+
health_subprocess: Optional[subprocess.Popen] = None
|
38
|
+
|
36
39
|
REDIS_CONSUMER_GROUP = os.environ.get("REDIS_CONSUMER_GROUP")
|
37
40
|
REDIS_STREAM = os.environ.get("REDIS_STREAM")
|
38
41
|
NEBU_EXECUTION_MODE = os.environ.get("NEBU_EXECUTION_MODE", "inline").lower()
|
39
42
|
execution_mode = NEBU_EXECUTION_MODE
|
40
43
|
|
44
|
+
# Define health check stream and group names
|
45
|
+
REDIS_HEALTH_STREAM = f"{REDIS_STREAM}.health" if REDIS_STREAM else None
|
46
|
+
REDIS_HEALTH_CONSUMER_GROUP = (
|
47
|
+
f"{REDIS_CONSUMER_GROUP}-health" if REDIS_CONSUMER_GROUP else None
|
48
|
+
)
|
49
|
+
|
41
50
|
if execution_mode not in ["inline", "subprocess"]:
|
42
51
|
logger.warning(
|
43
52
|
f"Invalid NEBU_EXECUTION_MODE: {NEBU_EXECUTION_MODE}. Must be 'inline' or 'subprocess'. Defaulting to 'inline'."
|
@@ -328,20 +337,34 @@ socks.set_default_proxy(socks.SOCKS5, "localhost", 1055)
|
|
328
337
|
socket.socket = socks.socksocket
|
329
338
|
logger.info("Configured SOCKS5 proxy for socket connections via localhost:1055")
|
330
339
|
|
331
|
-
#
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
340
|
+
# Global Redis connection for the main consumer
|
341
|
+
r: redis.Redis # Initialized by connect_redis, which sys.exits on failure
|
342
|
+
|
343
|
+
|
344
|
+
# --- Connect to Redis (Main Consumer) ---
|
345
|
+
def connect_redis(redis_url: str) -> redis.Redis:
|
346
|
+
"""Connects to Redis and returns the connection object."""
|
347
|
+
try:
|
348
|
+
# Parse the Redis URL to handle potential credentials or specific DBs if needed
|
349
|
+
# Although from_url should work now with the patched socket
|
350
|
+
logger.info(
|
351
|
+
f"Attempting to connect to Redis at {redis_url.split('@')[-1] if '@' in redis_url else redis_url}"
|
352
|
+
)
|
353
|
+
conn = redis.from_url(
|
354
|
+
redis_url, decode_responses=True
|
355
|
+
) # Added decode_responses for convenience
|
356
|
+
conn.ping() # Test connection
|
357
|
+
redis_info = redis_url.split("@")[-1] if "@" in redis_url else redis_url
|
358
|
+
logger.info(f"Connected to Redis via SOCKS proxy at {redis_info}")
|
359
|
+
return conn
|
360
|
+
except Exception as e:
|
361
|
+
logger.critical(f"Failed to connect to Redis via SOCKS proxy: {e}")
|
362
|
+
logger.exception("Redis Connection Error Traceback:")
|
363
|
+
sys.exit(1)
|
364
|
+
|
365
|
+
|
366
|
+
r = connect_redis(REDIS_URL)
|
367
|
+
|
345
368
|
|
346
369
|
# Create consumer group if it doesn't exist
|
347
370
|
try:
|
@@ -360,6 +383,103 @@ except ResponseError as e:
|
|
360
383
|
logger.exception("Consumer Group Creation Error Traceback:")
|
361
384
|
|
362
385
|
|
386
|
+
# --- Health Check Subprocess Management ---
|
387
|
+
def start_health_check_subprocess() -> Optional[subprocess.Popen]:
|
388
|
+
"""Start the health check consumer subprocess."""
|
389
|
+
global REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP
|
390
|
+
|
391
|
+
if not all([REDIS_URL, REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP]):
|
392
|
+
logger.warning(
|
393
|
+
"[Consumer] Health check stream not configured. Health consumer subprocess not started."
|
394
|
+
)
|
395
|
+
return None
|
396
|
+
|
397
|
+
try:
|
398
|
+
# Type assertions to ensure variables are strings before using them
|
399
|
+
assert isinstance(REDIS_HEALTH_STREAM, str)
|
400
|
+
assert isinstance(REDIS_HEALTH_CONSUMER_GROUP, str)
|
401
|
+
|
402
|
+
# Prepare environment variables for the subprocess
|
403
|
+
health_env = os.environ.copy()
|
404
|
+
health_env["REDIS_HEALTH_STREAM"] = REDIS_HEALTH_STREAM
|
405
|
+
health_env["REDIS_HEALTH_CONSUMER_GROUP"] = REDIS_HEALTH_CONSUMER_GROUP
|
406
|
+
|
407
|
+
# Start the health check worker subprocess
|
408
|
+
health_cmd = [
|
409
|
+
sys.executable,
|
410
|
+
"-u", # Force unbuffered stdout/stderr
|
411
|
+
"-m",
|
412
|
+
"nebu.processors.consumer_health_worker",
|
413
|
+
]
|
414
|
+
|
415
|
+
process = subprocess.Popen(
|
416
|
+
health_cmd,
|
417
|
+
stdout=subprocess.PIPE,
|
418
|
+
stderr=subprocess.STDOUT, # Combine stderr with stdout
|
419
|
+
text=True,
|
420
|
+
encoding="utf-8",
|
421
|
+
env=health_env,
|
422
|
+
bufsize=1, # Line buffered
|
423
|
+
)
|
424
|
+
|
425
|
+
logger.info(
|
426
|
+
f"[Consumer] Health check subprocess started with PID {process.pid}"
|
427
|
+
)
|
428
|
+
return process
|
429
|
+
|
430
|
+
except Exception as e:
|
431
|
+
logger.error(f"[Consumer] Failed to start health check subprocess: {e}")
|
432
|
+
logger.exception("Health Subprocess Start Error Traceback:")
|
433
|
+
return None
|
434
|
+
|
435
|
+
|
436
|
+
def monitor_health_subprocess(process: subprocess.Popen) -> None:
|
437
|
+
"""Monitor the health check subprocess and log its output."""
|
438
|
+
try:
|
439
|
+
# Read output from the subprocess
|
440
|
+
if process.stdout:
|
441
|
+
for line in iter(process.stdout.readline, ""):
|
442
|
+
logger.info(f"[HealthSubprocess] {line.strip()}")
|
443
|
+
process.stdout.close() if process.stdout else None
|
444
|
+
except Exception as e:
|
445
|
+
logger.error(f"[Consumer] Error monitoring health subprocess: {e}")
|
446
|
+
|
447
|
+
|
448
|
+
def check_health_subprocess() -> bool:
|
449
|
+
"""Check if the health subprocess is still running and restart if needed."""
|
450
|
+
global health_subprocess
|
451
|
+
|
452
|
+
if health_subprocess is None:
|
453
|
+
return False
|
454
|
+
|
455
|
+
# Check if process is still running
|
456
|
+
if health_subprocess.poll() is None:
|
457
|
+
return True # Still running
|
458
|
+
|
459
|
+
# Process has exited
|
460
|
+
exit_code = health_subprocess.returncode
|
461
|
+
logger.warning(
|
462
|
+
f"[Consumer] Health subprocess exited with code {exit_code}. Restarting..."
|
463
|
+
)
|
464
|
+
|
465
|
+
# Start a new health subprocess
|
466
|
+
health_subprocess = start_health_check_subprocess()
|
467
|
+
|
468
|
+
if health_subprocess:
|
469
|
+
# Start monitoring thread for the new subprocess
|
470
|
+
monitor_thread = threading.Thread(
|
471
|
+
target=monitor_health_subprocess, args=(health_subprocess,), daemon=True
|
472
|
+
)
|
473
|
+
monitor_thread.start()
|
474
|
+
logger.info(
|
475
|
+
"[Consumer] Health subprocess restarted and monitoring thread started."
|
476
|
+
)
|
477
|
+
return True
|
478
|
+
else:
|
479
|
+
logger.error("[Consumer] Failed to restart health subprocess.")
|
480
|
+
return False
|
481
|
+
|
482
|
+
|
363
483
|
# Function to process messages
|
364
484
|
def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
365
485
|
# Access the globally managed user code elements
|
@@ -1088,11 +1208,35 @@ logger.info(
|
|
1088
1208
|
f"[Consumer] Hot code reloading is {'DISABLED' if disable_hot_reload else 'ENABLED'}."
|
1089
1209
|
)
|
1090
1210
|
|
1211
|
+
# Start the health check consumer subprocess
|
1212
|
+
if REDIS_HEALTH_STREAM and REDIS_HEALTH_CONSUMER_GROUP:
|
1213
|
+
health_subprocess = start_health_check_subprocess()
|
1214
|
+
if health_subprocess:
|
1215
|
+
# Start monitoring thread for subprocess output
|
1216
|
+
monitor_thread = threading.Thread(
|
1217
|
+
target=monitor_health_subprocess, args=(health_subprocess,), daemon=True
|
1218
|
+
)
|
1219
|
+
monitor_thread.start()
|
1220
|
+
logger.info(
|
1221
|
+
f"[Consumer] Health check subprocess for {REDIS_HEALTH_STREAM} started and monitoring thread started."
|
1222
|
+
)
|
1223
|
+
else:
|
1224
|
+
logger.error("[Consumer] Failed to start health check subprocess.")
|
1225
|
+
else:
|
1226
|
+
logger.warning(
|
1227
|
+
"[Consumer] Health check stream not configured. Health consumer subprocess not started."
|
1228
|
+
)
|
1229
|
+
|
1091
1230
|
try:
|
1092
1231
|
while True:
|
1093
1232
|
logger.debug(
|
1094
1233
|
f"[{datetime.now(timezone.utc).isoformat()}] --- Top of main loop ---"
|
1095
1234
|
) # Added log
|
1235
|
+
|
1236
|
+
# --- Check Health Subprocess Status ---
|
1237
|
+
if health_subprocess:
|
1238
|
+
check_health_subprocess()
|
1239
|
+
|
1096
1240
|
# --- Check for Code Updates ---
|
1097
1241
|
if not disable_hot_reload:
|
1098
1242
|
logger.debug(
|
@@ -1356,18 +1500,21 @@ except ConnectionError as e:
|
|
1356
1500
|
# Attempt to reconnect explicitly
|
1357
1501
|
try:
|
1358
1502
|
logger.info("Attempting Redis reconnection...")
|
1359
|
-
# Close existing potentially broken connection?
|
1360
|
-
r
|
1361
|
-
|
1503
|
+
# Close existing potentially broken connection?
|
1504
|
+
if r: # Check if r was initialized
|
1505
|
+
try:
|
1506
|
+
r.close()
|
1507
|
+
except Exception:
|
1508
|
+
pass # Ignore errors during close
|
1509
|
+
r = connect_redis(REDIS_URL) # connect_redis will sys.exit on failure
|
1362
1510
|
logger.info("Reconnected to Redis.")
|
1363
|
-
except Exception as recon_e:
|
1511
|
+
except Exception as recon_e: # Should not be reached if connect_redis exits
|
1364
1512
|
logger.error(f"Failed to reconnect to Redis: {recon_e}")
|
1365
|
-
# Keep waiting
|
1366
1513
|
|
1367
1514
|
except ResponseError as e:
|
1368
1515
|
logger.error(f"Redis command error: {e}")
|
1369
1516
|
# Should we exit or retry?
|
1370
|
-
if "NOGROUP" in str(e):
|
1517
|
+
if r and "NOGROUP" in str(e): # Check if r is initialized
|
1371
1518
|
logger.critical("Consumer group seems to have disappeared. Exiting.")
|
1372
1519
|
sys.exit(1)
|
1373
1520
|
time.sleep(1)
|
@@ -1379,4 +1526,15 @@ except Exception as e:
|
|
1379
1526
|
|
1380
1527
|
finally:
|
1381
1528
|
logger.info("Consumer loop exited.")
|
1382
|
-
#
|
1529
|
+
# Cleanup health subprocess
|
1530
|
+
if health_subprocess and health_subprocess.poll() is None:
|
1531
|
+
logger.info("[Consumer] Terminating health check subprocess...")
|
1532
|
+
health_subprocess.terminate()
|
1533
|
+
try:
|
1534
|
+
health_subprocess.wait(timeout=5)
|
1535
|
+
except subprocess.TimeoutExpired:
|
1536
|
+
logger.warning(
|
1537
|
+
"[Consumer] Health subprocess did not terminate gracefully, killing it."
|
1538
|
+
)
|
1539
|
+
health_subprocess.kill()
|
1540
|
+
logger.info("[Consumer] Health subprocess cleanup complete.")
|
@@ -0,0 +1,222 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import socket
|
6
|
+
import sys
|
7
|
+
import time
|
8
|
+
from typing import Dict, List, Optional, Tuple, cast
|
9
|
+
|
10
|
+
import redis
|
11
|
+
import socks
|
12
|
+
from redis import ConnectionError, ResponseError
|
13
|
+
from redis.exceptions import TimeoutError as RedisTimeoutError
|
14
|
+
|
15
|
+
|
16
|
+
def setup_health_logging():
|
17
|
+
"""Set up logging for the health check worker to write to a dedicated file."""
|
18
|
+
# Create logs directory if it doesn't exist
|
19
|
+
log_dir = os.path.join(os.getcwd(), "logs")
|
20
|
+
os.makedirs(log_dir, exist_ok=True)
|
21
|
+
|
22
|
+
# Create log file path with timestamp
|
23
|
+
log_file = os.path.join(log_dir, f"health_consumer_{os.getpid()}.log")
|
24
|
+
|
25
|
+
# Configure logging
|
26
|
+
logging.basicConfig(
|
27
|
+
level=logging.INFO,
|
28
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
29
|
+
handlers=[
|
30
|
+
logging.FileHandler(log_file),
|
31
|
+
logging.StreamHandler(
|
32
|
+
sys.stdout
|
33
|
+
), # Also log to stdout for subprocess monitoring
|
34
|
+
],
|
35
|
+
)
|
36
|
+
|
37
|
+
logger = logging.getLogger("HealthConsumer")
|
38
|
+
logger.info(f"Health check worker started. Logging to: {log_file}")
|
39
|
+
return logger
|
40
|
+
|
41
|
+
|
42
|
+
def process_health_check_message(
|
43
|
+
message_id: str,
|
44
|
+
message_data: Dict[str, str],
|
45
|
+
redis_conn: redis.Redis,
|
46
|
+
logger: logging.Logger,
|
47
|
+
health_stream: str,
|
48
|
+
health_group: str,
|
49
|
+
) -> None:
|
50
|
+
"""Processes a single health check message."""
|
51
|
+
logger.info(f"Processing health check message {message_id}: {message_data}")
|
52
|
+
|
53
|
+
# Parse the message if it contains JSON data
|
54
|
+
try:
|
55
|
+
if "data" in message_data:
|
56
|
+
data = json.loads(message_data["data"])
|
57
|
+
logger.info(f"Health check data: {data}")
|
58
|
+
except (json.JSONDecodeError, KeyError) as e:
|
59
|
+
logger.warning(f"Could not parse health check message data: {e}")
|
60
|
+
|
61
|
+
# You could add more logic here, e.g., update an internal health status,
|
62
|
+
# send a response, perform actual health checks, etc.
|
63
|
+
|
64
|
+
# Acknowledge the health check message
|
65
|
+
try:
|
66
|
+
redis_conn.xack(health_stream, health_group, message_id)
|
67
|
+
logger.info(f"Acknowledged health check message {message_id}")
|
68
|
+
except Exception as e_ack:
|
69
|
+
logger.error(
|
70
|
+
f"Failed to acknowledge health check message {message_id}: {e_ack}"
|
71
|
+
)
|
72
|
+
|
73
|
+
|
74
|
+
def main():
|
75
|
+
"""Main function for the health check consumer subprocess."""
|
76
|
+
logger = setup_health_logging()
|
77
|
+
|
78
|
+
# Get environment variables
|
79
|
+
redis_url = os.environ.get("REDIS_URL")
|
80
|
+
health_stream = os.environ.get("REDIS_HEALTH_STREAM")
|
81
|
+
health_group = os.environ.get("REDIS_HEALTH_CONSUMER_GROUP")
|
82
|
+
|
83
|
+
if not all([redis_url, health_stream, health_group]):
|
84
|
+
logger.error(
|
85
|
+
"Missing required environment variables: REDIS_URL, REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP"
|
86
|
+
)
|
87
|
+
sys.exit(1)
|
88
|
+
|
89
|
+
# Type assertions after validation
|
90
|
+
assert isinstance(redis_url, str)
|
91
|
+
assert isinstance(health_stream, str)
|
92
|
+
assert isinstance(health_group, str)
|
93
|
+
|
94
|
+
logger.info(
|
95
|
+
f"Starting health consumer for stream: {health_stream}, group: {health_group}"
|
96
|
+
)
|
97
|
+
|
98
|
+
# Configure SOCKS proxy
|
99
|
+
socks.set_default_proxy(socks.SOCKS5, "localhost", 1055)
|
100
|
+
socket.socket = socks.socksocket
|
101
|
+
logger.info("Configured SOCKS5 proxy for socket connections via localhost:1055")
|
102
|
+
|
103
|
+
health_redis_conn: Optional[redis.Redis] = None
|
104
|
+
health_consumer_name = f"health-consumer-{os.getpid()}-{socket.gethostname()}"
|
105
|
+
|
106
|
+
while True:
|
107
|
+
try:
|
108
|
+
if health_redis_conn is None:
|
109
|
+
logger.info("Connecting to Redis for health stream...")
|
110
|
+
health_redis_conn = redis.from_url(redis_url, decode_responses=True)
|
111
|
+
health_redis_conn.ping()
|
112
|
+
logger.info("Connected to Redis for health stream.")
|
113
|
+
|
114
|
+
# Create health consumer group if it doesn't exist
|
115
|
+
try:
|
116
|
+
health_redis_conn.xgroup_create(
|
117
|
+
health_stream, health_group, id="0", mkstream=True
|
118
|
+
)
|
119
|
+
logger.info(
|
120
|
+
f"Created consumer group {health_group} for stream {health_stream}"
|
121
|
+
)
|
122
|
+
except ResponseError as e_group:
|
123
|
+
if "BUSYGROUP" in str(e_group):
|
124
|
+
logger.info(f"Consumer group {health_group} already exists.")
|
125
|
+
else:
|
126
|
+
logger.error(f"Error creating health consumer group: {e_group}")
|
127
|
+
time.sleep(5)
|
128
|
+
health_redis_conn = None
|
129
|
+
continue
|
130
|
+
except Exception as e_group_other:
|
131
|
+
logger.error(
|
132
|
+
f"Unexpected error creating health consumer group: {e_group_other}"
|
133
|
+
)
|
134
|
+
time.sleep(5)
|
135
|
+
health_redis_conn = None
|
136
|
+
continue
|
137
|
+
|
138
|
+
# Read from health stream
|
139
|
+
assert health_redis_conn is not None
|
140
|
+
|
141
|
+
health_streams_arg: Dict[str, object] = {health_stream: ">"}
|
142
|
+
raw_messages = health_redis_conn.xreadgroup(
|
143
|
+
health_group,
|
144
|
+
health_consumer_name,
|
145
|
+
health_streams_arg, # type: ignore[arg-type]
|
146
|
+
count=1,
|
147
|
+
block=5000, # Block for 5 seconds
|
148
|
+
)
|
149
|
+
|
150
|
+
if raw_messages:
|
151
|
+
# Cast to expected type for decode_responses=True
|
152
|
+
messages = cast(
|
153
|
+
List[Tuple[str, List[Tuple[str, Dict[str, str]]]]], raw_messages
|
154
|
+
)
|
155
|
+
for _stream_name, stream_messages in messages:
|
156
|
+
for message_id, message_data in stream_messages:
|
157
|
+
process_health_check_message(
|
158
|
+
message_id,
|
159
|
+
message_data,
|
160
|
+
health_redis_conn,
|
161
|
+
logger,
|
162
|
+
health_stream,
|
163
|
+
health_group,
|
164
|
+
)
|
165
|
+
|
166
|
+
except (ConnectionError, RedisTimeoutError, TimeoutError) as e_conn:
|
167
|
+
logger.error(f"Redis connection error: {e_conn}. Reconnecting in 5s...")
|
168
|
+
if health_redis_conn:
|
169
|
+
try:
|
170
|
+
health_redis_conn.close()
|
171
|
+
except Exception:
|
172
|
+
pass
|
173
|
+
health_redis_conn = None
|
174
|
+
time.sleep(5)
|
175
|
+
|
176
|
+
except ResponseError as e_resp:
|
177
|
+
logger.error(f"Redis response error: {e_resp}")
|
178
|
+
if "NOGROUP" in str(e_resp):
|
179
|
+
logger.warning(
|
180
|
+
"Health consumer group disappeared. Attempting to recreate..."
|
181
|
+
)
|
182
|
+
if health_redis_conn:
|
183
|
+
try:
|
184
|
+
health_redis_conn.close()
|
185
|
+
except Exception:
|
186
|
+
pass
|
187
|
+
health_redis_conn = None
|
188
|
+
elif "UNBLOCKED" in str(e_resp):
|
189
|
+
logger.info(
|
190
|
+
"XREADGROUP unblocked, connection might have been closed. Reconnecting."
|
191
|
+
)
|
192
|
+
if health_redis_conn:
|
193
|
+
try:
|
194
|
+
health_redis_conn.close()
|
195
|
+
except Exception:
|
196
|
+
pass
|
197
|
+
health_redis_conn = None
|
198
|
+
time.sleep(1)
|
199
|
+
else:
|
200
|
+
time.sleep(5)
|
201
|
+
|
202
|
+
except KeyboardInterrupt:
|
203
|
+
logger.info("Received interrupt signal. Shutting down health consumer...")
|
204
|
+
break
|
205
|
+
|
206
|
+
except Exception as e:
|
207
|
+
logger.error(f"Unexpected error in health check consumer: {e}")
|
208
|
+
logger.exception("Traceback:")
|
209
|
+
time.sleep(5)
|
210
|
+
|
211
|
+
# Cleanup
|
212
|
+
if health_redis_conn:
|
213
|
+
try:
|
214
|
+
health_redis_conn.close()
|
215
|
+
except Exception:
|
216
|
+
pass
|
217
|
+
|
218
|
+
logger.info("Health check consumer shutdown complete.")
|
219
|
+
|
220
|
+
|
221
|
+
if __name__ == "__main__":
|
222
|
+
main()
|
@@ -802,7 +802,13 @@ def processor(
|
|
802
802
|
)
|
803
803
|
origin = get_origin(param_type) if param_type else None
|
804
804
|
args = get_args(param_type) if param_type else tuple()
|
805
|
-
logger.debug(
|
805
|
+
logger.debug(
|
806
|
+
f"Decorator: For param_type '{param_type_str_repr}': origin = {origin!s}, args = {args!s}"
|
807
|
+
) # More detailed log
|
808
|
+
print(
|
809
|
+
f"Decorator: For param_type '{param_type_str_repr}': origin = {origin!s}, args = {args!s}"
|
810
|
+
) # More detailed log
|
811
|
+
|
806
812
|
is_stream_message = False
|
807
813
|
content_type = None
|
808
814
|
content_type_name_from_regex = None # Store regex result here
|
@@ -311,6 +311,13 @@ class Processor(Generic[InputType, OutputType]):
|
|
311
311
|
)
|
312
312
|
response.raise_for_status()
|
313
313
|
raw_response_json = response.json()
|
314
|
+
|
315
|
+
if "error" in raw_response_json:
|
316
|
+
raise Exception(raw_response_json["error"])
|
317
|
+
|
318
|
+
if "status" in raw_response_json:
|
319
|
+
return raw_response_json
|
320
|
+
|
314
321
|
raw_content = raw_response_json.get("content")
|
315
322
|
logger.debug(f">>> Raw content: {raw_content}")
|
316
323
|
|
@@ -22,6 +22,7 @@ src/nebu/containers/models.py
|
|
22
22
|
src/nebu/namespaces/models.py
|
23
23
|
src/nebu/namespaces/namespace.py
|
24
24
|
src/nebu/processors/consumer.py
|
25
|
+
src/nebu/processors/consumer_health_worker.py
|
25
26
|
src/nebu/processors/consumer_process_worker.py
|
26
27
|
src/nebu/processors/decorate.py
|
27
28
|
src/nebu/processors/default.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|