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 +5 -0
- nebu/processors/consumer.py +166 -35
- nebu/processors/consumer_process_worker.py +19 -1
- {nebu-0.1.76.dist-info → nebu-0.1.78.dist-info}/METADATA +1 -1
- {nebu-0.1.76.dist-info → nebu-0.1.78.dist-info}/RECORD +8 -7
- {nebu-0.1.76.dist-info → nebu-0.1.78.dist-info}/WHEEL +0 -0
- {nebu-0.1.76.dist-info → nebu-0.1.78.dist-info}/licenses/LICENSE +0 -0
- {nebu-0.1.76.dist-info → nebu-0.1.78.dist-info}/top_level.txt +0 -0
nebu/errors.py
ADDED
nebu/processors/consumer.py
CHANGED
@@ -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
|
-
#
|
365
|
-
|
366
|
-
# Pass message data via stdin
|
367
|
-
result = subprocess.run(
|
380
|
+
# Start the worker process
|
381
|
+
process = subprocess.Popen(
|
368
382
|
worker_cmd,
|
369
|
-
|
383
|
+
stdin=subprocess.PIPE,
|
384
|
+
stdout=subprocess.PIPE,
|
385
|
+
stderr=subprocess.PIPE,
|
370
386
|
text=True,
|
371
|
-
|
372
|
-
|
373
|
-
env=os.environ.copy(),
|
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
|
-
|
381
|
-
|
382
|
-
|
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
|
-
|
390
|
-
|
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
|
-
|
394
|
-
|
395
|
-
message_data.get(
|
396
|
-
|
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(
|
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
|
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)
|
@@ -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=
|
17
|
-
nebu/processors/consumer_process_worker.py,sha256=
|
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.
|
26
|
-
nebu-0.1.
|
27
|
-
nebu-0.1.
|
28
|
-
nebu-0.1.
|
29
|
-
nebu-0.1.
|
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
|
File without changes
|
File without changes
|