nebu 0.1.117__tar.gz → 0.1.119__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.
Files changed (36) hide show
  1. {nebu-0.1.117/src/nebu.egg-info → nebu-0.1.119}/PKG-INFO +1 -1
  2. {nebu-0.1.117 → nebu-0.1.119}/pyproject.toml +1 -1
  3. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/processors/consumer.py +197 -20
  4. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/processors/consumer_health_worker.py +147 -6
  5. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/processors/models.py +6 -0
  6. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/processors/processor.py +53 -30
  7. {nebu-0.1.117 → nebu-0.1.119/src/nebu.egg-info}/PKG-INFO +1 -1
  8. {nebu-0.1.117 → nebu-0.1.119}/LICENSE +0 -0
  9. {nebu-0.1.117 → nebu-0.1.119}/README.md +0 -0
  10. {nebu-0.1.117 → nebu-0.1.119}/setup.cfg +0 -0
  11. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/__init__.py +0 -0
  12. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/auth.py +0 -0
  13. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/builders/builder.py +0 -0
  14. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/builders/models.py +0 -0
  15. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/cache.py +0 -0
  16. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/config.py +0 -0
  17. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/containers/container.py +0 -0
  18. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/containers/models.py +0 -0
  19. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/data.py +0 -0
  20. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/errors.py +0 -0
  21. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/logging.py +0 -0
  22. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/meta.py +0 -0
  23. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/namespaces/models.py +0 -0
  24. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/namespaces/namespace.py +0 -0
  25. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/orign.py +0 -0
  26. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/processors/consumer_process_worker.py +0 -0
  27. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/processors/decorate.py +0 -0
  28. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/processors/default.py +0 -0
  29. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/redis/models.py +0 -0
  30. {nebu-0.1.117 → nebu-0.1.119}/src/nebu/services/service.py +0 -0
  31. {nebu-0.1.117 → nebu-0.1.119}/src/nebu.egg-info/SOURCES.txt +0 -0
  32. {nebu-0.1.117 → nebu-0.1.119}/src/nebu.egg-info/dependency_links.txt +0 -0
  33. {nebu-0.1.117 → nebu-0.1.119}/src/nebu.egg-info/requires.txt +0 -0
  34. {nebu-0.1.117 → nebu-0.1.119}/src/nebu.egg-info/top_level.txt +0 -0
  35. {nebu-0.1.117 → nebu-0.1.119}/tests/test_bucket.py +0 -0
  36. {nebu-0.1.117 → nebu-0.1.119}/tests/test_containers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nebu
3
- Version: 0.1.117
3
+ Version: 0.1.119
4
4
  Summary: A globally distributed container runtime
5
5
  Requires-Python: >=3.10.14
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nebu"
3
- version = "0.1.117"
3
+ version = "0.1.119"
4
4
  description = "A globally distributed container runtime"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10.14"
@@ -388,7 +388,13 @@ def start_health_check_subprocess() -> Optional[subprocess.Popen]:
388
388
  """Start the health check consumer subprocess."""
389
389
  global REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP
390
390
 
391
+ print(f"[DEBUG] start_health_check_subprocess called")
392
+ print(f"[DEBUG] REDIS_URL: {REDIS_URL}")
393
+ print(f"[DEBUG] REDIS_HEALTH_STREAM: {REDIS_HEALTH_STREAM}")
394
+ print(f"[DEBUG] REDIS_HEALTH_CONSUMER_GROUP: {REDIS_HEALTH_CONSUMER_GROUP}")
395
+
391
396
  if not all([REDIS_URL, REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP]):
397
+ print(f"[DEBUG] Health check not configured - missing required variables")
392
398
  logger.warning(
393
399
  "[Consumer] Health check stream not configured. Health consumer subprocess not started."
394
400
  )
@@ -399,6 +405,11 @@ def start_health_check_subprocess() -> Optional[subprocess.Popen]:
399
405
  assert isinstance(REDIS_HEALTH_STREAM, str)
400
406
  assert isinstance(REDIS_HEALTH_CONSUMER_GROUP, str)
401
407
 
408
+ print(
409
+ f"[DEBUG] Starting health check subprocess for stream: {REDIS_HEALTH_STREAM}"
410
+ )
411
+ print(f"[DEBUG] Health consumer group: {REDIS_HEALTH_CONSUMER_GROUP}")
412
+
402
413
  # Prepare environment variables for the subprocess
403
414
  health_env = os.environ.copy()
404
415
  health_env["REDIS_HEALTH_STREAM"] = REDIS_HEALTH_STREAM
@@ -412,6 +423,8 @@ def start_health_check_subprocess() -> Optional[subprocess.Popen]:
412
423
  "nebu.processors.consumer_health_worker",
413
424
  ]
414
425
 
426
+ print(f"[DEBUG] Health subprocess command: {' '.join(health_cmd)}")
427
+
415
428
  process = subprocess.Popen(
416
429
  health_cmd,
417
430
  stdout=subprocess.PIPE,
@@ -422,12 +435,17 @@ def start_health_check_subprocess() -> Optional[subprocess.Popen]:
422
435
  bufsize=1, # Line buffered
423
436
  )
424
437
 
438
+ print(
439
+ f"[DEBUG] Health check subprocess started successfully with PID {process.pid}"
440
+ )
425
441
  logger.info(
426
442
  f"[Consumer] Health check subprocess started with PID {process.pid}"
427
443
  )
428
444
  return process
429
445
 
430
446
  except Exception as e:
