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.
@@ -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
- print("reading from stream...")
892
- try:
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..."
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
- 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,
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
- if (
916
- reloaded_target_func is not None
917
- and reloaded_module is not None
918
- ):
919
- print(
920
- "[Consumer] Code reload successful. Updating functions."
921
- )
922
- target_function = reloaded_target_func
923
- init_function = reloaded_init_func # Update init ref too, though it's already run
924
- imported_module = reloaded_module
925
- local_namespace = (
926
- reloaded_namespace # Update namespace from includes
927
- )
928
- last_load_mtime = new_mtime
929
- else:
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
- except FileNotFoundError:
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] Error: Entrypoint file '{entrypoint_abs_path}' not found during check. Cannot reload."
1004
+ f"[Consumer] Claimed {len(claimed_messages[0][1])} pending message(s). Processing..."
938
1005
  )
939
- # Mark as non-runnable? Or just log?
940
- target_function = None # Stop processing until file reappears?
941
- imported_module = None
942
- last_load_mtime = 0 # Reset mtime to force check next time
943
- except Exception as e_reload_check:
944
- print(f"[Consumer] Error checking/reloading code: {e_reload_check}")
945
- traceback.print_exc()
946
- else:
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
- "[Consumer] Warning: Entrypoint absolute path not set, cannot check for code updates."
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
- # --- Read from Redis Stream ---
952
- if target_function is None:
953
- # If code failed to load initially or during reload, wait before retrying
954
- print(
955
- "[Consumer] Target function not loaded, waiting 5s before checking again..."
956
- )
957
- time.sleep(5)
958
- continue # Skip reading from Redis until code is loaded
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
- assert isinstance(REDIS_STREAM, str)
961
- assert isinstance(REDIS_CONSUMER_GROUP, str)
1056
+ assert isinstance(REDIS_STREAM, str)
1057
+ assert isinstance(REDIS_CONSUMER_GROUP, str)
962
1058
 
963
- streams_arg: Dict[str, str] = {REDIS_STREAM: ">"}
1059
+ streams_arg: Dict[str, str] = {REDIS_STREAM: ">"}
964
1060
 
965
- # With decode_responses=True, redis-py expects str types here
966
- messages = r.xreadgroup(
967
- REDIS_CONSUMER_GROUP,
968
- consumer_name,
969
- streams_arg, # type: ignore[arg-type]
970
- count=1,
971
- block=5000, # Use milliseconds for block
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
- if not messages:
975
- # print("[Consumer] No new messages.") # Reduce verbosity
976
- continue
1070
+ if not messages:
1071
+ # print("[Consumer] No new messages.") # Reduce verbosity
1072
+ continue
977
1073
 
978
- # Assert messages is not None to help type checker (already implied by `if not messages`)
979
- assert messages is not None
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
- # Cast messages to expected type to satisfy type checker
982
- typed_messages = cast(
983
- List[Tuple[str, List[Tuple[str, Dict[str, str]]]]], messages
984
- )
985
- stream_name_str, stream_messages = typed_messages[0]
986
-
987
- # for msg_id_bytes, msg_data_bytes_dict in stream_messages: # Original structure
988
- for (
989
- message_id_str,
990
- message_data_str_dict,
991
- ) in stream_messages: # Structure with decode_responses=True
992
- # message_id_str = msg_id_bytes.decode('utf-8') # No longer needed
993
- # Decode keys/values in the message data dict
994
- # message_data_str_dict = { k.decode('utf-8'): v.decode('utf-8')
995
- # for k, v in msg_data_bytes_dict.items() } # No longer needed
996
- # print(f"Processing message {message_id_str}") # Reduce verbosity
997
- # print(f"Message data: {message_data_str_dict}") # Reduce verbosity
998
- process_message(message_id_str, message_data_str_dict)
999
-
1000
- except ConnectionError as e:
1001
- print(f"Redis connection error: {e}. Reconnecting in 5s...")
1002
- time.sleep(5)
1003
- # Attempt to reconnect explicitly
1004
- try:
1005
- print("Attempting Redis reconnection...")
1006
- # Close existing potentially broken connection? `r.close()` if available
1007
- r = redis.from_url(REDIS_URL, decode_responses=True)
1008
- r.ping()
1009
- print("Reconnected to Redis.")
1010
- except Exception as recon_e:
1011
- print(f"Failed to reconnect to Redis: {recon_e}")
1012
- # Keep waiting
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
- except Exception as e:
1023
- print(f"Unexpected error in main loop: {e}")
1024
- traceback.print_exc()
1025
- time.sleep(1)
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.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nebu
3
- Version: 0.1.79
3
+ Version: 0.1.80
4
4
  Summary: A globally distributed container runtime
5
5
  Requires-Python: >=3.10.14
6
6
  Description-Content-Type: text/markdown
@@ -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=nWJmlTwJxfadMEQQwzwOJNPCAkcxqIzOpshMMbnfa-o,43617
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.79.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
27
- nebu-0.1.79.dist-info/METADATA,sha256=CGL9pnRUrW4gx4A6tIaOgxgC3WRlxUSANUHJcBuhLPc,1731
28
- nebu-0.1.79.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
29
- nebu-0.1.79.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
30
- nebu-0.1.79.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.0)
2
+ Generator: setuptools (80.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5