nebu 0.1.76__py3-none-any.whl → 0.1.78__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/errors.py ADDED
@@ -0,0 +1,5 @@
1
+ # Add this near the top with other imports/definitions
2
+ class RetriableError(Exception):
3
+ """Custom exception for failures during Training resource setup."""
4
+
5
+ pass
@@ -5,16 +5,19 @@ import os
5
5
  import socket
6
6
  import subprocess
7
7
  import sys
8
+ import threading
8
9
  import time
9
10
  import traceback
10
11
  import types
11
12
  from datetime import datetime, timezone
12
- from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, cast
13
+ from typing import IO, Any, Callable, Dict, List, Optional, Tuple, TypeVar, cast
13
14
 
14
15
  import redis
15
16
  import socks
16
17
  from redis import ConnectionError, ResponseError
17
18
 
19
+ from nebu.errors import RetriableError
20
+
18
21
  # Define TypeVar for generic models
19
22
  T = TypeVar("T")
20
23
 
@@ -351,9 +354,22 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
351
354
  # --- Subprocess Execution Path ---
352
355
  if execution_mode == "subprocess":
353
356
  print(f"Processing message {message_id} in subprocess...")
357
+ process = None # Initialize process variable
358
+
359
+ # Helper function to read and print stream lines
360
+ def stream_reader(stream: IO[str], prefix: str):
361
+ try:
362
+ for line in iter(stream.readline, ""):
363
+ print(f"{prefix}: {line.strip()}", flush=True)
364
+ except Exception as e:
365
+ print(f"Error reading stream {prefix}: {e}")
366
+ finally:
367
+ stream.close()
368
+
354
369
  try:
355
370
  worker_cmd = [
356
371
  sys.executable,
372
+ "-u", # Force unbuffered stdout/stderr in the subprocess
357
373
  "-m",
358
374
  "nebu.processors.consumer_process_worker",
359
375
  ]
@@ -361,52 +377,116 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
361
377
  {"message_id": message_id, "message_data": message_data}
362
378
  )
363
379
 
