nebu 0.1.45__py3-none-any.whl → 0.1.48__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/__init__.py +0 -1
- nebu/builders/builder.py +0 -0
- nebu/data.py +24 -3
- nebu/processors/consumer.py +581 -389
- nebu/processors/decorate.py +441 -411
- {nebu-0.1.45.dist-info → nebu-0.1.48.dist-info}/METADATA +1 -1
- {nebu-0.1.45.dist-info → nebu-0.1.48.dist-info}/RECORD +10 -12
- {nebu-0.1.45.dist-info → nebu-0.1.48.dist-info}/WHEEL +1 -1
- nebu/adapter.py +0 -20
- nebu/chatx/convert.py +0 -362
- nebu/chatx/openai.py +0 -976
- {nebu-0.1.45.dist-info → nebu-0.1.48.dist-info}/licenses/LICENSE +0 -0
- {nebu-0.1.45.dist-info → nebu-0.1.48.dist-info}/top_level.txt +0 -0
nebu/processors/consumer.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
+
import importlib
|
2
3
|
import json
|
3
4
|
import os
|
4
5
|
import socket
|
5
6
|
import sys
|
6
7
|
import time
|
7
8
|
import traceback
|
8
|
-
|
9
|
-
from
|
9
|
+
import types # Added for ModuleType
|
10
|
+
from datetime import datetime, timezone
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, cast
|
10
12
|
|
11
13
|
import redis
|
12
14
|
import socks
|
@@ -18,67 +20,168 @@ T = TypeVar("T")
|
|
18
20
|
# Environment variable name used as a guard in the decorator
|
19
21
|
_NEBU_INSIDE_CONSUMER_ENV_VAR = "_NEBU_INSIDE_CONSUMER_EXEC"
|
20
22
|
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
23
|
+
# --- Global variables for dynamically loaded code ---
|
24
|
+
target_function: Optional[Callable] = None
|
25
|
+
init_function: Optional[Callable] = None
|
26
|
+
imported_module: Optional[types.ModuleType] = None
|
27
|
+
local_namespace: Dict[str, Any] = {} # Namespace for included objects
|
28
|
+
last_load_mtime: float = 0.0
|
29
|
+
entrypoint_abs_path: Optional[str] = None
|
30
|
+
|
31
|
+
|
32
|
+
# --- Function to Load/Reload User Code ---
|
33
|
+
def load_or_reload_user_code(
|
34
|
+
module_path: str,
|
35
|
+
function_name: str,
|
36
|
+
entrypoint_abs_path: str,
|
37
|
+
init_func_name: Optional[str] = None,
|
38
|
+
included_object_sources: Optional[List[Tuple[str, List[str]]]] = None,
|
39
|
+
) -> Tuple[
|
40
|
+
Optional[Callable],
|
41
|
+
Optional[Callable],
|
42
|
+
Optional[types.ModuleType],
|
43
|
+
Dict[str, Any],
|
44
|
+
float,
|
45
|
+
]:
|
46
|
+
"""Loads or reloads the user code module, executes includes, and returns functions/module."""
|
47
|
+
global _NEBU_INSIDE_CONSUMER_ENV_VAR # Access the global guard var name
|
48
|
+
|
49
|
+
current_mtime = 0.0
|
50
|
+
loaded_target_func = None
|
51
|
+
loaded_init_func = None
|
52
|
+
loaded_module = None
|
53
|
+
exec_namespace: Dict[str, Any] = {} # Use a local namespace for this load attempt
|
54
|
+
|
55
|
+
print(f"[Code Loader] Attempting to load/reload module: '{module_path}'")
|
56
|
+
os.environ[_NEBU_INSIDE_CONSUMER_ENV_VAR] = "1" # Set guard *before* import/reload
|
57
|
+
print(f"[Code Loader] Set environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}=1")
|
35
58
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
59
|
+
try:
|
60
|
+
current_mtime = os.path.getmtime(entrypoint_abs_path)
|
61
|
+
|
62
|
+
# Execute included object sources FIRST (if any)
|
63
|
+
if included_object_sources:
|
64
|
+
print("[Code Loader] Executing @include object sources...")
|
65
|
+
# Include necessary imports for the exec context
|
66
|
+
exec("from pydantic import BaseModel, Field", exec_namespace)
|
67
|
+
exec(
|
68
|
+
"from typing import Optional, List, Dict, Any, Generic, TypeVar",
|
69
|
+
exec_namespace,
|
70
|
+
)
|
71
|
+
exec("T_exec = TypeVar('T_exec')", exec_namespace)
|
72
|
+
exec("from nebu.processors.models import *", exec_namespace)
|
73
|
+
# ... add other common imports if needed by included objects ...
|
40
74
|
|
41
|
-
|
42
|
-
|
43
|
-
|
75
|
+
for i, (obj_source, args_sources) in enumerate(included_object_sources):
|
76
|
+
try:
|
77
|
+
exec(obj_source, exec_namespace)
|
78
|
+
print(
|
79
|
+
f"[Code Loader] Successfully executed included object {i} base source"
|
80
|
+
)
|
81
|
+
for j, arg_source in enumerate(args_sources):
|
82
|
+
try:
|
83
|
+
exec(arg_source, exec_namespace)
|
84
|
+
print(
|
85
|
+
f"[Code Loader] Successfully executed included object {i} arg {j} source"
|
86
|
+
)
|
87
|
+
except Exception as e_arg:
|
88
|
+
print(
|
89
|
+
f"Error executing included object {i} arg {j} source: {e_arg}"
|
90
|
+
)
|
91
|
+
traceback.print_exc() # Log specific error but continue? Or fail reload?
|
92
|
+
except Exception as e_base:
|
93
|
+
print(f"Error executing included object {i} base source: {e_base}")
|
94
|
+
traceback.print_exc() # Log specific error but continue? Or fail reload?
|
95
|
+
print("[Code Loader] Finished executing included object sources.")
|
96
|
+
|
97
|
+
# Check if module is already loaded and needs reload
|
98
|
+
if module_path in sys.modules:
|
99
|
+
print(
|
100
|
+
f"[Code Loader] Module '{module_path}' already imported. Reloading..."
|
101
|
+
)
|
102
|
+
# Pass the exec_namespace as globals? Usually reload works within its own context.
|
103
|
+
# If included objects *modify* the module's global scope upon exec,
|
104
|
+
# reload might not pick that up easily. Might need a fresh import instead.
|
105
|
+
# Let's try reload first.
|
106
|
+
loaded_module = importlib.reload(sys.modules[module_path])
|
107
|
+
print(f"[Code Loader] Successfully reloaded module: {module_path}")
|
108
|
+
else:
|
109
|
+
# Import the main module
|
110
|
+
loaded_module = importlib.import_module(module_path)
|
111
|
+
print(
|
112
|
+
f"[Code Loader] Successfully imported module for the first time: {module_path}"
|
113
|
+
)
|
44
114
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
115
|
+
# Get the target function from the loaded/reloaded module
|
116
|
+
loaded_target_func = getattr(loaded_module, function_name)
|
117
|
+
print(
|
118
|
+
f"[Code Loader] Successfully loaded function '{function_name}' from module '{module_path}'"
|
119
|
+
)
|
49
120
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
121
|
+
# Get the init function if specified
|
122
|
+
if init_func_name:
|
123
|
+
loaded_init_func = getattr(loaded_module, init_func_name)
|
124
|
+
print(
|
125
|
+
f"[Code Loader] Successfully loaded init function '{init_func_name}' from module '{module_path}'"
|
126
|
+
)
|
127
|
+
# Execute init_func
|
128
|
+
print(f"[Code Loader] Executing init_func: {init_func_name}...")
|
129
|
+
loaded_init_func() # Call the function
|
130
|
+
print(f"[Code Loader] Successfully executed init_func: {init_func_name}")
|
131
|
+
|
132
|
+
print("[Code Loader] Code load/reload successful.")
|
133
|
+
return (
|
134
|
+
loaded_target_func,
|
135
|
+
loaded_init_func,
|
136
|
+
loaded_module,
|
137
|
+
exec_namespace,
|
138
|
+
current_mtime,
|
139
|
+
)
|
59
140
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
141
|
+
except FileNotFoundError:
|
142
|
+
print(
|
143
|
+
f"[Code Loader] Error: Entrypoint file not found at '{entrypoint_abs_path}'. Cannot load/reload."
|
144
|
+
)
|
145
|
+
return None, None, None, {}, 0.0 # Indicate failure
|
146
|
+
except ImportError as e:
|
147
|
+
print(f"[Code Loader] Error importing/reloading module '{module_path}': {e}")
|
148
|
+
traceback.print_exc()
|
149
|
+
return None, None, None, {}, 0.0 # Indicate failure
|
150
|
+
except AttributeError as e:
|
151
|
+
print(
|
152
|
+
f"[Code Loader] Error accessing function '{function_name}' or '{init_func_name}' in module '{module_path}': {e}"
|
153
|
+
)
|
154
|
+
traceback.print_exc()
|
155
|
+
return None, None, None, {}, 0.0 # Indicate failure
|
156
|
+
except Exception as e:
|
157
|
+
print(f"[Code Loader] Unexpected error during code load/reload: {e}")
|
158
|
+
traceback.print_exc()
|
159
|
+
return None, None, None, {}, 0.0 # Indicate failure
|
160
|
+
finally:
|
161
|
+
# Unset the guard environment variable
|
162
|
+
os.environ.pop(_NEBU_INSIDE_CONSUMER_ENV_VAR, None)
|
163
|
+
print(
|
164
|
+
f"[Code Loader] Unset environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}"
|
165
|
+
)
|
69
166
|
|
70
|
-
# Get content type arg sources
|
71
|
-
i = 0
|
72
|
-
while True:
|
73
|
-
arg_source = os.environ.get(f"CONTENT_TYPE_ARG_{i}_SOURCE")
|
74
|
-
if arg_source:
|
75
|
-
content_type_args.append(arg_source)
|
76
|
-
i += 1
|
77
|
-
else:
|
78
|
-
break
|
79
167
|
|
80
|
-
|
81
|
-
|
168
|
+
# --- Get Environment Variables ---
|
169
|
+
try:
|
170
|
+
# Core function info
|
171
|
+
_function_name = os.environ.get("FUNCTION_NAME")
|
172
|
+
_entrypoint_rel_path = os.environ.get("NEBU_ENTRYPOINT_MODULE_PATH")
|
173
|
+
|
174
|
+
# Type info
|
175
|
+
is_stream_message = os.environ.get("IS_STREAM_MESSAGE") == "True"
|
176
|
+
param_type_str = os.environ.get("PARAM_TYPE_STR")
|
177
|
+
return_type_str = os.environ.get("RETURN_TYPE_STR")
|
178
|
+
content_type_name = os.environ.get("CONTENT_TYPE_NAME")
|
179
|
+
|
180
|
+
# Init func info
|
181
|
+
_init_func_name = os.environ.get("INIT_FUNC_NAME")
|
182
|
+
|
183
|
+
# Included object sources
|
184
|
+
_included_object_sources = []
|
82
185
|
i = 0
|
83
186
|
while True:
|
84
187
|
obj_source = os.environ.get(f"INCLUDED_OBJECT_{i}_SOURCE")
|
@@ -92,213 +195,87 @@ try:
|
|
92
195
|
j += 1
|
93
196
|
else:
|
94
197
|
break
|
95
|
-
|
198
|
+
_included_object_sources.append((obj_source, args))
|
96
199
|
i += 1
|
97
200
|
else:
|
98
201
|
break
|
99
202
|
|
100
|
-
if not
|
101
|
-
print(
|
203
|
+
if not _function_name or not _entrypoint_rel_path:
|
204
|
+
print(
|
205
|
+
"FATAL: FUNCTION_NAME or NEBU_ENTRYPOINT_MODULE_PATH environment variables not set"
|
206
|
+
)
|
102
207
|
sys.exit(1)
|
103
208
|
|
104
|
-
#
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
exec("from nebu.processors.decorate import processor", local_namespace)
|
118
|
-
# Add import for chatx openai types
|
119
|
-
exec("from nebu.chatx.openai import *", local_namespace)
|
120
|
-
|
121
|
-
# Set the guard environment variable before executing any source code
|
122
|
-
os.environ[_NEBU_INSIDE_CONSUMER_ENV_VAR] = "1"
|
123
|
-
print(f"[Consumer] Set environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}=1")
|
124
|
-
|
125
|
-
try:
|
126
|
-
# Execute the source file of the decorated function FIRST
|
127
|
-
if decorated_func_file_source:
|
128
|
-
print("[Consumer] Executing decorated function's file source...")
|
129
|
-
try:
|
130
|
-
exec(decorated_func_file_source, local_namespace)
|
131
|
-
print(
|
132
|
-
"[Consumer] Successfully executed decorated function's file source."
|
133
|
-
)
|
134
|
-
except Exception as e:
|
135
|
-
print(f"Error executing decorated function's file source: {e}")
|
136
|
-
traceback.print_exc() # Warn and continue
|
137
|
-
else:
|
138
|
-
print(
|
139
|
-
"[Consumer] No decorated function's file source found in environment."
|
140
|
-
)
|
141
|
-
|
142
|
-
# Execute the sources from the decorated function's directory
|
143
|
-
# if decorated_dir_sources_json:
|
144
|
-
# print("[Consumer] Executing decorated function's directory sources...")
|
145
|
-
# try:
|
146
|
-
# dir_sources = json.loads(decorated_dir_sources_json)
|
147
|
-
# # Sort by relative path for some predictability (e.g., __init__.py first)
|
148
|
-
# for rel_path, source_code in sorted(dir_sources.items()):
|
149
|
-
# print(f"[Consumer] Executing source from: {rel_path}...")
|
150
|
-
# try:
|
151
|
-
# exec(source_code, local_namespace)
|
152
|
-
# print(f"[Consumer] Successfully executed source from: {rel_path}")
|
153
|
-
# except Exception as e:
|
154
|
-
# print(f"Error executing source from {rel_path}: {e}")
|
155
|
-
# traceback.print_exc() # Warn and continue
|
156
|
-
# except json.JSONDecodeError as e:
|
157
|
-
# print(f"Error decoding DECORATED_DIR_SOURCES JSON: {e}")
|
158
|
-
# traceback.print_exc()
|
159
|
-
# except Exception as e:
|
160
|
-
# print(f"Unexpected error processing directory sources: {e}")
|
161
|
-
# traceback.print_exc()
|
162
|
-
# else:
|
163
|
-
# print("[Consumer] No decorated function's directory sources found in environment.")
|
164
|
-
|
165
|
-
# Execute included object sources NEXT, as they might define types needed by others
|
166
|
-
print("[Consumer] Executing included object sources...")
|
167
|
-
for i, (obj_source, args_sources) in enumerate(included_object_sources):
|
168
|
-
try:
|
169
|
-
exec(obj_source, local_namespace)
|
209
|
+
# Calculate absolute path for modification time checking
|
210
|
+
# Assuming CWD or PYTHONPATH allows finding the relative path
|
211
|
+
# This might need adjustment based on deployment specifics
|
212
|
+
entrypoint_abs_path = os.path.abspath(_entrypoint_rel_path)
|
213
|
+
if not os.path.exists(entrypoint_abs_path):
|
214
|
+
# Try constructing path based on PYTHONPATH if direct abspath fails
|
215
|
+
python_path = os.environ.get("PYTHONPATH", "").split(os.pathsep)
|
216
|
+
found_path = False
|
217
|
+
for p_path in python_path:
|
218
|
+
potential_path = os.path.abspath(os.path.join(p_path, _entrypoint_rel_path))
|
219
|
+
if os.path.exists(potential_path):
|
220
|
+
entrypoint_abs_path = potential_path
|
221
|
+
found_path = True
|
170
222
|
print(
|
171
|
-
f"[Consumer]
|
223
|
+
f"[Consumer] Found entrypoint absolute path via PYTHONPATH: {entrypoint_abs_path}"
|
172
224
|
)
|
173
|
-
|
174
|
-
|
175
|
-
exec(arg_source, local_namespace)
|
176
|
-
print(
|
177
|
-
f"[Consumer] Successfully executed included object {i} arg {j} source"
|
178
|
-
)
|
179
|
-
except Exception as e:
|
180
|
-
print(
|
181
|
-
f"Error executing included object {i} arg {j} source: {e}"
|
182
|
-
)
|
183
|
-
traceback.print_exc()
|
184
|
-
except Exception as e:
|
185
|
-
print(f"Error executing included object {i} base source: {e}")
|
186
|
-
traceback.print_exc()
|
187
|
-
print("[Consumer] Finished executing included object sources.")
|
188
|
-
|
189
|
-
# First try to import the module to get any needed dependencies
|
190
|
-
# This is a fallback in case the module is available
|
191
|
-
module_name = os.environ.get("MODULE_NAME")
|
192
|
-
try:
|
193
|
-
if module_name:
|
194
|
-
exec(f"import {module_name}", local_namespace)
|
195
|
-
print(f"Successfully imported module {module_name}")
|
196
|
-
except Exception as e:
|
197
|
-
print(f"Warning: Could not import module {module_name}: {e}")
|
225
|
+
break
|
226
|
+
if not found_path:
|
198
227
|
print(
|
199
|
-
"
|
228
|
+
f"FATAL: Could not find entrypoint file via relative path '{_entrypoint_rel_path}' or in PYTHONPATH."
|
200
229
|
)
|
230
|
+
# Attempting abspath anyway for the error message in load function
|
231
|
+
entrypoint_abs_path = os.path.abspath(_entrypoint_rel_path)
|
232
|
+
|
233
|
+
# Convert entrypoint file path to module path
|
234
|
+
_module_path = _entrypoint_rel_path.replace(os.sep, ".")
|
235
|
+
if _module_path.endswith(".py"):
|
236
|
+
_module_path = _module_path[:-3]
|
237
|
+
if _module_path.endswith(".__init__"):
|
238
|
+
_module_path = _module_path[: -len(".__init__")]
|
239
|
+
elif _module_path == "__init__":
|
240
|
+
print(
|
241
|
+
f"FATAL: Entrypoint '{_entrypoint_rel_path}' resolves to ambiguous top-level __init__. Please use a named file or package."
|
242
|
+
)
|
243
|
+
sys.exit(1)
|
244
|
+
if not _module_path:
|
245
|
+
print(
|
246
|
+
f"FATAL: Could not derive a valid module path from entrypoint '{_entrypoint_rel_path}'"
|
247
|
+
)
|
248
|
+
sys.exit(1)
|
201
249
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
try:
|
206
|
-
exec(stream_message_source, local_namespace)
|
207
|
-
print("Successfully defined Message class")
|
208
|
-
except Exception as e:
|
209
|
-
print(f"Error defining Message: {e}")
|
210
|
-
traceback.print_exc()
|
211
|
-
|
212
|
-
# Define content type if available
|
213
|
-
if content_type_source:
|
214
|
-
try:
|
215
|
-
exec(content_type_source, local_namespace)
|
216
|
-
print(f"Successfully defined content type {content_type_name}")
|
217
|
-
|
218
|
-
# Define any content type args
|
219
|
-
for arg_source in content_type_args:
|
220
|
-
try:
|
221
|
-
exec(arg_source, local_namespace)
|
222
|
-
print("Successfully defined content type argument")
|
223
|
-
except Exception as e:
|
224
|
-
print(f"Error defining content type argument: {e}")
|
225
|
-
traceback.print_exc()
|
226
|
-
except Exception as e:
|
227
|
-
print(f"Error defining content type: {e}")
|
228
|
-
traceback.print_exc()
|
229
|
-
|
230
|
-
# Define input model if different from stream message
|
231
|
-
if input_model_source and (
|
232
|
-
not is_stream_message or input_model_source != stream_message_source
|
233
|
-
):
|
234
|
-
try:
|
235
|
-
exec(input_model_source, local_namespace)
|
236
|
-
print(f"Successfully defined input model {param_type_str}")
|
237
|
-
|
238
|
-
# Define any input model args
|
239
|
-
for arg_source in input_model_args:
|
240
|
-
try:
|
241
|
-
exec(arg_source, local_namespace)
|
242
|
-
print("Successfully defined input model argument")
|
243
|
-
except Exception as e:
|
244
|
-
print(f"Error defining input model argument: {e}")
|
245
|
-
traceback.print_exc()
|
246
|
-
except Exception as e:
|
247
|
-
print(f"Error defining input model: {e}")
|
248
|
-
traceback.print_exc()
|
249
|
-
|
250
|
-
# Define output model
|
251
|
-
if output_model_source:
|
252
|
-
try:
|
253
|
-
exec(output_model_source, local_namespace)
|
254
|
-
print(f"Successfully defined output model {return_type_str}")
|
250
|
+
print(
|
251
|
+
f"[Consumer] Initializing. Entrypoint: '{_entrypoint_rel_path}', Module: '{_module_path}', Function: '{_function_name}', Init: '{_init_func_name}'"
|
252
|
+
)
|
255
253
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
254
|
+
# --- Initial Load of User Code ---
|
255
|
+
(
|
256
|
+
target_function,
|
257
|
+
init_function,
|
258
|
+
imported_module,
|
259
|
+
local_namespace,
|
260
|
+
last_load_mtime,
|
261
|
+
) = load_or_reload_user_code(
|
262
|
+
_module_path,
|
263
|
+
_function_name,
|
264
|
+
entrypoint_abs_path,
|
265
|
+
_init_func_name,
|
266
|
+
_included_object_sources,
|
267
|
+
)
|
267
268
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
print(f"Error creating function from source: {e}")
|
275
|
-
traceback.print_exc()
|
276
|
-
sys.exit(1)
|
269
|
+
if target_function is None or imported_module is None:
|
270
|
+
print("FATAL: Initial load of user code failed. Exiting.")
|
271
|
+
sys.exit(1)
|
272
|
+
print(
|
273
|
+
f"[Consumer] Initial code load successful. Last modified time: {last_load_mtime}"
|
274
|
+
)
|
277
275
|
|
278
|
-
# Execute init_func if provided
|
279
|
-
if init_func_source and init_func_name:
|
280
|
-
print(f"Executing init_func: {init_func_name}...")
|
281
|
-
try:
|
282
|
-
exec(init_func_source, local_namespace)
|
283
|
-
init_function = local_namespace[init_func_name]
|
284
|
-
print(
|
285
|
-
f"[Consumer] Environment before calling init_func {init_func_name}: {os.environ}"
|
286
|
-
)
|
287
|
-
init_function() # Call the function
|
288
|
-
print(f"Successfully executed init_func: {init_func_name}")
|
289
|
-
except Exception as e:
|
290
|
-
print(f"Error executing init_func '{init_func_name}': {e}")
|
291
|
-
traceback.print_exc()
|
292
|
-
# Decide if failure is critical. For now, let's exit.
|
293
|
-
print("Exiting due to init_func failure.")
|
294
|
-
sys.exit(1)
|
295
|
-
finally:
|
296
|
-
# Unset the guard environment variable after all execs are done
|
297
|
-
os.environ.pop(_NEBU_INSIDE_CONSUMER_ENV_VAR, None)
|
298
|
-
print(f"[Consumer] Unset environment variable {_NEBU_INSIDE_CONSUMER_ENV_VAR}")
|
299
276
|
|
300
277
|
except Exception as e:
|
301
|
-
print(f"Error
|
278
|
+
print(f"FATAL: Error during initial environment setup or code load: {e}")
|
302
279
|
traceback.print_exc()
|
303
280
|
sys.exit(1)
|
304
281
|
|
@@ -349,46 +326,66 @@ except ResponseError as e:
|
|
349
326
|
|
350
327
|
# Function to process messages
|
351
328
|
def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
352
|
-
#
|
329
|
+
# Access the globally managed user code elements
|
330
|
+
global target_function, imported_module, local_namespace
|
331
|
+
|
332
|
+
# Check if target_function is loaded (might be None if reload failed)
|
333
|
+
if target_function is None or imported_module is None:
|
334
|
+
print(
|
335
|
+
f"Error processing message {message_id}: User code (target_function or module) is not loaded. Skipping."
|
336
|
+
)
|
337
|
+
# Decide how to handle this - skip and ack? Send error?
|
338
|
+
# Sending error for now, but not acking yet.
|
339
|
+
# This requires the main loop to handle potential ack failure later if needed.
|
340
|
+
_send_error_response(
|
341
|
+
message_id,
|
342
|
+
"User code is not loaded (likely due to a failed reload)",
|
343
|
+
traceback.format_exc(),
|
344
|
+
None,
|
345
|
+
None,
|
346
|
+
) # Pass None for user_id if unavailable here
|
347
|
+
return # Skip processing
|
348
|
+
|
353
349
|
return_stream = None
|
354
350
|
user_id = None
|
355
351
|
try:
|
356
|
-
# Extract the JSON string payload from the 'data' field
|
357
352
|
payload_str = message_data.get("data")
|
358
|
-
|
359
|
-
# decode_responses=True should mean payload_str is a string if found.
|
360
|
-
if not payload_str:
|
353
|
+
if not payload_str or not isinstance(payload_str, str):
|
361
354
|
raise ValueError(
|
362
|
-
f"Missing or invalid 'data' field (expected string): {message_data}"
|
355
|
+
f"Missing or invalid 'data' field (expected non-empty string): {message_data}"
|
363
356
|
)
|
364
|
-
|
365
|
-
# Parse the JSON string into a dictionary
|
366
357
|
try:
|
367
358
|
raw_payload = json.loads(payload_str)
|
368
359
|
except json.JSONDecodeError as json_err:
|
369
360
|
raise ValueError(f"Failed to parse JSON payload: {json_err}") from json_err
|
370
|
-
|
371
|
-
# Validate that raw_payload is a dictionary as expected
|
372
361
|
if not isinstance(raw_payload, dict):
|
373
362
|
raise TypeError(
|
374
363
|
f"Expected parsed payload to be a dictionary, but got {type(raw_payload)}"
|
375
364
|
)
|
376
365
|
|
377
|
-
print(f"Raw payload: {raw_payload}")
|
366
|
+
# print(f"Raw payload: {raw_payload}") # Reduce verbosity
|
378
367
|
|
379
|
-
|
380
|
-
|
381
|
-
kind = raw_payload.get("kind", "") # kind
|
382
|
-
msg_id = raw_payload.get("id", "") # msg_id
|
368
|
+
kind = raw_payload.get("kind", "")
|
369
|
+
msg_id = raw_payload.get("id", "")
|
383
370
|
content_raw = raw_payload.get("content", {})
|
384
|
-
|
371
|
+
created_at_str = raw_payload.get("created_at") # Get as string or None
|
372
|
+
# Attempt to parse created_at, fallback to now()
|
373
|
+
try:
|
374
|
+
created_at = (
|
375
|
+
datetime.fromisoformat(created_at_str)
|
376
|
+
if created_at_str
|
377
|
+
else datetime.now(timezone.utc)
|
378
|
+
)
|
379
|
+
except ValueError:
|
380
|
+
created_at = datetime.now(timezone.utc)
|
381
|
+
|
385
382
|
return_stream = raw_payload.get("return_stream")
|
386
383
|
user_id = raw_payload.get("user_id")
|
387
|
-
orgs = raw_payload.get("organizations")
|
388
|
-
handle = raw_payload.get("handle")
|
389
|
-
adapter = raw_payload.get("adapter")
|
384
|
+
orgs = raw_payload.get("organizations")
|
385
|
+
handle = raw_payload.get("handle")
|
386
|
+
adapter = raw_payload.get("adapter")
|
390
387
|
|
391
|
-
# --- Health Check Logic
|
388
|
+
# --- Health Check Logic (Keep as is) ---
|
392
389
|
if kind == "HealthCheck":
|
393
390
|
print(f"Received HealthCheck message {message_id}")
|
394
391
|
health_response = {
|
@@ -413,109 +410,180 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
413
410
|
return # Exit early for health checks
|
414
411
|
# --- End Health Check Logic ---
|
415
412
|
|
416
|
-
# Parse
|
413
|
+
# Parse content if it's a string (e.g., double-encoded JSON)
|
417
414
|
if isinstance(content_raw, str):
|
418
415
|
try:
|
419
416
|
content = json.loads(content_raw)
|
420
417
|
except json.JSONDecodeError:
|
421
|
-
content = content_raw
|
418
|
+
content = content_raw # Keep as string if not valid JSON
|
422
419
|
else:
|
423
420
|
content = content_raw
|
424
421
|
|
425
|
-
print(f"Content: {content}")
|
422
|
+
# print(f"Content: {content}") # Reduce verbosity
|
426
423
|
|
427
|
-
#
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
424
|
+
# --- Construct Input Object using Imported Types ---
|
425
|
+
input_obj: Any = None
|
426
|
+
input_type_class = None
|
427
|
+
|
428
|
+
try:
|
429
|
+
# Try to get the actual model classes (they should be available via import)
|
430
|
+
# Need to handle potential NameErrors if imports failed silently
|
431
|
+
# Note: This assumes models are defined in the imported module scope
|
432
|
+
# Or imported by the imported module.
|
433
|
+
from nebu.processors.models import Message # Import needed message class
|
434
|
+
|
435
|
+
if is_stream_message:
|
436
|
+
message_class = Message # Use imported class
|
437
|
+
content_model_class = None
|
438
|
+
if content_type_name:
|
439
|
+
try:
|
440
|
+
# Assume content_type_name refers to a class available in the global scope
|
441
|
+
# (either from imported module or included objects)
|
442
|
+
# Use the globally managed imported_module and local_namespace
|
443
|
+
content_model_class = getattr(
|
444
|
+
imported_module, content_type_name, None
|
445
|
+
)
|
446
|
+
if content_model_class is None:
|
447
|
+
# Check in local_namespace from included objects as fallback
|
448
|
+
content_model_class = local_namespace.get(content_type_name)
|
449
|
+
if content_model_class is None:
|
450
|
+
print(
|
451
|
+
f"Warning: Content type class '{content_type_name}' not found in imported module or includes."
|
452
|
+
)
|
453
|
+
else:
|
454
|
+
print(f"Found content model class: {content_model_class}")
|
455
|
+
except AttributeError:
|
456
|
+
print(
|
457
|
+
f"Warning: Content type class '{content_type_name}' not found in imported module."
|
458
|
+
)
|
459
|
+
except Exception as e:
|
460
|
+
print(
|
461
|
+
f"Warning: Error resolving content type class '{content_type_name}': {e}"
|
462
|
+
)
|
463
|
+
|
464
|
+
if content_model_class:
|
465
|
+
try:
|
466
|
+
content_model = content_model_class.model_validate(content)
|
467
|
+
print(f"Validated content model: {content_model}")
|
468
|
+
input_obj = message_class(
|
469
|
+
kind=kind,
|
470
|
+
id=msg_id,
|
471
|
+
content=content_model,
|
472
|
+
created_at=int(created_at.timestamp()),
|
473
|
+
return_stream=return_stream,
|
474
|
+
user_id=user_id,
|
475
|
+
orgs=orgs,
|
476
|
+
handle=handle,
|
477
|
+
adapter=adapter,
|
478
|
+
)
|
479
|
+
except Exception as e:
|
480
|
+
print(
|
481
|
+
f"Error validating/creating content model '{content_type_name}': {e}. Falling back."
|
482
|
+
)
|
483
|
+
# Fallback to raw content in Message
|
484
|
+
input_obj = message_class(
|
485
|
+
kind=kind,
|
486
|
+
id=msg_id,
|
487
|
+
content=cast(Any, content),
|
488
|
+
created_at=int(created_at.timestamp()),
|
489
|
+
return_stream=return_stream,
|
490
|
+
user_id=user_id,
|
491
|
+
orgs=orgs,
|
492
|
+
handle=handle,
|
493
|
+
adapter=adapter,
|
494
|
+
)
|
495
|
+
else:
|
496
|
+
# No content type name or class found, use raw content
|
497
|
+
input_obj = message_class(
|
438
498
|
kind=kind,
|
439
499
|
id=msg_id,
|
440
|
-
content=
|
441
|
-
created_at=created_at,
|
500
|
+
content=cast(Any, content),
|
501
|
+
created_at=int(created_at.timestamp()),
|
442
502
|
return_stream=return_stream,
|
443
503
|
user_id=user_id,
|
444
504
|
orgs=orgs,
|
445
505
|
handle=handle,
|
446
506
|
adapter=adapter,
|
447
507
|
)
|
508
|
+
else: # Not a stream message, use the function's parameter type
|
509
|
+
param_type_name = (
|
510
|
+
param_type_str # Assume param_type_str holds the class name
|
511
|
+
)
|
512
|
+
# Attempt to resolve the parameter type class
|
513
|
+
try:
|
514
|
+
# Use the globally managed imported_module and local_namespace
|
515
|
+
input_type_class = (
|
516
|
+
getattr(imported_module, param_type_name, None)
|
517
|
+
if param_type_name
|
518
|
+
else None
|
519
|
+
)
|
520
|
+
if input_type_class is None and param_type_name:
|
521
|
+
input_type_class = local_namespace.get(param_type_name)
|
522
|
+
if input_type_class is None:
|
523
|
+
if param_type_name: # Only warn if a name was expected
|
524
|
+
print(
|
525
|
+
f"Warning: Input type class '{param_type_name}' not found. Passing raw content."
|
526
|
+
)
|
527
|
+
input_obj = content
|
528
|
+
else:
|
529
|
+
print(f"Found input model class: {input_type_class}")
|
530
|
+
input_obj = input_type_class.model_validate(content)
|
531
|
+
print(f"Validated input model: {input_obj}")
|
532
|
+
except AttributeError:
|
533
|
+
print(
|
534
|
+
f"Warning: Input type class '{param_type_name}' not found in imported module."
|
535
|
+
)
|
536
|
+
input_obj = content
|
448
537
|
except Exception as e:
|
449
|
-
print(
|
450
|
-
|
451
|
-
input_obj = local_namespace["Message"](
|
452
|
-
kind=kind,
|
453
|
-
id=msg_id,
|
454
|
-
content=content,
|
455
|
-
created_at=created_at,
|
456
|
-
return_stream=return_stream,
|
457
|
-
user_id=user_id,
|
458
|
-
orgs=orgs,
|
459
|
-
handle=handle,
|
460
|
-
adapter=adapter,
|
538
|
+
print(
|
539
|
+
f"Error resolving/validating input type '{param_type_name}': {e}. Passing raw content."
|
461
540
|
)
|
462
|
-
else:
|
463
|
-
# Just use the raw content
|
464
|
-
print("Using raw content")
|
465
|
-
input_obj = local_namespace["Message"](
|
466
|
-
kind=kind,
|
467
|
-
id=msg_id,
|
468
|
-
content=content,
|
469
|
-
created_at=created_at,
|
470
|
-
return_stream=return_stream,
|
471
|
-
user_id=user_id,
|
472
|
-
orgs=orgs,
|
473
|
-
handle=handle,
|
474
|
-
adapter=adapter,
|
475
|
-
)
|
476
|
-
else:
|
477
|
-
# Otherwise use the param type directly
|
478
|
-
try:
|
479
|
-
if param_type_str in local_namespace:
|
480
|
-
print(f"Validating content against {param_type_str}")
|
481
|
-
input_obj = local_namespace[param_type_str].model_validate(content)
|
482
|
-
else:
|
483
|
-
# If we can't find the exact type, just pass the content directly
|
484
541
|
input_obj = content
|
485
|
-
except Exception as e:
|
486
|
-
print(f"Error creating input model: {e}, using raw content")
|
487
|
-
raise e
|
488
542
|
|
489
|
-
|
543
|
+
except NameError as e:
|
544
|
+
print(
|
545
|
+
f"Error: Required class (e.g., Message or parameter type) not found. Import failed? {e}"
|
546
|
+
)
|
547
|
+
# Can't proceed without types, re-raise or handle error response
|
548
|
+
raise RuntimeError(f"Required class not found: {e}") from e
|
549
|
+
except Exception as e:
|
550
|
+
print(f"Error constructing input object: {e}")
|
551
|
+
raise # Re-raise unexpected errors during input construction
|
552
|
+
|
553
|
+
# print(f"Input object: {input_obj}") # Reduce verbosity
|
490
554
|
|
491
555
|
# Execute the function
|
556
|
+
print(f"Executing function...")
|
492
557
|
result = target_function(input_obj)
|
493
|
-
print(f"Result: {result}")
|
494
|
-
|
495
|
-
|
496
|
-
|
558
|
+
print(f"Result: {result}") # Reduce verbosity
|
559
|
+
|
560
|
+
# Convert result to dict if it's a Pydantic model
|
561
|
+
if hasattr(result, "model_dump"): # Use model_dump for Pydantic v2+
|
562
|
+
result_content = result.model_dump(mode="json") # Serialize properly
|
563
|
+
elif hasattr(result, "dict"): # Fallback for older Pydantic
|
564
|
+
result_content = result.dict()
|
565
|
+
else:
|
566
|
+
result_content = result # Assume JSON-serializable
|
497
567
|
|
498
568
|
# Prepare the response
|
499
569
|
response = {
|
500
570
|
"kind": "StreamResponseMessage",
|
501
571
|
"id": message_id,
|
502
|
-
"content":
|
572
|
+
"content": result_content,
|
503
573
|
"status": "success",
|
504
574
|
"created_at": datetime.now().isoformat(),
|
505
|
-
"user_id": user_id,
|
575
|
+
"user_id": user_id, # Pass user_id back
|
506
576
|
}
|
507
577
|
|
508
|
-
print(f"Response: {response}")
|
578
|
+
# print(f"Response: {response}") # Reduce verbosity
|
509
579
|
|
510
580
|
# Send the result to the return stream
|
511
581
|
if return_stream:
|
512
|
-
# Assert type again closer to usage for type checker clarity
|
513
582
|
assert isinstance(return_stream, str)
|
514
583
|
r.xadd(return_stream, {"data": json.dumps(response)})
|
515
584
|
print(f"Processed message {message_id}, result sent to {return_stream}")
|
516
585
|
|
517
586
|
# Acknowledge the message
|
518
|
-
# Assert types again closer to usage for type checker clarity
|
519
587
|
assert isinstance(REDIS_STREAM, str)
|
520
588
|
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
521
589
|
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
@@ -523,77 +591,201 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
|
|
523
591
|
except Exception as e:
|
524
592
|
print(f"Error processing message {message_id}: {e}")
|
525
593
|
traceback.print_exc()
|
594
|
+
_send_error_response(
|
595
|
+
message_id, str(e), traceback.format_exc(), return_stream, user_id
|
596
|
+
)
|
526
597
|
|
527
|
-
#
|
528
|
-
|
529
|
-
"kind": "StreamResponseMessage",
|
530
|
-
"id": message_id,
|
531
|
-
"content": {
|
532
|
-
"error": str(e),
|
533
|
-
"traceback": traceback.format_exc(),
|
534
|
-
},
|
535
|
-
"status": "error",
|
536
|
-
"created_at": datetime.now().isoformat(),
|
537
|
-
"user_id": user_id,
|
538
|
-
}
|
539
|
-
|
540
|
-
# Send the error to the return stream
|
541
|
-
if return_stream:
|
542
|
-
# Assert type again closer to usage for type checker clarity
|
543
|
-
assert isinstance(return_stream, str)
|
544
|
-
r.xadd(return_stream, {"data": json.dumps(error_response)})
|
545
|
-
else:
|
546
|
-
# Assert type again closer to usage for type checker clarity
|
598
|
+
# Acknowledge the message even if processing failed
|
599
|
+
try:
|
547
600
|
assert isinstance(REDIS_STREAM, str)
|
548
|
-
|
601
|
+
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
602
|
+
r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
|
603
|
+
print(f"Acknowledged failed message {message_id}")
|
604
|
+
except Exception as e_ack:
|
605
|
+
print(
|
606
|
+
f"CRITICAL: Failed to acknowledge failed message {message_id}: {e_ack}"
|
607
|
+
)
|
549
608
|
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
609
|
+
|
610
|
+
# --- Helper to Send Error Response ---
|
611
|
+
def _send_error_response(
|
612
|
+
message_id: str,
|
613
|
+
error_msg: str,
|
614
|
+
tb: str,
|
615
|
+
return_stream: Optional[str],
|
616
|
+
user_id: Optional[str],
|
617
|
+
):
|
618
|
+
"""Sends a standardized error response to Redis."""
|
619
|
+
global r, REDIS_STREAM # Access global Redis connection and stream name
|
620
|
+
|
621
|
+
error_response = {
|
622
|
+
"kind": "StreamResponseMessage",
|
623
|
+
"id": message_id,
|
624
|
+
"content": {
|
625
|
+
"error": error_msg,
|
626
|
+
"traceback": tb,
|
627
|
+
},
|
628
|
+
"status": "error",
|
629
|
+
"created_at": datetime.now(timezone.utc).isoformat(), # Use UTC
|
630
|
+
"user_id": user_id,
|
631
|
+
}
|
632
|
+
|
633
|
+
error_destination = f"{REDIS_STREAM}.errors" # Default error stream
|
634
|
+
if return_stream: # Prefer return_stream if available
|
635
|
+
error_destination = return_stream
|
636
|
+
|
637
|
+
try:
|
638
|
+
assert isinstance(error_destination, str)
|
639
|
+
r.xadd(error_destination, {"data": json.dumps(error_response)})
|
640
|
+
print(f"Sent error response for message {message_id} to {error_destination}")
|
641
|
+
except Exception as e_redis:
|
642
|
+
print(
|
643
|
+
f"CRITICAL: Failed to send error response for {message_id} to Redis: {e_redis}"
|
644
|
+
)
|
645
|
+
traceback.print_exc()
|
555
646
|
|
556
647
|
|
557
648
|
# Main loop
|
558
649
|
print(f"Starting consumer for stream {REDIS_STREAM} in group {REDIS_CONSUMER_GROUP}")
|
559
|
-
consumer_name = f"consumer-{os.getpid()}"
|
650
|
+
consumer_name = f"consumer-{os.getpid()}-{socket.gethostname()}" # More unique name
|
560
651
|
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
652
|
+
try:
|
653
|
+
while True:
|
654
|
+
try:
|
655
|
+
# --- Check for Code Updates ---
|
656
|
+
if entrypoint_abs_path: # Should always be set after init
|
657
|
+
try:
|
658
|
+
current_mtime = os.path.getmtime(entrypoint_abs_path)
|
659
|
+
if current_mtime > last_load_mtime:
|
660
|
+
print(
|
661
|
+
f"[Consumer] Detected change in entrypoint file: {entrypoint_abs_path}. Reloading code..."
|
662
|
+
)
|
663
|
+
(
|
664
|
+
reloaded_target_func,
|
665
|
+
reloaded_init_func,
|
666
|
+
reloaded_module,
|
667
|
+
reloaded_namespace,
|
668
|
+
new_mtime,
|
669
|
+
) = load_or_reload_user_code(
|
670
|
+
_module_path,
|
671
|
+
_function_name,
|
672
|
+
entrypoint_abs_path,
|
673
|
+
_init_func_name,
|
674
|
+
_included_object_sources,
|
675
|
+
)
|
566
676
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
677
|
+
if (
|
678
|
+
reloaded_target_func is not None
|
679
|
+
and reloaded_module is not None
|
680
|
+
):
|
681
|
+
print(
|
682
|
+
"[Consumer] Code reload successful. Updating functions."
|
683
|
+
)
|
684
|
+
target_function = reloaded_target_func
|
685
|
+
init_function = reloaded_init_func # Update init ref too, though it's already run
|
686
|
+
imported_module = reloaded_module
|
687
|
+
local_namespace = (
|
688
|
+
reloaded_namespace # Update namespace from includes
|
689
|
+
)
|
690
|
+
last_load_mtime = new_mtime
|
691
|
+
else:
|
692
|
+
print(
|
693
|
+
"[Consumer] Code reload failed. Continuing with previously loaded code."
|
694
|
+
)
|
695
|
+
# Optionally: Send an alert/log prominently that reload failed
|
696
|
+
|
697
|
+
except FileNotFoundError:
|
698
|
+
print(
|
699
|
+
f"[Consumer] Error: Entrypoint file '{entrypoint_abs_path}' not found during check. Cannot reload."
|
700
|
+
)
|
701
|
+
# Mark as non-runnable? Or just log?
|
702
|
+
target_function = None # Stop processing until file reappears?
|
703
|
+
imported_module = None
|
704
|
+
last_load_mtime = 0 # Reset mtime to force check next time
|
705
|
+
except Exception as e_reload_check:
|
706
|
+
print(f"[Consumer] Error checking/reloading code: {e_reload_check}")
|
707
|
+
traceback.print_exc()
|
708
|
+
else:
|
709
|
+
print(
|
710
|
+
"[Consumer] Warning: Entrypoint absolute path not set, cannot check for code updates."
|
711
|
+
)
|
574
712
|
|
575
|
-
|
576
|
-
|
577
|
-
|
713
|
+
# --- Read from Redis Stream ---
|
714
|
+
if target_function is None:
|
715
|
+
# If code failed to load initially or during reload, wait before retrying
|
716
|
+
print(
|
717
|
+
"[Consumer] Target function not loaded, waiting 5s before checking again..."
|
718
|
+
)
|
719
|
+
time.sleep(5)
|
720
|
+
continue # Skip reading from Redis until code is loaded
|
578
721
|
|
579
|
-
|
580
|
-
|
581
|
-
messages, list
|
582
|
-
), f"Expected list from xreadgroup, got {type(messages)}"
|
583
|
-
assert len(messages) > 0 # Ensure the list is not empty before indexing
|
722
|
+
assert isinstance(REDIS_STREAM, str)
|
723
|
+
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
584
724
|
|
585
|
-
|
725
|
+
# Simplified xreadgroup call - redis-py handles encoding now
|
726
|
+
# With decode_responses=True, redis-py expects str types here
|
727
|
+
streams_arg = {REDIS_STREAM: ">"}
|
728
|
+
messages = r.xreadgroup(
|
729
|
+
REDIS_CONSUMER_GROUP,
|
730
|
+
consumer_name,
|
731
|
+
streams_arg,
|
732
|
+
count=1,
|
733
|
+
block=5000, # Use milliseconds for block
|
734
|
+
)
|
586
735
|
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
process_message(message_id, message_data)
|
736
|
+
if not messages:
|
737
|
+
# print("[Consumer] No new messages.") # Reduce verbosity
|
738
|
+
continue
|
591
739
|
|
592
|
-
|
593
|
-
|
594
|
-
time.sleep(5) # Wait before retrying
|
740
|
+
# Assert messages is not None to help type checker (already implied by `if not messages`)
|
741
|
+
assert messages is not None
|
595
742
|
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
743
|
+
# Cast messages to expected type to satisfy type checker
|
744
|
+
typed_messages = cast(
|
745
|
+
List[Tuple[str, List[Tuple[str, Dict[str, str]]]]], messages
|
746
|
+
)
|
747
|
+
stream_name_str, stream_messages = typed_messages[0]
|
748
|
+
|
749
|
+
# for msg_id_bytes, msg_data_bytes_dict in stream_messages: # Original structure
|
750
|
+
for (
|
751
|
+
message_id_str,
|
752
|
+
message_data_str_dict,
|
753
|
+
) in stream_messages: # Structure with decode_responses=True
|
754
|
+
# message_id_str = msg_id_bytes.decode('utf-8') # No longer needed
|
755
|
+
# Decode keys/values in the message data dict
|
756
|
+
# message_data_str_dict = { k.decode('utf-8'): v.decode('utf-8')
|
757
|
+
# for k, v in msg_data_bytes_dict.items() } # No longer needed
|
758
|
+
# print(f"Processing message {message_id_str}") # Reduce verbosity
|
759
|
+
# print(f"Message data: {message_data_str_dict}") # Reduce verbosity
|
760
|
+
process_message(message_id_str, message_data_str_dict)
|
761
|
+
|
762
|
+
except ConnectionError as e:
|
763
|
+
print(f"Redis connection error: {e}. Reconnecting in 5s...")
|
764
|
+
time.sleep(5)
|
765
|
+
# Attempt to reconnect explicitly
|
766
|
+
try:
|
767
|
+
print("Attempting Redis reconnection...")
|
768
|
+
# Close existing potentially broken connection? `r.close()` if available
|
769
|
+
r = redis.from_url(REDIS_URL, decode_responses=True)
|
770
|
+
r.ping()
|
771
|
+
print("Reconnected to Redis.")
|
772
|
+
except Exception as recon_e:
|
773
|
+
print(f"Failed to reconnect to Redis: {recon_e}")
|
774
|
+
# Keep waiting
|
775
|
+
|
776
|
+
except ResponseError as e:
|
777
|
+
print(f"Redis command error: {e}")
|
778
|
+
# Should we exit or retry?
|
779
|
+
if "NOGROUP" in str(e):
|
780
|
+
print("Consumer group seems to have disappeared. Exiting.")
|
781
|
+
sys.exit(1)
|
782
|
+
time.sleep(1)
|
783
|
+
|
784
|
+
except Exception as e:
|
785
|
+
print(f"Unexpected error in main loop: {e}")
|
786
|
+
traceback.print_exc()
|
787
|
+
time.sleep(1)
|
788
|
+
|
789
|
+
finally:
|
790
|
+
print("Consumer loop exited.")
|
791
|
+
# Any other cleanup needed?
|