447
+ print(f"[DEBUG] Failed to start health check subprocess: {e}")
448
+ print(f"[DEBUG] Exception type: {type(e)}")
431
449
  logger.error(f"[Consumer] Failed to start health check subprocess: {e}")
432
450
  logger.exception("Health Subprocess Start Error Traceback:")
433
451
  return None
@@ -435,13 +453,17 @@ def start_health_check_subprocess() -> Optional[subprocess.Popen]:
435
453
 
436
454
  def monitor_health_subprocess(process: subprocess.Popen) -> None:
437
455
  """Monitor the health check subprocess and log its output."""
456
+ print(f"[DEBUG] monitor_health_subprocess started for PID {process.pid}")
438
457
  try:
439
458
  # Read output from the subprocess
440
459
  if process.stdout:
441
460
  for line in iter(process.stdout.readline, ""):
461
+ print(f"[DEBUG] [HealthSubprocess] {line.strip()}")
442
462
  logger.info(f"[HealthSubprocess] {line.strip()}")
443
463
  process.stdout.close() if process.stdout else None
464
+ print(f"[DEBUG] monitor_health_subprocess finished for PID {process.pid}")
444
465
  except Exception as e:
466
+ print(f"[DEBUG] Error monitoring health subprocess: {e}")
445
467
  logger.error(f"[Consumer] Error monitoring health subprocess: {e}")
446
468
 
447
469
 
@@ -449,33 +471,45 @@ def check_health_subprocess() -> bool:
449
471
  """Check if the health subprocess is still running and restart if needed."""
450
472
  global health_subprocess
451
473
 
474
+ print(f"[DEBUG] check_health_subprocess called")
475
+
452
476
  if health_subprocess is None:
477
+ print(f"[DEBUG] health_subprocess is None")
453
478
  return False
454
479
 
455
480
  # Check if process is still running
456
- if health_subprocess.poll() is None:
481
+ poll_result = health_subprocess.poll()
482
+ print(f"[DEBUG] health_subprocess.poll() returned: {poll_result}")
483
+
484
+ if poll_result is None:
485
+ print(f"[DEBUG] Health subprocess still running (PID {health_subprocess.pid})")
457
486
  return True # Still running
458
487
 
459
488
  # Process has exited
460
489
  exit_code = health_subprocess.returncode
490
+ print(f"[DEBUG] Health subprocess exited with code {exit_code}")
461
491
  logger.warning(
462
492
  f"[Consumer] Health subprocess exited with code {exit_code}. Restarting..."
463
493
  )
464
494
 
465
495
  # Start a new health subprocess
496
+ print(f"[DEBUG] Attempting to restart health subprocess...")
466
497
  health_subprocess = start_health_check_subprocess()
467
498
 
468
499
  if health_subprocess:
500
+ print(f"[DEBUG] Health subprocess restarted successfully")
469
501
  # Start monitoring thread for the new subprocess
470
502
  monitor_thread = threading.Thread(
471
503
  target=monitor_health_subprocess, args=(health_subprocess,), daemon=True
472
504
  )
473
505
  monitor_thread.start()
506
+ print(f"[DEBUG] Monitor thread started for health subprocess")
474
507
  logger.info(
475
508
  "[Consumer] Health subprocess restarted and monitoring thread started."
476
509
  )
477
510
  return True
478
511
  else:
512
+ print(f"[DEBUG] Failed to restart health subprocess")
479
513
  logger.error("[Consumer] Failed to restart health subprocess.")
480
514
  return False
481
515
 
@@ -714,6 +748,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
714
748
  user_id = None
715
749
  try:
716
750
  payload_str = message_data.get("data")
751
+ print(f"[DEBUG] Raw message_data: {message_data}")
752
+ print(f"[DEBUG] Payload string: {payload_str}")
753
+
717
754
  if (
718
755
  not payload_str
719
756
  ): # Covers None and empty string, isinstance check is redundant
@@ -722,6 +759,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
722
759
  )
723
760
  try:
724
761
  raw_payload = json.loads(payload_str)
762
+ print(f"[DEBUG] Parsed raw_payload: {json.dumps(raw_payload, indent=2)}")
725
763
  except json.JSONDecodeError as json_err:
726
764
  raise ValueError(f"Failed to parse JSON payload: {json_err}") from json_err
727
765
  if not isinstance(raw_payload, dict):
@@ -734,6 +772,9 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
734
772
  # --- Extract fields from the *inner* message content for HealthCheck and regular processing ---
735
773
  # The actual message content is inside raw_payload["content"]
736
774
  inner_content_data = raw_payload.get("content", {})
775
+ print(
776
+ f"[DEBUG] Extracted inner_content_data: {json.dumps(inner_content_data, indent=2) if isinstance(inner_content_data, dict) else inner_content_data}"
777
+ )
737
778
 
738
779
  # Add debug logging for content structure analysis
739
780
  logger.debug(f">> inner_content_data type: {type(inner_content_data)}")
@@ -746,6 +787,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
746
787
  # we can't reliably get 'kind' or other fields from it.
747
788
  # This case is more relevant for non-StreamMessage processors.
748
789
  # For HealthChecks, we expect a structured 'content'.
790
+ print(f"[DEBUG] inner_content_data is not a dict, using defaults")
749
791
  logger.warning(
750
792
  f"Received non-dict inner_content_data: {inner_content_data}. HealthCheck might be missed if applicable."
751
793
  )
@@ -762,6 +804,8 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
762
804
  and "content" in inner_content_data
763
805
  )
764
806
 
807
+ print(f"[DEBUG] has_message_structure: {has_message_structure}")
808
+ print(f"[DEBUG] inner_content_data keys: {list(inner_content_data.keys())}")
765
809
  logger.debug(f">> has_message_structure: {has_message_structure}")