364
- # Run the worker script as a module
365
- # Inherit environment variables automatically
366
- # Pass message data via stdin
367
- result = subprocess.run(
380
+ # Start the worker process
381
+ process = subprocess.Popen(
368
382
  worker_cmd,
369
- input=process_input,
383
+ stdin=subprocess.PIPE,
384
+ stdout=subprocess.PIPE,
385
+ stderr=subprocess.PIPE,
370
386
  text=True,
371
- capture_output=True,
372
- check=True, # Raise CalledProcessError on non-zero exit
373
- env=os.environ.copy(), # Ensure environment is passed
387
+ encoding="utf-8",
388
+ bufsize=1, # Line buffered
389
+ env=os.environ.copy(),
374
390
  )
375
- print(f"Subprocess for {message_id} completed successfully.")
376
- if result.stdout:
377
- print(f"Subprocess stdout:\n{result.stdout}")
378
- # Assume the subprocess handled response sending and acknowledgement
379
391
 
380
- except subprocess.CalledProcessError as e:
381
- print(
382
- f"Subprocess for message {message_id} failed with exit code {e.returncode}."
392
+ # Create threads to read stdout and stderr concurrently
393
+ stdout_thread = threading.Thread(
394
+ target=stream_reader,
395
+ args=(process.stdout, f"[Subprocess STDOUT {message_id[:8]}]"),
396
+ )
397
+ stderr_thread = threading.Thread(
398
+ target=stream_reader,
399
+ args=(process.stderr, f"[Subprocess STDERR {message_id[:8]}]"),
383
400
  )
384
- if e.stdout:
385
- print(f"Subprocess stdout:\n{e.stdout}")
386
- if e.stderr:
387
- print(f"Subprocess stderr:\n{e.stderr}")
388
401
 
389
- # Send a generic error message back, as the subprocess likely failed
390
- # before it could send its specific error.
402
+ stdout_thread.start()
403
+ stderr_thread.start()
404
+
405
+ # Send input data to the subprocess
406
+ # Ensure process and stdin are valid before writing/closing
407
+ if process and process.stdin:
408
+ try:
409
+ process.stdin.write(process_input)
410
+ process.stdin.close() # Signal end of input
411
+ except (BrokenPipeError, OSError) as e:
412
+ # Handle cases where the process might have exited early
413
+ print(
414
+ f"Warning: Failed to write full input to subprocess {message_id}: {e}. It might have exited prematurely."
415
+ )
416
+ # Continue to wait and check return code
417
+ else:
418
+ print(
419
+ f"Error: Subprocess stdin stream not available for {message_id}. Cannot send input."
420
+ )
421
+ # Handle this case - perhaps terminate and report error?
422
+ # For now, we'll let it proceed to wait() which will likely show an error code.
423
+
424
+ # Wait for the process to finish
425
+ return_code = (
426
+ process.wait() if process else -1
427
+ ) # Handle case where process is None
428
+
429
+ # Wait for reader threads to finish consuming remaining output
430
+ stdout_thread.join()
431
+ stderr_thread.join()
432
+
433
+ if return_code == 0:
434
+ print(
435
+ f"Subprocess for {message_id} completed successfully (return code 0)."
436
+ )
437
+ # Assume success handling (ack/response) was done by the worker
438
+ elif return_code == 3:
439
+ print(
440
+ f"Subprocess for {message_id} reported a retriable error (exit code 3). Message will not be acknowledged."
441
+ )
442
+ # Optionally send an error response here, though the worker already did.
443
+ # _send_error_response(...)
444
+ # DO NOT Acknowledge the message here, let it be retried.
445
+ else:
446
+ print(
447
+ f"Subprocess for {message_id} failed with exit code {return_code}."
448
+ )
449
+ # Worker likely failed, send generic error and ACK here
450
+ _send_error_response(
451
+ message_id,
452
+ f"Subprocess execution failed with exit code {return_code}",
453
+ "See consumer logs for subprocess stderr.", # stderr was already printed
454
+ message_data.get("return_stream"),
455
+ message_data.get("user_id"),
456
+ )
457
+ # CRITICAL: Acknowledge the message here since the subprocess failed
458
+ try:
459
+ assert isinstance(REDIS_STREAM, str)
460
+ assert isinstance(REDIS_CONSUMER_GROUP, str)
461
+ r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
462
+ print(f"Acknowledged failed subprocess message {message_id}")
463
+ except Exception as e_ack:
464
+ print(
465
+ f"CRITICAL: Failed to acknowledge failed subprocess message {message_id}: {e_ack}"
466
+ )
467
+
468
+ except FileNotFoundError:
469
+ print(
470
+ "FATAL: Worker script 'nebu.processors.consumer_process_worker' not found. Check PYTHONPATH."
471
+ )
472
+ # Send error and ack if possible
391
473
  _send_error_response(
392
474
  message_id,
393
- f"Subprocess execution failed with exit code {e.returncode}",
394
- e.stderr or "No stderr captured",
395
- message_data.get(
396
- "return_stream"
397
- ), # Try to get return stream from original data
398
- message_data.get("user_id"), # Try to get user_id from original data
475
+ "Worker script not found",
476
+ traceback.format_exc(),
477
+ message_data.get("return_stream"),
478
+ message_data.get("user_id"),
399
479
  )
400
-
401
- # CRITICAL: Acknowledge the message here since the subprocess failed
402
480
  try:
403
481
  assert isinstance(REDIS_STREAM, str)
404
482
  assert isinstance(REDIS_CONSUMER_GROUP, str)
405
483
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
406
- print(f"Acknowledged failed subprocess message {message_id}")
484
+ print(
485
+ f"Acknowledged message {message_id} after worker script not found failure"
486
+ )
407
487
  except Exception as e_ack:
408
488
  print(
409
- f"CRITICAL: Failed to acknowledge failed subprocess message {message_id}: {e_ack}"
489
+ f"CRITICAL: Failed to acknowledge message {message_id} after worker script not found failure: {e_ack}"
410
490
  )
411
491
 
412
492
  except Exception as e:
@@ -417,7 +497,7 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
417
497
  # Also send an error and acknowledge
418
498
  _send_error_response(
419
499
  message_id,
420
- f"Failed to launch subprocess: {e}",
500
+ f"Failed to launch/manage subprocess: {e}",
421
501
  traceback.format_exc(),
422
502
  message_data.get("return_stream"),
423
503
  message_data.get("user_id"),
@@ -427,12 +507,44 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
427
507
  assert isinstance(REDIS_CONSUMER_GROUP, str)
428
508
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
429
509
  print(
430
- f"Acknowledged message {message_id} after subprocess launch failure"
510
+ f"Acknowledged message {message_id} after subprocess launch/manage failure"
431
511
  )
432
512
  except Exception as e_ack:
433
513
  print(
434
- f"CRITICAL: Failed to acknowledge message {message_id} after subprocess launch failure: {e_ack}"
514
+ f"CRITICAL: Failed to acknowledge message {message_id} after subprocess launch/manage failure: {e_ack}"
515
+ )
516
+ # Ensure process is terminated if it's still running after an error
517
+ if process and process.poll() is None:
518
+ print(
519
+ f"Terminating potentially lingering subprocess for {message_id}..."
435
520
  )
521
+ process.terminate()
522
+ process.wait(timeout=5) # Give it a moment to terminate
523
+ if process.poll() is None:
524
+ print(
525
+ f"Subprocess for {message_id} did not terminate gracefully, killing."
526
+ )
527
+ process.kill()
528
+ finally:
529
+ # Ensure streams are closed even if threads failed or process is None
530
+ if process:
531
+ if process.stdout:
532
+ try:
533
+ process.stdout.close()
534
+ except Exception:
535
+ pass # Ignore errors during cleanup close
536
+ if process.stderr:
537
+ try:
538
+ process.stderr.close()
539
+ except Exception:
540
+ pass # Ignore errors during cleanup close
541
+ # Stdin should already be closed, but doesn't hurt to be safe
542
+ if process.stdin and not process.stdin.closed:
543
+ try:
544
+ process.stdin.close()
545
+ except Exception:
546
+ pass
547
+
436
548
  return # Exit process_message after handling subprocess logic
437
549
 
438
550
  # --- Inline Execution Path (Original Logic) ---
@@ -447,6 +559,16 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
447
559
  None,
448
560
  None,
449
561
  ) # Pass None for user_id if unavailable here
562
+ # Acknowledge message with code load failure to prevent reprocessing loop
563
+ try:
564
+ assert isinstance(REDIS_STREAM, str)
565
+ assert isinstance(REDIS_CONSUMER_GROUP, str)
566
+ r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
567
+ print(f"Acknowledged message {message_id} due to code load failure.")
568
+ except Exception as e_ack:
569
+ print(
570
+ f"CRITICAL: Failed to acknowledge message {message_id} after code load failure: {e_ack}"
571
+ )
450
572
  return # Skip processing
451
573
 
452
574
  return_stream = None
@@ -694,6 +816,15 @@ def process_message(message_id: str, message_data: Dict[str, str]) -> None:
694
816
  assert isinstance(REDIS_CONSUMER_GROUP, str)
695
817
  r.xack(REDIS_STREAM, REDIS_CONSUMER_GROUP, message_id)
696
818
 
819
+ except RetriableError as e:
820
+ print(f"Retriable error processing message {message_id}: {e}")
821
+ traceback.print_exc()
822
+ _send_error_response(
823
+ message_id, str(e), traceback.format_exc(), return_stream, user_id
824
+ )
825
+ # DO NOT Acknowledge the message for retriable errors
826
+ print(f"Message {message_id} will be retried later.")
827
+
697
828
  except Exception as e:
698
829
  print(f"Error processing message {message_id}: {e}")
699
830
  traceback.print_exc()
@@ -14,6 +14,8 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, cast
14
14
  import redis
15
15
  import socks
16
16
 
17
+ from nebu.errors import RetriableError
18
+
17
19
  # from redis import ConnectionError, ResponseError # Removed unused imports
18
20
 
19
21
  # Define TypeVar for generic models
@@ -640,8 +642,24 @@ if __name__ == "__main__":
640
642
  print("[Worker] Exiting with status 0.")
641
643
  sys.exit(0)
642
644
 
645
+ except RetriableError as e:
646
+ # --- Handle Retriable Processing Error ---
647
+ print(f"[Worker] Retriable error processing message {message_id}: {e}")
648
+ tb = traceback.format_exc()
649
+ print(tb)
650
+ # Assert message_id is str before sending error
651
+ assert isinstance(message_id, str)
652
+ # Send error response (optional, consider suppressing later if too noisy)
653
+ _send_error_response(message_id, str(e), tb, return_stream, user_id)
654
+
655
+ # DO NOT Acknowledge the message for retriable errors
656
+
657
+ # --- 9. Exit with specific code for retriable failure ---
658
+ print("[Worker] Exiting with status 3 due to retriable error.")
659
+ sys.exit(3)
660
+
643
661
  except Exception as e:
644
- # --- Handle Processing Error ---
662
+ # --- Handle Non-Retriable Processing Error ---
645
663
  print(f"[Worker] Error processing message {message_id}: {e}")
646
664
  tb = traceback.format_exc()
647
665
  print(tb)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nebu
3
- Version: 0.1.76
3
+ Version: 0.1.78
4
4
  Summary: A globally distributed container runtime
5
5
  Requires-Python: >=3.10.14
6
6
  Description-Content-Type: text/markdown
@@ -3,6 +3,7 @@ nebu/auth.py,sha256=N_v6SPFD9HU_UoRDTaouH03g2Hmo9C-xxqInE1FweXE,1471
3
3
  nebu/cache.py,sha256=jmluqvWnE9N8uNq6nppXSxEJK7DKWaB79GicaGg9KmY,4718
4
4
  nebu/config.py,sha256=C5Jt9Bd0i0HrgzBSVNJ-Ml3KwX_gaYbYYZEtNL2gvJg,7031
5
5
  nebu/data.py,sha256=X0aAJYuHNVcTCRHpIDDm546HwMqIZpv40lGrozlL41A,39797
6
+ nebu/errors.py,sha256=bBnK5YQ6qZg4OMY81AN2k03ppefg89FUwF_SHEMlqCA,170
6
7
  nebu/meta.py,sha256=CzFHMND9seuewzq9zNNx9WTr6JvrCBExe7BLqDSr7lM,745
7
8
  nebu/orign.py,sha256=SkVfHgpadwik58KCZCrjdV5EHY0dhpEhDvijzLxY11Y,2052
8
9
  nebu/builders/builder.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -13,8 +14,8 @@ nebu/containers/models.py,sha256=0j6NGy4yto-enRDh_4JH_ZTbHrLdSpuMOqNQPnIrwC4,681
13
14
  nebu/containers/server.py,sha256=yFa2Y9PzBn59E1HftKiv0iapPonli2rbGAiU6r-wwe0,2513
14
15
  nebu/namespaces/models.py,sha256=EqUOpzhVBhvJw2P92ONDUbIgC31M9jMmcaG5vyOrsWg,497
15
16
  nebu/namespaces/namespace.py,sha256=Q_EDH7BgQrTkaDh_l4tbo22qpq-uARfIk8ZPBLjITGY,4967
16
- nebu/processors/consumer.py,sha256=Fxgl0cuqKpX3UMsig_aw5KBbg1blE4xUY4yNnIvIHuw,37806
17
- nebu/processors/consumer_process_worker.py,sha256=l5_BSMfqy-n2yK_UC3sm_pimzelaASeMdPxRE97HFwc,30959
17
+ nebu/processors/consumer.py,sha256=nWJmlTwJxfadMEQQwzwOJNPCAkcxqIzOpshMMbnfa-o,43617
18
+ nebu/processors/consumer_process_worker.py,sha256=tF5KU3Rnmzfc3Y0cM8J5nwGg1cJMe-ry0FmMSgGvXrY,31765
18
19
  nebu/processors/decorate.py,sha256=mu1o05BjNcbJ4M1so4Xvt7UbslX--B4dsYLgs5h8bEg,54610
19
20
  nebu/processors/default.py,sha256=W4slJenG59rvyTlJ7gRp58eFfXcNOTT2Hfi6zzJAobI,365
20
21
  nebu/processors/models.py,sha256=y40HoW-MEzDWB2dm_tsYlUy3Nf3s6eiLC0iGO9BoNog,3956
@@ -22,8 +23,8 @@ nebu/processors/processor.py,sha256=OgEK8Fz0ehSe_VFiNsxweVKZIckhgVvQQ11NNffYZqA,
22
23
  nebu/processors/remote.py,sha256=TeAIPGEMqnDIb7H1iett26IEZrBlcbPB_-DSm6jcH1E,1285
23
24
  nebu/redis/models.py,sha256=coPovAcVXnOU1Xh_fpJL4PO3QctgK9nBe5QYoqEcnxg,1230
24
25
  nebu/services/service.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- nebu-0.1.76.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
26
- nebu-0.1.76.dist-info/METADATA,sha256=j130KtOVQeqJRkv6xgC6CJ0BUSmqUmb8jFyhKAC11jE,1731
27
- nebu-0.1.76.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
28
- nebu-0.1.76.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
29
- nebu-0.1.76.dist-info/RECORD,,
26
+ nebu-0.1.78.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
27
+ nebu-0.1.78.dist-info/METADATA,sha256=5qScVLcCucIHRKQgNam4OlHIKzDvh6qAKP_ompV6UY4,1731
28
+ nebu-0.1.78.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
29
+ nebu-0.1.78.dist-info/top_level.txt,sha256=uLIbEKJeGSHWOAJN5S0i5XBGwybALlF9bYoB1UhdEgQ,5
30
+ nebu-0.1.78.dist-info/RECORD,,
File without changes