nebu 0.1.79__py3-none-any.whl → 0.1.80__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/processors/consumer.py +215 -119
- {nebu-0.1.79.dist-info → nebu-0.1.80.dist-info}/METADATA +1 -1
- {nebu-0.1.79.dist-info → nebu-0.1.80.dist-info}/RECORD +6 -6
- {nebu-0.1.79.dist-info → nebu-0.1.80.dist-info}/WHEEL +1 -1
- {nebu-0.1.79.dist-info → nebu-0.1.80.dist-info}/licenses/LICENSE +0 -0
- {nebu-0.1.79.dist-info → nebu-0.1.80.dist-info}/top_level.txt +0 -0
nebu/processors/consumer.py
CHANGED
@@ -885,144 +885,240 @@ def _send_error_response(
|
|
885
885
|
# Main loop
|
886
886
|
print(f"Starting consumer for stream {REDIS_STREAM} in group {REDIS_CONSUMER_GROUP}")
|
887
887
|
consumer_name = f"consumer-{os.getpid()}-{socket.gethostname()}" # More unique name
|
888
|
+
MIN_IDLE_TIME_MS = 60000 # Minimum idle time in milliseconds (e.g., 60 seconds)
|
889
|
+
CLAIM_COUNT = 10 # Max messages to claim at once
|
888
890
|
|
889
891
|
try:
|
890
892
|
while True:
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
893
|
+
# --- Check for Code Updates ---
|
894
|
+
if entrypoint_abs_path: # Should always be set after init
|
895
|
+
try:
|
896
|
+
current_mtime = os.path.getmtime(entrypoint_abs_path)
|
897
|
+
if current_mtime > last_load_mtime:
|
898
|
+
print(
|
899
|
+
f"[Consumer] Detected change in entrypoint file: {entrypoint_abs_path}. Reloading code..."
|
900
|
+
)
|
901
|
+
(
|
902
|
+
reloaded_target_func,
|
903
|
+
reloaded_init_func,
|
904
|
+
reloaded_module,
|
905
|
+
reloaded_namespace,
|
906
|
+
new_mtime,
|
907
|
+
) = load_or_reload_user_code(
|
908
|
+
_module_path,
|
909
|
+
_function_name,
|
910
|
+
entrypoint_abs_path,
|
911
|
+
_init_func_name,
|
912
|
+
_included_object_sources,
|
913
|
+
)
|
914
|
+
|
915
|
+
if reloaded_target_func is not None and reloaded_module is not None:
|
916
|
+
print("[Consumer] Code reload successful. Updating functions.")
|
917
|
+
target_function = reloaded_target_func
|
918
|
+
init_function = reloaded_init_func # Update init ref too, though it's already run
|
919
|
+
imported_module = reloaded_module
|
920
|
+
local_namespace = (
|
921
|
+
reloaded_namespace # Update namespace from includes
|
900
922
|
)
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
reloaded_namespace,
|
906
|
-
new_mtime,
|
907
|
-
) = load_or_reload_user_code(
|
908
|
-
_module_path,
|
909
|
-
_function_name,
|
910
|
-
entrypoint_abs_path,
|
911
|
-
_init_func_name,
|
912
|
-
_included_object_sources,
|
923
|
+
last_load_mtime = new_mtime
|
924
|
+
else:
|
925
|
+
print(
|
926
|
+
"[Consumer] Code reload failed. Continuing with previously loaded code."
|
913
927
|
)
|
928
|
+
# Optionally: Send an alert/log prominently that reload failed
|
914
929
|
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
print(
|
931
|
-
"[Consumer] Code reload failed. Continuing with previously loaded code."
|
932
|
-
)
|
933
|
-
# Optionally: Send an alert/log prominently that reload failed
|
930
|
+
except FileNotFoundError:
|
931
|
+
print(
|
932
|
+
f"[Consumer] Error: Entrypoint file '{entrypoint_abs_path}' not found during check. Cannot reload."
|
933
|
+
)
|
934
|
+
# Mark as non-runnable? Or just log?
|
935
|
+
target_function = None # Stop processing until file reappears?
|
936
|
+
imported_module = None
|
937
|
+
last_load_mtime = 0 # Reset mtime to force check next time
|
938
|
+
except Exception as e_reload_check:
|
939
|
+
print(f"[Consumer] Error checking/reloading code: {e_reload_check}")
|
940
|
+
traceback.print_exc()
|
941
|
+
else:
|
942
|
+
print(
|
943
|
+
"[Consumer] Warning: Entrypoint absolute path not set, cannot check for code updates."
|
944
|
+
)
|
934
945
|
|
935
|
-
|
946
|
+
# --- Claim Old Pending Messages ---
|
947
|
+
try:
|
948
|
+
if target_function is not None: # Only claim if we can process
|
949
|
+
assert isinstance(REDIS_STREAM, str)
|
950
|
+
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
951
|
+
|
952
|
+
# Claim messages pending for longer than MIN_IDLE_TIME_MS for *this* consumer
|
953
|
+
# xautoclaim returns (next_id, claimed_messages_list)
|
954
|
+
# Note: We don't need next_id if we always start from '0-0'
|
955
|
+
# but redis-py < 5 requires it to be handled.
|
956
|
+
# We only get messages assigned to *this* consumer_name
|
957
|
+
claim_result = r.xautoclaim(
|
958
|
+
name=REDIS_STREAM,
|
959
|
+
groupname=REDIS_CONSUMER_GROUP,
|
960
|
+
consumername=consumer_name,
|
961
|
+
min_idle_time=MIN_IDLE_TIME_MS,
|
962
|
+
start_id="0-0", # Check from the beginning of the PEL
|
963
|
+
count=CLAIM_COUNT,
|
964
|
+
)
|
965
|
+
|
966
|
+
# Compatibility check for redis-py versions
|
967
|
+
# Newer versions (>=5.0) return a tuple: (next_id, messages, count_deleted)
|
968
|
+
# Older versions (e.g., 4.x) return a list: [next_id, messages] or just messages if redis < 6.2
|
969
|
+
# We primarily care about the 'messages' part.
|
970
|
+
claimed_messages = None
|
971
|
+
if isinstance(claim_result, tuple) and len(claim_result) >= 2:
|
972
|
+
# next_id_bytes, claimed_messages = claim_result # Original structure
|
973
|
+
_next_id, claimed_messages_list = claim_result[
|
974
|
+
:2
|
975
|
+
] # Handle tuple structure (>=5.0)
|
976
|
+
# claimed_messages need to be processed like xreadgroup results
|
977
|
+
# Wrap in the stream name structure expected by the processing loop
|
978
|
+
if claimed_messages_list:
|
979
|
+
# Assume decode_responses=True is set, so use string directly
|
980
|
+
claimed_messages = [(REDIS_STREAM, claimed_messages_list)]
|
981
|
+
|
982
|
+
elif isinstance(claim_result, list) and claim_result:
|
983
|
+
# Handle older redis-py versions or direct message list if redis server < 6.2
|
984
|
+
# Check if the first element might be the next_id
|
985
|
+
if isinstance(
|
986
|
+
claim_result[0], (str, bytes)
|
987
|
+
): # Likely [next_id, messages] structure
|
988
|
+
if len(claim_result) > 1 and isinstance(claim_result[1], list):
|
989
|
+
_next_id, claimed_messages_list = claim_result[:2]
|
990
|
+
if claimed_messages_list:
|
991
|
+
# Assume decode_responses=True is set
|
992
|
+
claimed_messages = [
|
993
|
+
(REDIS_STREAM, claimed_messages_list)
|
994
|
+
]
|
995
|
+
elif isinstance(
|
996
|
+
claim_result[0], tuple
|
997
|
+
): # Direct list of messages [[id, data], ...]
|
998
|
+
claimed_messages_list = claim_result
|
999
|
+
# Assume decode_responses=True is set
|
1000
|
+
claimed_messages = [(REDIS_STREAM, claimed_messages_list)]
|
1001
|
+
|
1002
|
+
if claimed_messages:
|
936
1003
|
print(
|
937
|
-
f"[Consumer]
|
1004
|
+
f"[Consumer] Claimed {len(claimed_messages[0][1])} pending message(s). Processing..."
|
938
1005
|
)
|
939
|
-
#
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
1006
|
+
# Process claimed messages immediately
|
1007
|
+
# Cast messages to expected type to satisfy type checker
|
1008
|
+
typed_messages = cast(
|
1009
|
+
List[Tuple[str, List[Tuple[str, Dict[str, str]]]]],
|
1010
|
+
claimed_messages,
|
1011
|
+
)
|
1012
|
+
stream_name_str, stream_messages = typed_messages[0]
|
1013
|
+
for (
|
1014
|
+
message_id_str,
|
1015
|
+
message_data_str_dict,
|
1016
|
+
) in stream_messages:
|
1017
|
+
print(f"[Consumer] Processing claimed message {message_id_str}")
|
1018
|
+
process_message(message_id_str, message_data_str_dict)
|
1019
|
+
# After processing claimed messages, loop back to check for more potentially
|
1020
|
+
# This avoids immediately blocking on XREADGROUP if there were claimed messages
|
1021
|
+
continue
|
1022
|
+
|
1023
|
+
except ResponseError as e_claim:
|
1024
|
+
# Handle specific errors like NOGROUP gracefully if needed
|
1025
|
+
if "NOGROUP" in str(e_claim):
|
947
1026
|
print(
|
948
|
-
"
|
1027
|
+
f"Consumer group {REDIS_CONSUMER_GROUP} not found during xautoclaim. Exiting."
|
949
1028
|
)
|
1029
|
+
sys.exit(1)
|
1030
|
+
else:
|
1031
|
+
print(f"[Consumer] Error during XAUTOCLAIM: {e_claim}")
|
1032
|
+
# Decide if this is fatal or recoverable
|
1033
|
+
time.sleep(5) # Wait before retrying claim
|
1034
|
+
except ConnectionError as e_claim_conn:
|
1035
|
+
print(
|
1036
|
+
f"Redis connection error during XAUTOCLAIM: {e_claim_conn}. Will attempt reconnect in main loop."
|
1037
|
+
)
|
1038
|
+
# Let the main ConnectionError handler below deal with reconnection
|
1039
|
+
time.sleep(5) # Avoid tight loop on connection errors during claim
|
1040
|
+
except Exception as e_claim_other:
|
1041
|
+
print(
|
1042
|
+
f"[Consumer] Unexpected error during XAUTOCLAIM/processing claimed messages: {e_claim_other}"
|
1043
|
+
)
|
1044
|
+
traceback.print_exc()
|
1045
|
+
time.sleep(5) # Wait before retrying
|
950
1046
|
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
1047
|
+
# --- Read New Messages from Redis Stream ---
|
1048
|
+
if target_function is None:
|
1049
|
+
# If code failed to load initially or during reload, wait before retrying
|
1050
|
+
print(
|
1051
|
+
"[Consumer] Target function not loaded, waiting 5s before checking again..."
|
1052
|
+
)
|
1053
|
+
time.sleep(5)
|
1054
|
+
continue # Skip reading from Redis until code is loaded
|
959
1055
|
|
960
|
-
|
961
|
-
|
1056
|
+
assert isinstance(REDIS_STREAM, str)
|
1057
|
+
assert isinstance(REDIS_CONSUMER_GROUP, str)
|
962
1058
|
|
963
|
-
|
1059
|
+
streams_arg: Dict[str, str] = {REDIS_STREAM: ">"}
|
964
1060
|
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
1061
|
+
# With decode_responses=True, redis-py expects str types here
|
1062
|
+
messages = r.xreadgroup(
|
1063
|
+
REDIS_CONSUMER_GROUP,
|
1064
|
+
consumer_name,
|
1065
|
+
streams_arg, # type: ignore[arg-type]
|
1066
|
+
count=1,
|
1067
|
+
block=5000, # Use milliseconds for block
|
1068
|
+
)
|
973
1069
|
|
974
|
-
|
975
|
-
|
976
|
-
|
1070
|
+
if not messages:
|
1071
|
+
# print("[Consumer] No new messages.") # Reduce verbosity
|
1072
|
+
continue
|
977
1073
|
|
978
|
-
|
979
|
-
|
1074
|
+
# Assert messages is not None to help type checker (already implied by `if not messages`)
|
1075
|
+
assert messages is not None
|
980
1076
|
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
except ResponseError as e:
|
1015
|
-
print(f"Redis command error: {e}")
|
1016
|
-
# Should we exit or retry?
|
1017
|
-
if "NOGROUP" in str(e):
|
1018
|
-
print("Consumer group seems to have disappeared. Exiting.")
|
1019
|
-
sys.exit(1)
|
1020
|
-
time.sleep(1)
|
1077
|
+
# Cast messages to expected type to satisfy type checker
|
1078
|
+
typed_messages = cast(
|
1079
|
+
List[Tuple[str, List[Tuple[str, Dict[str, str]]]]], messages
|
1080
|
+
)
|
1081
|
+
stream_name_str, stream_messages = typed_messages[0]
|
1082
|
+
|
1083
|
+
# for msg_id_bytes, msg_data_bytes_dict in stream_messages: # Original structure
|
1084
|
+
for (
|
1085
|
+
message_id_str,
|
1086
|
+
message_data_str_dict,
|
1087
|
+
) in stream_messages: # Structure with decode_responses=True
|
1088
|
+
# message_id_str = msg_id_bytes.decode('utf-8') # No longer needed
|
1089
|
+
# Decode keys/values in the message data dict
|
1090
|
+
# message_data_str_dict = { k.decode('utf-8'): v.decode('utf-8')
|
1091
|
+
# for k, v in msg_data_bytes_dict.items() } # No longer needed
|
1092
|
+
# print(f"Processing message {message_id_str}") # Reduce verbosity
|
1093
|
+
# print(f"Message data: {message_data_str_dict}") # Reduce verbosity
|
1094
|
+
process_message(message_id_str, message_data_str_dict)
|
1095
|
+
|
1096
|
+
except ConnectionError as e:
|
1097
|
+
print(f"Redis connection error: {e}. Reconnecting in 5s...")
|
1098
|
+
time.sleep(5)
|
1099
|
+
# Attempt to reconnect explicitly
|
1100
|
+
try:
|
1101
|
+
print("Attempting Redis reconnection...")
|
1102
|
+
# Close existing potentially broken connection? `r.close()` if available
|
1103
|
+
r = redis.from_url(REDIS_URL, decode_responses=True)
|
1104
|
+
r.ping()
|
1105
|
+
print("Reconnected to Redis.")
|
1106
|
+
except Exception as recon_e:
|
1107
|
+
print(f"Failed to reconnect to Redis: {recon_e}")
|
1108
|
+
# Keep waiting
|
1021
1109
|
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1110
|
+
except ResponseError as e:
|
1111
|
+
print(f"Redis command error: {e}")
|
1112
|
+
# Should we exit or retry?
|
1113
|
+
if "NOGROUP" in str(e):
|
1114
|
+
print("Consumer group seems to have disappeared. Exiting.")
|
1115
|
+
sys.exit(1)
|
1116
|
+
time.sleep(1)
|
1117
|
+
|
1118
|
+
except Exception as e:
|
1119
|
+
print(f"Unexpected error in main loop: {e}")
|
1120
|
+
traceback.print_exc()
|
1121
|
+
time.sleep(1)
|
1026
1122
|
|
1027
1123
|
finally:
|
1028
1124
|
print("Consumer loop exited.")
|
@@ -14,7 +14,7 @@ nebu/containers/models.py,sha256=0j6NGy4yto-enRDh_4JH_ZTbHrLdSpuMOqNQPnIrwC4,681
|
|
14
14
|
nebu/containers/server.py,sha256=yFa2Y9PzBn59E1HftKiv0iapPonli2rbGAiU6r-wwe0,2513
|
15
15
|
nebu/namespaces/models.py,sha256=EqUOpzhVBhvJw2P92ONDUbIgC31M9jMmcaG5vyOrsWg,497
|
16
16
|
nebu/namespaces/namespace.py,sha256=Q_EDH7BgQrTkaDh_l4tbo22qpq-uARfIk8ZPBLjITGY,4967
|
17
|
-
nebu/processors/consumer.py,sha256=
|
17
|
+
nebu/processors/consumer.py,sha256=2_pH__T2in8BTwMBqR-DPZtEKX7np5okp5snP-kVQ5g,48567
|
18
18
|
nebu/processors/consumer_process_worker.py,sha256=tF5KU3Rnmzfc3Y0cM8J5nwGg1cJMe-ry0FmMSgGvXrY,31765
|
19
19
|
nebu/processors/decorate.py,sha256=U-NjFszyfKD6ACEyPJogFCbOPsfRYJUgGobLzfaHwD8,54766
|
20
20
|
nebu/processors/default.py,sha256=W4slJenG59rvyTlJ7gRp58eFfXcNOTT2Hfi6zzJAobI,365
|
@@ -23,8 +23,8 @@ nebu/processors/processor.py,sha256=OgEK8Fz0ehSe_VFiNsxweVKZIckhgVvQQ11NNffYZqA,
|
|
23
23
|
nebu/processors/remote.py,sha256=TeAIPGEMqnDIb7H1iett26IEZrBlcbPB_-DSm6jcH1E,1285
|
24
24
|
nebu/redis/models.py,sha256=coPovAcVXnOU1Xh_fpJL4PO3QctgK9nBe5QYoqEcnxg,1230
|
25
25
|
nebu/services/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
|
-
nebu-0.1.
|
27
|
-
nebu-0.1.
|
28
|
-
nebu-0.1.
|
29
|
-
nebu-0.1.
|
30
|
-
nebu-0.1.
|
26
|
+
nebu-0.1.80.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
27
|
+
nebu-0.1.80.dist-info/METADATA,sha256=DXHXKn8jWc84RMqFzPvl4REz0CG0r956mmDCIJEG8WU,1731
|
28
|
+
nebu-0.1.80.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
|
29
|
+
nebu-0.1.80.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
|
30
|
+
nebu-0.1.80.dist-info/RECORD,,
|
File without changes
|
File without changes
|