766
810
 
767
811
  if has_message_structure:
@@ -770,6 +814,13 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
770
814
  inner_msg_id = inner_content_data.get("id", "")
771
815
  actual_content_to_process = inner_content_data.get("content", {})
772
816
  inner_created_at_str = inner_content_data.get("created_at")
817
+ print(f"[DEBUG] Using nested structure:")
818
+ print(f"[DEBUG] inner_kind: '{inner_kind}'")
819
+ print(f"[DEBUG] inner_msg_id: '{inner_msg_id}'")
820
+ print(
821
+ f"[DEBUG] actual_content_to_process: {actual_content_to_process}"
822
+ )
823
+ print(f"[DEBUG] inner_created_at_str: {inner_created_at_str}")
773
824
  logger.debug(
774
825
  f">> Using nested structure - inner_kind: {inner_kind}, inner_msg_id: {inner_msg_id}"
775
826
  )
@@ -786,6 +837,13 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
786
837
  inner_created_at_str = raw_payload.get(
787
838
  "created_at"
788
839
  ) # Get created_at from outer payload
840
+ print(f"[DEBUG] Using direct structure:")
841
+ print(f"[DEBUG] inner_kind: '{inner_kind}' (from outer payload)")
842
+ print(f"[DEBUG] inner_msg_id: '{inner_msg_id}' (from outer payload)")
843
+ print(
844
+ f"[DEBUG] actual_content_to_process: {actual_content_to_process}"
845
+ )
846
+ print(f"[DEBUG] inner_created_at_str: {inner_created_at_str}")
789
847
  logger.debug(
790
848
  f">> Using direct structure - inner_kind: {inner_kind}, inner_msg_id: {inner_msg_id}"
791
849
  )
@@ -810,31 +868,117 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
810
868
  handle = raw_payload.get("handle") # from outer
811
869
  adapter = raw_payload.get("adapter") # from outer
812
870
  api_key = raw_payload.get("api_key") # from outer
871
+
872
+ print(f"[DEBUG] Extracted outer envelope data:")
873
+ print(f"[DEBUG] return_stream: {return_stream}")
874
+ print(f"[DEBUG] user_id: {user_id}")
875
+ print(f"[DEBUG] orgs: {orgs}")
876
+ print(f"[DEBUG] handle: {handle}")
877
+ print(f"[DEBUG] adapter: {adapter}")
878
+ print(f"[DEBUG] api_key length: {len(api_key) if api_key else 0}")
879
+
813
880
  logger.debug(f">> Extracted API key length: {len(api_key) if api_key else 0}")
814
881
 
815
882
  # --- Health Check Logic ---
816
883
  # Use inner_kind for health check
817
- if inner_kind == "HealthCheck":
884
+ print(f"[DEBUG] Checking if message is HealthCheck. inner_kind: '{inner_kind}'")
885
+ print(
886
+ f"[DEBUG] Message kind comparison: '{inner_kind}' == 'HealthCheckRequest' -> {inner_kind == 'HealthCheckRequest'}"
887
+ )
888
+
889
+ if inner_kind == "HealthCheckRequest":
890
+ print(f"[DEBUG] *** HEALTH CHECK REQUEST MESSAGE DETECTED ***")
891
+ print(f"[DEBUG] Message ID: {message_id}")
892
+ print(f"[DEBUG] Inner message ID: {inner_msg_id}")
893
+ print(f"[DEBUG] Return stream: {return_stream}")
894
+ print(f"[DEBUG] User ID: {user_id}")
895
+ print(f"[DEBUG] Content: {actual_content_to_process}")
896
+
818
897
  logger.info(
819
- f"Received HealthCheck message {message_id} (inner_id: {inner_msg_id})"
898
+ f"Received HealthCheckRequest message {message_id} (inner_id: {inner_msg_id})"
820
899
  )
821
- health_response = {
822
- "kind": "StreamResponseMessage",
823
- "id": message_id, # Use outer message_id for response ID
824
- "content": {"status": "healthy", "checked_message_id": inner_msg_id},
825
- "status": "success",
826
- "created_at": datetime.now(timezone.utc).isoformat(),
827
- "user_id": user_id,
828
- }
829
- if return_stream:
830
- assert isinstance(return_stream, str)
831
- r.xadd(return_stream, {"data": json.dumps(health_response)})
832
- logger.info(f"Sent health check response to {return_stream}")
833
900
 
834
- assert isinstance(REDIS_STREAM, str)
835
- assert isinstance(REDIS_CONSUMER_GROUP, str)
836
- r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
837
- logger.info(f"Acknowledged HealthCheck message {message_id}")
901
+ # Forward to health stream for health worker subprocess to process
902
+ if REDIS_HEALTH_STREAM:
903
+ print(
904
+ f"[DEBUG] Forwarding health check to health stream: {REDIS_HEALTH_STREAM}"
905
+ )
906
+ try:
907
+ # Forward the entire message data to the health stream
908
+ health_message_data = {
909
+ "data": json.dumps(
910
+ {
911
+ "kind": inner_kind,
912
+ "id": inner_msg_id,
913
+ "content": actual_content_to_process,
914
+ "created_at": inner_created_at.isoformat(),
915
+ "return_stream": return_stream,
916
+ "user_id": user_id,
917
+ "orgs": orgs,
918
+ "handle": handle,
919
+ "adapter": adapter,
920
+ "api_key": api_key,
921
+ "original_message_id": message_id, # Include original message ID for tracking
922
+ }
923
+ )
924
+ }
925
+
926
+ print(
927
+ f"[DEBUG] Health message data to forward: {json.dumps(health_message_data, indent=2)}"
928
+ )
929
+
930
+ r.xadd(REDIS_HEALTH_STREAM, health_message_data) # type: ignore[arg-type]
931
+ print(
932
+ f"[DEBUG] Health check forwarded successfully to {REDIS_HEALTH_STREAM}"
933
+ )
934
+ logger.info(
935
+ f"Forwarded HealthCheckRequest {message_id} to health stream {REDIS_HEALTH_STREAM}"
936
+ )
937
+
938
+ except Exception as e:
939
+ print(
940
+ f"[DEBUG] ERROR forwarding health check to health stream: {e}"
941
+ )
942
+ logger.error(f"Error forwarding health check to health stream: {e}")
943
+ # Fall back to sending error response directly
944
+ _send_error_response(
945
+ message_id,
946
+ f"Failed to forward health check: {e}",
947
+ traceback.format_exc(),
948
+ return_stream,
949
+ user_id,
950
+ )
951
+ else:
952
+ print(
953
+ f"[DEBUG] No health stream configured, cannot forward health check"
954
+ )
955
+ logger.warning(
956
+ "No health stream configured for health check forwarding"
957
+ )
958
+ # Send error response since we can't process health checks
959
+ _send_error_response(
960
+ message_id,
961
+ "Health check stream not configured",
962
+ "REDIS_HEALTH_STREAM environment variable not set",
963
+ return_stream,
964
+ user_id,
965
+ )
966
+
967
+ # Acknowledge the original message since we've forwarded it
968
+ print(f"[DEBUG] Acknowledging original health check message {message_id}")
969
+ try:
970
+ assert isinstance(REDIS_STREAM, str)
971
+ assert isinstance(REDIS_CONSUMER_GROUP, str)
972
+ r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
973
+ print(
974
+ f"[DEBUG] Health check message {message_id} acknowledged successfully"
975
+ )
976
+ logger.info(f"Acknowledged HealthCheckRequest message {message_id}")
977
+ except Exception as e:
978
+ print(f"[DEBUG] ERROR acknowledging health check message: {e}")
979
+ logger.error(f"Error acknowledging health check message: {e}")
980
+
981
+ print(f"[DEBUG] *** HEALTH CHECK FORWARDING COMPLETE ***")
838
982
  return # Exit early for health checks
839
983
  # --- End Health Check Logic ---
840
984
 
@@ -1210,19 +1354,31 @@ logger.info(
1210
1354
 
1211
1355
  # Start the health check consumer subprocess
1212
1356
  if REDIS_HEALTH_STREAM and REDIS_HEALTH_CONSUMER_GROUP:
1357
+ print(f"[DEBUG] === HEALTH SUBPROCESS INITIALIZATION ===")
1358
+ print(f"[DEBUG] REDIS_HEALTH_STREAM is set: {REDIS_HEALTH_STREAM}")
1359
+ print(f"[DEBUG] REDIS_HEALTH_CONSUMER_GROUP is set: {REDIS_HEALTH_CONSUMER_GROUP}")
1360
+
1213
1361
  health_subprocess = start_health_check_subprocess()
1214
1362
  if health_subprocess:
1363
+ print(
1364
+ f"[DEBUG] Health subprocess started successfully, starting monitor thread..."
1365
+ )
1215
1366
  # Start monitoring thread for subprocess output
1216
1367
  monitor_thread = threading.Thread(
1217
1368
  target=monitor_health_subprocess, args=(health_subprocess,), daemon=True
1218
1369
  )
1219
1370
  monitor_thread.start()
1371
+ print(f"[DEBUG] Monitor thread started for health subprocess")
1220
1372
  logger.info(
1221
1373
  f"[Consumer] Health check subprocess for {REDIS_HEALTH_STREAM} started and monitoring thread started."
1222
1374
  )
1223
1375
  else:
1376
+ print(f"[DEBUG] Health subprocess failed to start")
1224
1377
  logger.error("[Consumer] Failed to start health check subprocess.")
1225
1378
  else:
1379
+ print(f"[DEBUG] === HEALTH SUBPROCESS NOT CONFIGURED ===")
1380
+ print(f"[DEBUG] REDIS_HEALTH_STREAM: {REDIS_HEALTH_STREAM}")
1381
+ print(f"[DEBUG] REDIS_HEALTH_CONSUMER_GROUP: {REDIS_HEALTH_CONSUMER_GROUP}")
1226
1382
  logger.warning(
1227
1383
  "[Consumer] Health check stream not configured. Health consumer subprocess not started."
1228
1384
  )
@@ -1232,10 +1388,15 @@ try:
1232
1388
  logger.debug(
1233
1389
  f"[{datetime.now(timezone.utc).isoformat()}] --- Top of main loop ---"
1234
1390
  ) # Added log
1391
+ print(f"[DEBUG] === MAIN LOOP ITERATION START ===")
1235
1392
 
1236
1393
  # --- Check Health Subprocess Status ---
1237
1394
  if health_subprocess:
1238
- check_health_subprocess()
1395
+ print(f"[DEBUG] Checking health subprocess status...")
1396
+ health_status = check_health_subprocess()
1397
+ print(f"[DEBUG] Health subprocess status check result: {health_status}")
1398
+ else:
1399
+ print(f"[DEBUG] No health subprocess to check")
1239
1400
 
1240
1401
  # --- Check for Code Updates ---
1241
1402
  if not disable_hot_reload:
@@ -1448,6 +1609,11 @@ try:
1448
1609
  logger.debug(
1449
1610
  f"[{datetime.now(timezone.utc).isoformat()}] Calling xreadgroup (block=5000ms)..."
1450
1611
  ) # Added log
1612
+ print(f"[DEBUG] About to call xreadgroup...")
1613
+ print(f"[DEBUG] Stream: {REDIS_STREAM}")
1614
+ print(f"[DEBUG] Consumer group: {REDIS_CONSUMER_GROUP}")
1615
+ print(f"[DEBUG] Consumer name: {consumer_name}")
1616
+
1451
1617
  messages = r.xreadgroup(
1452
1618
  REDIS_CONSUMER_GROUP,
1453
1619
  consumer_name,
@@ -1456,10 +1622,14 @@ try:
1456
1622
  block=5000, # Use milliseconds for block
1457
1623
  )
1458
1624
 
1625
+ print(f"[DEBUG] xreadgroup returned: {messages}")
1626
+ print(f"[DEBUG] Messages type: {type(messages)}")
1627
+
1459
1628
  if not messages:
1460
1629
  logger.trace(
1461
1630
  f"[{datetime.now(timezone.utc).isoformat()}] xreadgroup timed out (no new messages)."
1462
1631
  ) # Added log
1632
+ print(f"[DEBUG] No messages received (timeout or empty)")
1463
1633
  # logger.debug("[Consumer] No new messages.") # Reduce verbosity
1464
1634
  continue
1465
1635
  # Removed the else block here
@@ -1475,6 +1645,8 @@ try:
1475
1645
  stream_name_str, stream_messages = typed_messages[0]
1476
1646
  num_msgs = len(stream_messages)
1477
1647
 
1648
+ print(f"[DEBUG] Processing {num_msgs} message(s) from stream {stream_name_str}")
1649
+
1478
1650
  # Log reception and count before processing
1479
1651
  logger.info(
1480
1652
  f"[{datetime.now(timezone.utc).isoformat()}] xreadgroup returned {num_msgs} message(s). Processing..."
@@ -1492,8 +1664,13 @@ try:
1492
1664
  # for k, v in msg_data_bytes_dict.items() } # No longer needed
1493
1665
  # print(f"Processing message {message_id_str}") # Reduce verbosity
1494
1666
  # print(f"Message data: {message_data_str_dict}") # Reduce verbosity
1667
+ print(f"[DEBUG] === PROCESSING MESSAGE {message_id_str} ===")
1668
+ print(f"[DEBUG] Message data keys: {list(message_data_str_dict.keys())}")
1669
+
1495
1670
  process_message(message_id_str, message_data_str_dict)
1496
1671
 
1672
+ print(f"[DEBUG] === FINISHED PROCESSING MESSAGE {message_id_str} ===")
1673
+
1497
1674
  except ConnectionError as e:
1498
1675
  logger.error(f"Redis connection error: {e}. Reconnecting in 5s...")
1499
1676
  time.sleep(5)
@@ -5,13 +5,16 @@ import os
5
5
  import socket
6
6
  import sys
7
7
  import time
8
- from typing import Dict, List, Optional, Tuple, cast
8
+ from typing import Any, Dict, List, Optional, Tuple, cast
9
9
 
10
10
  import redis
11
11
  import socks
12
12
  from redis import ConnectionError, ResponseError
13
13
  from redis.exceptions import TimeoutError as RedisTimeoutError
14
14
 
15
+ # Assuming these are imported from other modules
16
+ from nebu.processors.models import V1ProcessorHealthResponse
17
+
15
18
 
16
19
  def setup_health_logging():
17
20
  """Set up logging for the health check worker to write to a dedicated file."""
@@ -48,31 +51,131 @@ def process_health_check_message(
48
51
  health_group: str,
49
52
  ) -> None:
50
53
  """Processes a single health check message."""
54
+ print(f"[HEALTH DEBUG] === PROCESSING HEALTH CHECK MESSAGE ===")
55
+ print(f"[HEALTH DEBUG] Message ID: {message_id}")
56
+ print(f"[HEALTH DEBUG] Message data: {message_data}")
57
+ print(f"[HEALTH DEBUG] Health stream: {health_stream}")
58
+ print(f"[HEALTH DEBUG] Health group: {health_group}")
59
+
51
60
  logger.info(f"Processing health check message {message_id}: {message_data}")
52
61
 
53
- # Parse the message if it contains JSON data
62
+ health_status = "ok"
63
+ health_message: Optional[str] = "Health check processed successfully."
64
+ details: Optional[Dict[str, Any]] = None
65
+ return_stream: Optional[str] = None
66
+ original_message_id: Optional[str] = None
67
+ user_id: Optional[str] = None
68
+
54
69
  try:
55
70
  if "data" in message_data:
56
- data = json.loads(message_data["data"])
71
+ print(f"[HEALTH DEBUG] Found 'data' field in message")
72
+ data_str = message_data["data"]
73
+ print(f"[HEALTH DEBUG] Raw data string: {data_str}")
74
+
75
+ data = json.loads(data_str)
76
+ print(f"[HEALTH DEBUG] Parsed data: {json.dumps(data, indent=2)}")
57
77
  logger.info(f"Health check data: {data}")
78
+
79
+ # Extract important fields from the forwarded message
80
+ return_stream = data.get("return_stream")
81
+ original_message_id = data.get("original_message_id")
82
+ user_id = data.get("user_id")
83
+ inner_kind = data.get("kind")
84
+ inner_id = data.get("id")
85
+ content = data.get("content")
86
+
87
+ print(f"[HEALTH DEBUG] Extracted fields:")
88
+ print(f"[HEALTH DEBUG] return_stream: {return_stream}")
89
+ print(f"[HEALTH DEBUG] original_message_id: {original_message_id}")
90
+ print(f"[HEALTH DEBUG] user_id: {user_id}")
91
+ print(f"[HEALTH DEBUG] inner_kind: {inner_kind}")
92
+ print(f"[HEALTH DEBUG] inner_id: {inner_id}")
93
+ print(f"[HEALTH DEBUG] content: {content}")
94
+
95
+ # Update details with health check info
96
+ details = {
97
+ "checked_message_id": inner_id,
98
+ "original_message_id": original_message_id,
99
+ "health_stream": health_stream,
100
+ }
101
+
102
+ if content and isinstance(content, dict):
103
+ details.update({"check_content": content})
104
+ print(f"[HEALTH DEBUG] Added check_content to details")
105
+ else:
106
+ print(f"[HEALTH DEBUG] No 'data' field found in message_data")
107
+
58
108
  except (json.JSONDecodeError, KeyError) as e:
109
+ print(f"[HEALTH DEBUG] ERROR parsing message data: {e}")
59
110
  logger.warning(f"Could not parse health check message data: {e}")
111
+ health_status = "error"
112
+ health_message = f"Failed to parse health check message data: {e}"
113
+ details = {"error": str(e)}
114
+
115
+ # Construct the health response using V1ProcessorHealthResponse
116
+ health_response = V1ProcessorHealthResponse(
117
+ status=health_status, message=health_message, details=details
118
+ )
60
119
 
61
- # You could add more logic here, e.g., update an internal health status,
62
- # send a response, perform actual health checks, etc.
120
+ print(
121
+ f"[HEALTH DEBUG] Constructed V1ProcessorHealthResponse: {health_response.model_dump_json()}"
122
+ )
123
+ logger.info(
124
+ f"Health response for message {message_id}: {health_response.model_dump_json()}"
125
+ )
126
+
127
+ # Send the response to the return stream in the format expected by the system
128
+ if return_stream:
129
+ print(
130
+ f"[HEALTH DEBUG] Sending V1ProcessorHealthResponse to return_stream: {return_stream}"
131
+ )
132
+ try:
133
+ # Send the V1ProcessorHealthResponse directly (not wrapped in StreamResponseMessage)
134
+ response_data = health_response.model_dump()
135
+
136
+ print(
137
+ f"[HEALTH DEBUG] Sending V1ProcessorHealthResponse directly: {json.dumps(response_data, indent=2)}"
138
+ )
139
+
140
+ # Send to return stream
141
+ redis_conn.xadd(
142
+ return_stream,
143
+ {"data": json.dumps(response_data)},
144
+ maxlen=1000,
145
+ approximate=True,
146
+ )
147
+ print(
148
+ f"[HEALTH DEBUG] V1ProcessorHealthResponse sent successfully to {return_stream}"
149
+ )
150
+ logger.info(
151
+ f"Sent health response for {message_id} to stream: {return_stream}"
152
+ )
153
+ except Exception as e_resp_send:
154
+ print(f"[HEALTH DEBUG] ERROR sending response: {e_resp_send}")
155
+ logger.error(
156
+ f"Failed to send health response for {message_id} to stream {return_stream}: {e_resp_send}"
157
+ )
158
+ else:
159
+ print(f"[HEALTH DEBUG] No return_stream specified, not sending response")
63
160
 
64
161
  # Acknowledge the health check message
162
+ print(f"[HEALTH DEBUG] Acknowledging message {message_id}")
65
163
  try:
66
164
  redis_conn.xack(health_stream, health_group, message_id)
165
+ print(f"[HEALTH DEBUG] Message {message_id} acknowledged successfully")
67
166
  logger.info(f"Acknowledged health check message {message_id}")
68
167
  except Exception as e_ack:
168
+ print(f"[HEALTH DEBUG] ERROR acknowledging message: {e_ack}")
69
169
  logger.error(
70
170
  f"Failed to acknowledge health check message {message_id}: {e_ack}"
71
171
  )
72
172
 
173
+ print(f"[HEALTH DEBUG] === FINISHED PROCESSING HEALTH CHECK MESSAGE ===")
174
+
73
175
 
74
176
  def main():
75
177
  """Main function for the health check consumer subprocess."""
178
+ print(f"[HEALTH DEBUG] === HEALTH WORKER STARTING ===")
76
179
  logger = setup_health_logging()
77
180
 
78
181
  # Get environment variables
@@ -80,7 +183,13 @@ def main():
80
183
  health_stream = os.environ.get("REDIS_HEALTH_STREAM")
81
184
  health_group = os.environ.get("REDIS_HEALTH_CONSUMER_GROUP")
82
185
 
186
+ print(f"[HEALTH DEBUG] Environment variables:")
187
+ print(f"[HEALTH DEBUG] REDIS_URL: {redis_url}")
188
+ print(f"[HEALTH DEBUG] REDIS_HEALTH_STREAM: {health_stream}")
189
+ print(f"[HEALTH DEBUG] REDIS_HEALTH_CONSUMER_GROUP: {health_group}")
190
+
83
191
  if not all([redis_url, health_stream, health_group]):
192
+ print(f"[HEALTH DEBUG] ERROR: Missing required environment variables")
84
193
  logger.error(
85
194
  "Missing required environment variables: REDIS_URL, REDIS_HEALTH_STREAM, REDIS_HEALTH_CONSUMER_GROUP"
86
195
  )
@@ -91,43 +200,64 @@ def main():
91
200
  assert isinstance(health_stream, str)
92
201
  assert isinstance(health_group, str)
93
202
 
203
+ print(
204
+ f"[HEALTH DEBUG] Starting health consumer for stream: {health_stream}, group: {health_group}"
205
+ )
94
206
  logger.info(
95
207
  f"Starting health consumer for stream: {health_stream}, group: {health_group}"
96
208
  )
97
209
 
98
210
  # Configure SOCKS proxy
211
+ print(f"[HEALTH DEBUG] Configuring SOCKS proxy...")
99
212
  socks.set_default_proxy(socks.SOCKS5, "localhost", 1055)
100
213
  socket.socket = socks.socksocket
101
214
  logger.info("Configured SOCKS5 proxy for socket connections via localhost:1055")
102
215
 
103
216
  health_redis_conn: Optional[redis.Redis] = None
104
217
  health_consumer_name = f"health-consumer-{os.getpid()}-{socket.gethostname()}"
218
+ print(f"[HEALTH DEBUG] Health consumer name: {health_consumer_name}")
105
219
 
220
+ print(f"[HEALTH DEBUG] === ENTERING MAIN LOOP ===")
106
221
  while True:
107
222
  try:
108
223
  if health_redis_conn is None:
224
+ print(f"[HEALTH DEBUG] Connecting to Redis for health stream...")
109
225
  logger.info("Connecting to Redis for health stream...")
110
226
  health_redis_conn = redis.from_url(redis_url, decode_responses=True)
111
227
  health_redis_conn.ping()
228
+ print(f"[HEALTH DEBUG] Connected to Redis successfully")
112
229
  logger.info("Connected to Redis for health stream.")
113
230
 
114
231
  # Create health consumer group if it doesn't exist
232
+ print(f"[HEALTH DEBUG] Creating/checking consumer group...")
115
233
  try:
116
234
  health_redis_conn.xgroup_create(
117
235
  health_stream, health_group, id="0", mkstream=True
118
236
  )
237
+ print(
238
+ f"[HEALTH DEBUG] Created consumer group {health_group} for stream {health_stream}"
239
+ )
119
240
  logger.info(
120
241
  f"Created consumer group {health_group} for stream {health_stream}"
121
242
  )
122
243
  except ResponseError as e_group:
123
244
  if "BUSYGROUP" in str(e_group):
245
+ print(
246
+ f"[HEALTH DEBUG] Consumer group {health_group} already exists"
247
+ )
124
248
  logger.info(f"Consumer group {health_group} already exists.")
125
249
  else:
250
+ print(
251
+ f"[HEALTH DEBUG] ERROR creating health consumer group: {e_group}"
252
+ )
126
253
  logger.error(f"Error creating health consumer group: {e_group}")
127
254
  time.sleep(5)
128
255
  health_redis_conn = None
129
256
  continue
130
257
  except Exception as e_group_other:
258
+ print(
259
+ f"[HEALTH DEBUG] UNEXPECTED ERROR creating health consumer group: {e_group_other}"
260
+ )
131
261
  logger.error(
132
262
  f"Unexpected error creating health consumer group: {e_group_other}"
133
263
  )
@@ -138,6 +268,7 @@ def main():
138
268
  # Read from health stream
139
269
  assert health_redis_conn is not None
140
270
 
271
+ print(f"[HEALTH DEBUG] Reading from health stream {health_stream}...")
141
272
  health_streams_arg: Dict[str, object] = {health_stream: ">"}
142
273
  raw_messages = health_redis_conn.xreadgroup(
143
274
  health_group,
@@ -147,13 +278,21 @@ def main():
147
278
  block=5000, # Block for 5 seconds
148
279
  )
149
280
 
281
+ print(f"[HEALTH DEBUG] xreadgroup returned: {raw_messages}")
282
+ print(f"[HEALTH DEBUG] Messages type: {type(raw_messages)}")
283
+
150
284
  if raw_messages:
285
+ print(f"[HEALTH DEBUG] Found messages to process")
151
286
  # Cast to expected type for decode_responses=True
152
287
  messages = cast(
153
288
  List[Tuple[str, List[Tuple[str, Dict[str, str]]]]], raw_messages
154
289
  )
155
- for _stream_name, stream_messages in messages:
290
+ for stream_name, stream_messages in messages:
291
+ print(
292
+ f"[HEALTH DEBUG] Processing stream: {stream_name} with {len(stream_messages)} message(s)"
293
+ )
156
294
  for message_id, message_data in stream_messages:
295
+ print(f"[HEALTH DEBUG] Processing message {message_id}")
157
296
  process_health_check_message(
158
297
  message_id,
159
298
  message_data,
@@ -162,6 +301,8 @@ def main():
162
301
  health_stream,
163
302
  health_group,
164
303
  )
304
+ else:
305
+ print(f"[HEALTH DEBUG] No messages received (timeout)")
165
306
 
166
307
  except (ConnectionError, RedisTimeoutError, TimeoutError) as e_conn:
167
308
  logger.error(f"Redis connection error: {e_conn}. Reconnecting in 5s...")
@@ -149,3 +149,9 @@ class V1OpenAIStreamResponse(BaseModel):
149
149
  content: Any # Using Any for ChatCompletionResponse
150
150
  created_at: int
151
151
  user_id: Optional[str] = None
152
+
153
+
154
+ class V1ProcessorHealthResponse(BaseModel):
155
+ status: str
156
+ message: Optional[str] = None
157
+ details: Optional[Any] = None
@@ -1,7 +1,8 @@
1
1
  import json
2
2
  import threading
3
3
  import time
4
- import uuid
4
+
5
+ # import uuid # Removed unused import
5
6
  from typing import (
6
7
  Any,
7
8
  Dict,
@@ -23,6 +24,7 @@ from nebu.meta import V1ResourceMetaRequest, V1ResourceReference
23
24
  from nebu.processors.models import (
24
25
  V1ContainerRequest,
25
26
  V1Processor,
27
+ V1ProcessorHealthResponse,
26
28
  V1ProcessorRequest,
27
29
  V1Processors,
28
30
  V1ProcessorScaleRequest,
@@ -246,7 +248,7 @@ class Processor(Generic[InputType, OutputType]):
246
248
 
247
249
  # --- Wait for health check if requested ---
248
250
  if wait_for_healthy:
249
- self.wait_for_health_check()
251
+ self.wait_for_healthy()
250
252
 
251
253
  def __call__(
252
254
  self,
@@ -559,11 +561,11 @@ class Processor(Generic[InputType, OutputType]):
559
561
  else:
560
562
  logger.info(f"No active log stream to stop for {self.name}.")
561
563
 
562
- def wait_for_health_check(
564
+ def wait_for_healthy(
563
565
  self, timeout: float = 3600.0, retry_interval: float = 5.0
564
566
  ) -> None:
565
567
  """
566
- Wait for the processor to respond to health checks.
568
+ Wait for the processor to respond to health checks using the health endpoint.
567
569
 
568
570
  Args:
569
571
  timeout: Maximum time to wait for health check in seconds
@@ -573,40 +575,25 @@ class Processor(Generic[InputType, OutputType]):
573
575
  raise ValueError("Processor not found, cannot perform health check")
574
576
 
575
577
  logger.info(
576
- f"Waiting for processor {self.processor.metadata.name} to be healthy..."
578
+ f"Waiting for processor {self.processor.metadata.name} to be healthy via health endpoint..."
577
579
  )
578
580
 
579
581
  start_time = time.time()
580
582
  while time.time() - start_time < timeout:
581
583
  try:
582
- # Create a health check message
583
- health_check_data = {
584
- "kind": "HealthCheck",
585
- "id": str(uuid.uuid4()),
586
- "content": {},
587
- "created_at": time.time(),
588
- }
589
-
590
- # Send health check and wait for response
591
- response = self.send(
592
- data=health_check_data, # type: ignore[arg-type]
593
- wait=True,
594
- timeout=30.0, # Short timeout for individual health check
584
+ health_response = self.health() # Use the new health() method
585
+ logger.info(
586
+ f">>> Health check response: {health_response.model_dump_json()}"
595
587
  )
596
- logger.info(f">>> Health check response: {response}")
597
588
 
598
589
  # Check if the response indicates health
599
- if response and isinstance(response, dict):
600
- status = response.get("status")
601
- if status == "healthy":
602
- logger.info(
603
- f"Processor {self.processor.metadata.name} is healthy!"
604
- )
605
- return
606
-
607
- logger.info(
608
- f"Health check attempt failed, retrying in {retry_interval}s..."
609
- )
590
+ if health_response.status == "ok": # Check for "ok" status
591
+ logger.info(f"Processor {self.processor.metadata.name} is healthy!")
592
+ return
593
+ else:
594
+ logger.info(
595
+ f"Processor {self.processor.metadata.name} reported status: {health_response.status}. Retrying in {retry_interval}s..."
596
+ )
610
597
 
611
598
  except Exception as e:
612
599
  logger.info(
@@ -619,3 +606,39 @@ class Processor(Generic[InputType, OutputType]):
619
606
  raise TimeoutError(
620
607
  f"Processor {self.processor.metadata.name} failed to become healthy within {timeout} seconds"
621
608
  )
609
+
610
+ def health(self) -> V1ProcessorHealthResponse:
611
+ """
612
+ Performs a health check on the processor by calling the health endpoint.
613
+ """
614
+ if (
615
+ not self.processor
616
+ or not self.processor.metadata.name
617
+ or not self.processor.metadata.namespace
618
+ ):
619
+ raise ValueError(
620
+ "Processor not found or missing metadata (name/namespace), cannot perform health check."
621
+ )
622
+
623
+ health_url = f"{self.orign_host}/v1/processors/{self.processor.metadata.namespace}/{self.processor.metadata.name}/health"
624
+ logger.debug(f"Calling health check endpoint: {health_url}")
625
+
626
+ try:
627
+ response = requests.get(
628
+ health_url,
629
+ headers={"Authorization": f"Bearer {self.api_key}"},
630
+ timeout=30.0, # Standard timeout for a health check
631
+ )
632
+ response.raise_for_status() # Raise an exception for HTTP errors
633
+ health_response_data = response.json()
634
+ return V1ProcessorHealthResponse.model_validate(health_response_data)
635
+ except requests.exceptions.RequestException as e:
636
+ logger.error(f"Health check request to {health_url} failed: {e}")
637
+ # Optionally, return a V1ProcessorHealthResponse indicating an error
638
+ # For now, re-raising the exception or a custom one might be better
639
+ raise RuntimeError(f"Failed to get health status: {e}") from e
640
+ except Exception as e:
641
+ logger.error(f"An unexpected error occurred during health check: {e}")
642
+ raise RuntimeError(
643
+ f"Unexpected error during health status retrieval: {e}"
644
+ ) from e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nebu
3
- Version: 0.1.117
3
+ Version: 0.1.119
4
4
  Summary: A globally distributed container runtime
5
5
  Requires-Python: >=3.10.14
6
6
  Description-Content-Type: text/markdown
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