AbstractIntegratedModule 0.9.5__tar.gz → 0.9.6__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.
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractIntegratedModule.egg-info/PKG-INFO +2 -2
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractIntegratedModule.py +321 -149
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractOptimizedModules.c +200 -200
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/PKG-INFO +2 -2
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/README.md +1 -1
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/setup.py +1 -1
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractIntegratedModule.egg-info/SOURCES.txt +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractIntegratedModule.egg-info/dependency_links.txt +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractIntegratedModule.egg-info/requires.txt +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractIntegratedModule.egg-info/top_level.txt +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractOptimizedModules.pyx +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/MANIFEST.in +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/Cargo.toml +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/pyproject.toml +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/src/lib.rs +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/target/debug/build/libsqlite3-sys-ed07b882cd2aa5e2/out/bindgen.rs +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/target/debug/build/serde_core-ebc15f2e9cad7f5f/out/private.rs +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/target/debug/build/target-lexicon-08527f45de28143d/out/host.rs +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/target/release/build/libsqlite3-sys-bf0400df4523274c/out/bindgen.rs +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/target/release/build/serde_core-5cdb76131825e4af/out/private.rs +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/abstract_model_storage/target/release/build/target-lexicon-43eb95a0588bf457/out/host.rs +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/pyproject.toml +0 -0
- {abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AbstractIntegratedModule
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: Library for Advanced Integrated Non-LLM AI Models - Optimized Backend Framework For Non-LLM AI Agent
|
|
5
5
|
Author: Micro-Novelty
|
|
6
6
|
Author-email: hernikpuspita5@gmail.com
|
|
@@ -42,7 +42,7 @@ https://github.com/Micro-Novelty/IntegratedPipeline-Specialized-Non-LLM-AI-Agent
|
|
|
42
42
|
#### Note: The README here you are reading is a direct copy from my README Repository, to download the necessary files, you can visit my Repository with the provided link above.
|
|
43
43
|
|
|
44
44
|
### Library Short Description:
|
|
45
|
-
- Development Stage: 0.9.
|
|
45
|
+
- Development Stage: 0.9.6 Official Release.
|
|
46
46
|
- Maintainer: Micro-Novelty.
|
|
47
47
|
- library Source-Code is Open-sourced on github.
|
|
48
48
|
- Purpose: Specifically Designed for providing Non-LLM AI Agent Framework for edge Devices, Optimized for ARM64 architecture.
|
{abstractintegratedmodule-0.9.5 → abstractintegratedmodule-0.9.6}/AbstractIntegratedModule.py
RENAMED
|
@@ -2034,7 +2034,7 @@ class MLP:
|
|
|
2034
2034
|
gradient = np.gradient(subnet.flatten())
|
|
2035
2035
|
|
|
2036
2036
|
val = [np.linalg.norm(v) for v in gradient]
|
|
2037
|
-
anisotropy = np.std(val) / np.mean(val)
|
|
2037
|
+
anisotropy = np.std(val) + eps / np.mean(val)
|
|
2038
2038
|
return anisotropy
|
|
2039
2039
|
|
|
2040
2040
|
def adapt_predict_shape(self, y_pred, y_true):
|
|
@@ -6171,54 +6171,65 @@ class AsyncMessageQueue:
|
|
|
6171
6171
|
|
|
6172
6172
|
|
|
6173
6173
|
class ThreadedMessageQueue:
|
|
6174
|
-
# Thread-based message queue for synchronous code
|
|
6174
|
+
# Thread-based message queue for synchronous code.
|
|
6175
6175
|
def __init__(self, max_size=1000, worker_threads=4):
|
|
6176
|
-
self.queue
|
|
6177
|
-
self.results
|
|
6178
|
-
self.handlers
|
|
6179
|
-
self._running
|
|
6180
|
-
self._workers
|
|
6176
|
+
self.queue = queue.Queue(maxsize=max_size)
|
|
6177
|
+
self.results = {}
|
|
6178
|
+
self.handlers = {}
|
|
6179
|
+
self._running = False
|
|
6180
|
+
self._workers = []
|
|
6181
6181
|
self._worker_threads = worker_threads
|
|
6182
6182
|
self._stats = {
|
|
6183
6183
|
'messages_processed': 0,
|
|
6184
|
-
'messages_failed': 0,
|
|
6185
|
-
'active_workers': 0
|
|
6184
|
+
'messages_failed' : 0,
|
|
6185
|
+
'active_workers' : 0
|
|
6186
6186
|
}
|
|
6187
6187
|
self._lock = threading.Lock()
|
|
6188
|
-
|
|
6188
|
+
|
|
6189
|
+
|
|
6189
6190
|
def register_handler(self, message_type: str, handler: Callable):
|
|
6190
|
-
# Register a handler for specific message type
|
|
6191
6191
|
self.handlers[message_type] = handler
|
|
6192
6192
|
logger.info(f"[=] Registered handler for {message_type}")
|
|
6193
|
-
|
|
6193
|
+
|
|
6194
|
+
|
|
6194
6195
|
def publish(self, message: Message, timeout: float = 30.0) -> Any:
|
|
6195
|
-
#
|
|
6196
|
-
|
|
6197
|
-
|
|
6196
|
+
# threading.Event.
|
|
6197
|
+
done_event = threading.Event()
|
|
6198
|
+
result_container = {'result': None, 'error': None}
|
|
6199
|
+
|
|
6198
6200
|
def callback_wrapper(res):
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6201
|
+
# distinguish success from failure
|
|
6202
|
+
if isinstance(res, Exception):
|
|
6203
|
+
result_container['error'] = res
|
|
6204
|
+
else:
|
|
6205
|
+
result_container['result'] = res
|
|
6206
|
+
done_event.set()
|
|
6207
|
+
|
|
6202
6208
|
message.callback = callback_wrapper
|
|
6203
|
-
self.queue.put(message)
|
|
6204
|
-
|
|
6205
|
-
start_time = time.time()
|
|
6206
|
-
while not result_container['ready'] and (time.time() - start_time) < timeout:
|
|
6207
|
-
time.sleep(0.01)
|
|
6208
6209
|
|
|
6209
|
-
|
|
6210
|
+
try:
|
|
6211
|
+
self.queue.put(message, timeout=timeout)
|
|
6212
|
+
except queue.Full:
|
|
6213
|
+
raise TimeoutError(f"[!] Queue full, could not enqueue message {message.id}")
|
|
6214
|
+
|
|
6215
|
+
signaled = done_event.wait(timeout=timeout)
|
|
6216
|
+
|
|
6217
|
+
if not signaled:
|
|
6218
|
+
with self._lock:
|
|
6219
|
+
self._stats['messages_failed'] += 1 # actually tracked now
|
|
6210
6220
|
raise TimeoutError(f"[!] Message {message.id} timed out")
|
|
6211
|
-
|
|
6212
|
-
if result_container['error']:
|
|
6213
|
-
|
|
6214
|
-
|
|
6221
|
+
|
|
6222
|
+
if result_container['error'] is not None:
|
|
6223
|
+
with self._lock:
|
|
6224
|
+
self._stats['messages_failed'] += 1
|
|
6225
|
+
raise result_container['error'] # now actually raises
|
|
6226
|
+
|
|
6215
6227
|
with self._lock:
|
|
6216
6228
|
self._stats['messages_processed'] += 1
|
|
6217
|
-
|
|
6229
|
+
|
|
6218
6230
|
return result_container['result']
|
|
6219
|
-
|
|
6231
|
+
|
|
6220
6232
|
def publish_async(self, message: Message, callback: Optional[Callable] = None):
|
|
6221
|
-
# Publish without waiting
|
|
6222
6233
|
message.callback = callback
|
|
6223
6234
|
try:
|
|
6224
6235
|
self.queue.put(message, block=False)
|
|
@@ -6226,15 +6237,16 @@ class ThreadedMessageQueue:
|
|
|
6226
6237
|
except queue.Full:
|
|
6227
6238
|
logger.error(f"[=] Queue full, cannot publish message {message.id}")
|
|
6228
6239
|
return False
|
|
6229
|
-
|
|
6230
|
-
def _worker(self, worker_id: int):
|
|
6231
|
-
# Worker thread processing messages.
|
|
6232
6240
|
|
|
6233
|
-
|
|
6234
|
-
|
|
6241
|
+
|
|
6242
|
+
|
|
6243
|
+
def _worker(self, worker_id: int, stop_event: threading.Event):
|
|
6244
|
+
logger.info(f'[=] Worker started: {worker_id}')
|
|
6245
|
+
|
|
6246
|
+
while self._running and not stop_event.is_set():
|
|
6235
6247
|
try:
|
|
6236
6248
|
message = self.queue.get(timeout=1)
|
|
6237
|
-
|
|
6249
|
+
|
|
6238
6250
|
if message.type in self.handlers:
|
|
6239
6251
|
try:
|
|
6240
6252
|
result = self.handlers[message.type](message)
|
|
@@ -6242,57 +6254,90 @@ class ThreadedMessageQueue:
|
|
|
6242
6254
|
message.callback(result)
|
|
6243
6255
|
except Exception as e:
|
|
6244
6256
|
logger.error(f"[=] Worker {worker_id} handler failed: {e}")
|
|
6257
|
+
with self._lock:
|
|
6258
|
+
self._stats['messages_failed'] += 1
|
|
6245
6259
|
if message.callback:
|
|
6260
|
+
# callback_wrapper
|
|
6246
6261
|
message.callback(e)
|
|
6247
6262
|
else:
|
|
6248
6263
|
logger.warning(f"[=] No handler for message type: {message.type}")
|
|
6249
|
-
|
|
6264
|
+
|
|
6250
6265
|
except queue.Empty:
|
|
6251
6266
|
continue
|
|
6252
6267
|
except Exception as e:
|
|
6253
6268
|
logger.error(f"[=] Worker {worker_id} error: {e}")
|
|
6254
|
-
|
|
6269
|
+
# track that this worker degraded, without killing loop entirely
|
|
6270
|
+
with self._lock:
|
|
6271
|
+
self._stats.setdefault('worker_errors', {})
|
|
6272
|
+
self._stats['worker_errors'][worker_id] = \
|
|
6273
|
+
self._stats['worker_errors'].get(worker_id, 0) + 1
|
|
6274
|
+
|
|
6275
|
+
logger.info(f'[=] Worker {worker_id} exiting cleanly')
|
|
6276
|
+
with self._lock:
|
|
6277
|
+
self._stats['active_workers'] = max(0, self._stats['active_workers'] - 1)
|
|
6278
|
+
|
|
6279
|
+
|
|
6255
6280
|
def start(self):
|
|
6256
|
-
# Start worker threads.
|
|
6257
6281
|
if self._running:
|
|
6282
|
+
logger.warning('[=] ThreadedMessageQueue already running — ignoring duplicate start')
|
|
6258
6283
|
return
|
|
6259
|
-
|
|
6260
|
-
self._running
|
|
6284
|
+
|
|
6285
|
+
self._running = True
|
|
6286
|
+
self._stop_event = threading.Event() # allows prompt wake-up on stop
|
|
6287
|
+
self._workers = []
|
|
6288
|
+
|
|
6261
6289
|
for i in range(self._worker_threads):
|
|
6262
|
-
thread = threading.Thread(
|
|
6290
|
+
thread = threading.Thread(
|
|
6291
|
+
target=self._worker, args=(i, self._stop_event), daemon=True
|
|
6292
|
+
)
|
|
6263
6293
|
thread.start()
|
|
6264
6294
|
self._workers.append(thread)
|
|
6265
|
-
|
|
6295
|
+
|
|
6266
6296
|
with self._lock:
|
|
6267
6297
|
self._stats['active_workers'] = len(self._workers)
|
|
6268
|
-
|
|
6298
|
+
|
|
6269
6299
|
logger.info(f"[=] Threaded message queue started with {self._worker_threads} workers")
|
|
6270
|
-
|
|
6271
|
-
async def stop(self):
|
|
6272
|
-
# Stop worker threads gracefully.
|
|
6273
|
-
await self.result_queue.stop()
|
|
6274
6300
|
|
|
6275
|
-
|
|
6301
|
+
def stop(self, timeout: float = 5.0):
|
|
6302
|
+
"""
|
|
6303
|
+
completely rewritten. This is a threading-based class.
|
|
6304
|
+
"""
|
|
6305
|
+
if not self._running:
|
|
6306
|
+
return
|
|
6307
|
+
|
|
6308
|
+
logger.info("[=] Stopping threaded message queue...")
|
|
6309
|
+
self._running = False
|
|
6310
|
+
self._stop_event.set() # signal immediately
|
|
6311
|
+
|
|
6276
6312
|
for worker in self._workers:
|
|
6277
|
-
worker.
|
|
6313
|
+
worker.join(timeout=timeout)
|
|
6314
|
+
if worker.is_alive():
|
|
6315
|
+
logger.warning(
|
|
6316
|
+
f'[!] Worker thread {worker.name} did not stop within '
|
|
6317
|
+
f'{timeout}s — it will be abandoned as a daemon thread '
|
|
6318
|
+
f'(Python cannot forcibly kill threads)'
|
|
6319
|
+
)
|
|
6278
6320
|
|
|
6279
|
-
|
|
6280
|
-
|
|
6321
|
+
self._workers.clear() # clear references regardless,
|
|
6322
|
+
# any stragglers are daemon threads that
|
|
6323
|
+
# die automatically when the process exits
|
|
6281
6324
|
|
|
6282
|
-
self.
|
|
6325
|
+
with self._lock:
|
|
6326
|
+
self._stats['active_workers'] = 0
|
|
6283
6327
|
|
|
6284
6328
|
logger.info("[=] Threaded message queue stopped")
|
|
6285
|
-
|
|
6329
|
+
|
|
6330
|
+
|
|
6286
6331
|
def get_stats(self) -> Dict:
|
|
6287
|
-
# Get queue statistics.
|
|
6288
6332
|
with self._lock:
|
|
6289
6333
|
return {
|
|
6290
6334
|
**self._stats,
|
|
6291
6335
|
'queue_size': self.queue.qsize(),
|
|
6292
|
-
'workers': len(self._workers),
|
|
6336
|
+
'workers' : len(self._workers),
|
|
6293
6337
|
'is_running': self._running
|
|
6294
6338
|
}
|
|
6295
6339
|
|
|
6340
|
+
|
|
6296
6341
|
# Integrated inference module that allows multiple agents to connect and share their predictions, attention maps, and confidence scores for ensemble decision making.
|
|
6297
6342
|
# while also providing security features like authentication, rate limiting, and message validation.
|
|
6298
6343
|
class AgentDistributedInference:
|
|
@@ -8735,73 +8780,145 @@ class QueryNode:
|
|
|
8735
8780
|
# It collects requests over a short time window or until a maximum batch size is reached, then processes them together through the pipeline. This allows for improved throughput while still providing timely responses to individual requests.
|
|
8736
8781
|
class AutoBatcherAutomation:
|
|
8737
8782
|
def __init__(self, pipeline, max_batch_size=32, max_wait_ms=50):
|
|
8738
|
-
self.pipeline
|
|
8739
|
-
self.dataset = None
|
|
8783
|
+
self.pipeline = pipeline
|
|
8740
8784
|
self.max_batch_size = max_batch_size
|
|
8741
|
-
self.max_wait_ms
|
|
8742
|
-
|
|
8743
|
-
self.request_queue
|
|
8744
|
-
self.processing
|
|
8745
|
-
self.results
|
|
8746
|
-
self.
|
|
8747
|
-
|
|
8785
|
+
self.max_wait_ms = max_wait_ms
|
|
8786
|
+
|
|
8787
|
+
self.request_queue = deque()
|
|
8788
|
+
self.processing = False
|
|
8789
|
+
self.results = {}
|
|
8790
|
+
self.result_events = {} # per-request Event
|
|
8791
|
+
self.next_id = 0
|
|
8792
|
+
|
|
8793
|
+
self._state_lock = threading.Lock() # FIX 1 — guards processing flag + next_id
|
|
8794
|
+
|
|
8748
8795
|
def add_request(self, text, callback=None):
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8796
|
+
with self._state_lock:
|
|
8797
|
+
request_id = self.next_id
|
|
8798
|
+
self.next_id += 1
|
|
8799
|
+
|
|
8800
|
+
event = threading.Event()
|
|
8801
|
+
self.result_events[request_id] = event
|
|
8802
|
+
|
|
8803
|
+
self.request_queue.append({
|
|
8804
|
+
'id' : request_id,
|
|
8805
|
+
'text' : text,
|
|
8806
|
+
'callback' : callback,
|
|
8807
|
+
'timestamp': time.time()
|
|
8808
|
+
})
|
|
8809
|
+
|
|
8810
|
+
# check-and-set happens atomically under the same lock
|
|
8811
|
+
should_start = not self.processing
|
|
8812
|
+
if should_start:
|
|
8813
|
+
self.processing = True
|
|
8814
|
+
|
|
8815
|
+
if should_start:
|
|
8760
8816
|
self._start_processing()
|
|
8761
|
-
|
|
8817
|
+
|
|
8762
8818
|
return request_id
|
|
8763
|
-
|
|
8819
|
+
|
|
8764
8820
|
def _start_processing(self):
|
|
8765
|
-
self.processing = True
|
|
8766
8821
|
thread = threading.Thread(target=self._process_batches, daemon=True)
|
|
8767
8822
|
thread.start()
|
|
8768
|
-
|
|
8823
|
+
|
|
8769
8824
|
def _process_batches(self):
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
batch
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8825
|
+
try:
|
|
8826
|
+
while True:
|
|
8827
|
+
with self._state_lock:
|
|
8828
|
+
if not self.request_queue:
|
|
8829
|
+
break
|
|
8830
|
+
|
|
8831
|
+
time.sleep(self.max_wait_ms / 1000)
|
|
8832
|
+
|
|
8833
|
+
batch = []
|
|
8834
|
+
with self._state_lock:
|
|
8835
|
+
while self.request_queue and len(batch) < self.max_batch_size:
|
|
8836
|
+
batch.append(self.request_queue.popleft())
|
|
8837
|
+
|
|
8838
|
+
if batch:
|
|
8839
|
+
# never let one bad batch kill the worker permanently here.
|
|
8840
|
+
try:
|
|
8841
|
+
self._process_batch(batch)
|
|
8842
|
+
except Exception as e:
|
|
8843
|
+
logger.error(f'[!] Batch processing failed: {e}')
|
|
8844
|
+
# deliver the failure to every waiter in this batch
|
|
8845
|
+
# instead of leaving them hanging forever
|
|
8846
|
+
for req in batch:
|
|
8847
|
+
self._deliver_result(req, None, error=e)
|
|
8848
|
+
finally:
|
|
8849
|
+
# always reset processing, even if something above
|
|
8850
|
+
# raised unexpectedly
|
|
8851
|
+
with self._state_lock:
|
|
8852
|
+
self.processing = False
|
|
8853
|
+
|
|
8854
|
+
# need catch the case where requests arrived after the
|
|
8855
|
+
# while-loop's last empty check but before processing=False landed
|
|
8856
|
+
with self._state_lock:
|
|
8857
|
+
still_pending = bool(self.request_queue)
|
|
8858
|
+
if still_pending and not self.processing:
|
|
8859
|
+
self.processing = True
|
|
8860
|
+
restart = True
|
|
8861
|
+
else:
|
|
8862
|
+
restart = False
|
|
8863
|
+
if restart:
|
|
8864
|
+
self._start_processing()
|
|
8865
|
+
|
|
8785
8866
|
def _process_batch(self, batch):
|
|
8786
|
-
texts
|
|
8787
|
-
|
|
8867
|
+
texts = [req['text'] for req in batch]
|
|
8788
8868
|
results = self.pipeline.prediction_batch(texts)
|
|
8789
|
-
|
|
8790
|
-
# Send results back
|
|
8869
|
+
|
|
8791
8870
|
for i, req in enumerate(batch):
|
|
8792
8871
|
result = results[i] if i < len(results) else None
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8872
|
+
self._deliver_result(req, result)
|
|
8873
|
+
|
|
8874
|
+
def _deliver_result(self, req, result, error=None):
|
|
8875
|
+
"""Single delivery path — callback or stored result, always signals."""
|
|
8876
|
+
if req['callback']:
|
|
8877
|
+
try:
|
|
8878
|
+
req['callback'](result if error is None else error)
|
|
8879
|
+
except Exception as cb_err:
|
|
8880
|
+
logger.error(f'[!] Callback failed for request {req["id"]}: {cb_err}')
|
|
8881
|
+
else:
|
|
8882
|
+
with self._state_lock:
|
|
8883
|
+
self.results[req['id']] = result if error is None else None
|
|
8884
|
+
|
|
8885
|
+
# signal the waiting event
|
|
8886
|
+
event = self.result_events.get(req['id'])
|
|
8887
|
+
if event:
|
|
8888
|
+
event.set()
|
|
8889
|
+
|
|
8798
8890
|
def get_result(self, request_id, timeout=5):
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8891
|
+
event = self.result_events.get(request_id)
|
|
8892
|
+
if event is None:
|
|
8893
|
+
return None
|
|
8894
|
+
|
|
8895
|
+
# blocks efficiently
|
|
8896
|
+
signaled = event.wait(timeout=timeout)
|
|
8897
|
+
|
|
8898
|
+
with self._state_lock:
|
|
8899
|
+
# always need clean up
|
|
8900
|
+
result = self.results.pop(request_id, None)
|
|
8901
|
+
self.result_events.pop(request_id, None)
|
|
8902
|
+
|
|
8903
|
+
return result if signaled else None
|
|
8904
|
+
|
|
8905
|
+
def cleanup_stale(self, max_age_seconds=300):
|
|
8906
|
+
"""
|
|
8907
|
+
periodic sweep for requests that were never collected
|
|
8908
|
+
via get_result (e.g. caller crashed or forgot to call it).
|
|
8909
|
+
Call this periodically from a health check loop.
|
|
8910
|
+
"""
|
|
8911
|
+
with self._state_lock:
|
|
8912
|
+
now = time.time()
|
|
8913
|
+
stale_ids = [
|
|
8914
|
+
rid for rid, event in self.result_events.items()
|
|
8915
|
+
if event.is_set() # already delivered but never collected
|
|
8916
|
+
]
|
|
8917
|
+
for rid in stale_ids:
|
|
8918
|
+
self.results.pop(rid, None)
|
|
8919
|
+
self.result_events.pop(rid, None)
|
|
8920
|
+
if stale_ids:
|
|
8921
|
+
logger.info(f'[=] Cleaned up {len(stale_ids)} stale results')
|
|
8805
8922
|
|
|
8806
8923
|
|
|
8807
8924
|
# The IntegratedPipeline class serves as the central component that integrates all the different modules and functionalities of the system.
|
|
@@ -13359,63 +13476,74 @@ class AsyncResultQueue:
|
|
|
13359
13476
|
}
|
|
13360
13477
|
|
|
13361
13478
|
class WorkerPool:
|
|
13362
|
-
"""
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
def __init__(self, result_queue: AsyncResultQueue, num_workers: int = 4):
|
|
13479
|
+
"""Worker pool that processes requests from the result queue."""
|
|
13480
|
+
|
|
13481
|
+
def __init__(self, result_queue: AsyncResultQueue, num_workers: int = 4,
|
|
13482
|
+
max_consecutive_errors: int = 10):
|
|
13367
13483
|
self.result_queue = result_queue
|
|
13368
|
-
self.num_workers
|
|
13484
|
+
self.num_workers = num_workers
|
|
13369
13485
|
self._workers: List[asyncio.Task] = []
|
|
13370
|
-
self._running
|
|
13371
|
-
|
|
13486
|
+
self._running = False
|
|
13487
|
+
self._start_lock = asyncio.Lock() # FIX 1 — prevent double-start
|
|
13488
|
+
self.max_consecutive_errors = max_consecutive_errors
|
|
13489
|
+
|
|
13490
|
+
# per-worker health tracking — FIX 4
|
|
13491
|
+
self._worker_error_counts = [0] * num_workers
|
|
13492
|
+
self._worker_last_active = [0.0] * num_workers
|
|
13493
|
+
|
|
13372
13494
|
async def start(self, predict_func):
|
|
13373
|
-
#
|
|
13374
|
-
self.
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13495
|
+
# guard against double-start leaking orphaned tasks
|
|
13496
|
+
async with self._start_lock:
|
|
13497
|
+
if self._running:
|
|
13498
|
+
logger.warning('[=] WorkerPool.start() called while already '
|
|
13499
|
+
'running — ignoring duplicate start')
|
|
13500
|
+
return
|
|
13501
|
+
|
|
13502
|
+
self._running = True
|
|
13503
|
+
self._worker_error_counts = [0] * self.num_workers
|
|
13504
|
+
self._worker_last_active = [time.time()] * self.num_workers
|
|
13505
|
+
|
|
13506
|
+
self._workers = [
|
|
13507
|
+
asyncio.create_task(self._worker(predict_func, worker_idx=i))
|
|
13508
|
+
for i in range(self.num_workers)
|
|
13509
|
+
]
|
|
13510
|
+
await self.result_queue.start()
|
|
13511
|
+
logger.info(f'[=] WorkerPool started with {self.num_workers} workers')
|
|
13379
13512
|
|
|
13380
|
-
await self.result_queue.start()
|
|
13381
|
-
|
|
13382
13513
|
async def stop(self):
|
|
13383
|
-
# Stop all workers
|
|
13384
13514
|
self._running = False
|
|
13385
13515
|
await self.result_queue.stop()
|
|
13386
13516
|
|
|
13387
|
-
# cancel and wait for all workers to exit cleanly
|
|
13388
13517
|
for worker in self._workers:
|
|
13389
13518
|
worker.cancel()
|
|
13390
13519
|
|
|
13391
13520
|
if self._workers:
|
|
13392
13521
|
await asyncio.gather(*self._workers, return_exceptions=True)
|
|
13393
13522
|
|
|
13394
|
-
self._workers.clear()
|
|
13523
|
+
self._workers.clear() # always clear, no stale references remain
|
|
13524
|
+
logger.info('[=] WorkerPool stopped, all workers cleared')
|
|
13395
13525
|
|
|
13526
|
+
async def _worker(self, predict_func, worker_idx: int):
|
|
13527
|
+
consecutive_errors = 0
|
|
13396
13528
|
|
|
13397
|
-
async def _worker(self, predict_func):
|
|
13398
|
-
# Worker that processes requests
|
|
13399
13529
|
while self._running:
|
|
13400
13530
|
try:
|
|
13401
|
-
# Get next pending request
|
|
13402
13531
|
try:
|
|
13403
13532
|
request = await asyncio.wait_for(
|
|
13404
|
-
self.result_queue.get_pending(),
|
|
13405
|
-
timeout=1.0
|
|
13533
|
+
self.result_queue.get_pending(), timeout=1.0
|
|
13406
13534
|
)
|
|
13407
13535
|
except asyncio.TimeoutError:
|
|
13408
|
-
continue
|
|
13536
|
+
continue
|
|
13409
13537
|
|
|
13410
13538
|
if not request:
|
|
13411
13539
|
continue
|
|
13412
|
-
|
|
13413
|
-
|
|
13540
|
+
|
|
13541
|
+
self._worker_last_active[worker_idx] = time.time()
|
|
13542
|
+
|
|
13414
13543
|
async with self.result_queue._lock:
|
|
13415
13544
|
request.status = RequestStatus.PROCESSING
|
|
13416
|
-
|
|
13545
|
+
|
|
13417
13546
|
try:
|
|
13418
|
-
# Execute prediction (run sync function in thread pool)
|
|
13419
13547
|
result = await asyncio.wait_for(
|
|
13420
13548
|
asyncio.to_thread(
|
|
13421
13549
|
predict_func,
|
|
@@ -13425,25 +13553,69 @@ class WorkerPool:
|
|
|
13425
13553
|
),
|
|
13426
13554
|
timeout=30.0
|
|
13427
13555
|
)
|
|
13428
|
-
|
|
13429
|
-
# Mark as completed
|
|
13430
13556
|
await self.result_queue.complete(request.request_id, result)
|
|
13557
|
+
consecutive_errors = 0
|
|
13558
|
+
|
|
13431
13559
|
except asyncio.TimeoutError:
|
|
13432
|
-
|
|
13560
|
+
# log that the underlying thread may still be
|
|
13561
|
+
# running; can't force-kill it, but need at least
|
|
13562
|
+
# that this is a known limitation rather than silent
|
|
13563
|
+
logger.warning(
|
|
13564
|
+
f'[!] Worker {worker_idx}: request {request.request_id} '
|
|
13565
|
+
f'timed out after 30s. The underlying OS thread may '
|
|
13566
|
+
f'still be running in the background (Python threads '
|
|
13567
|
+
f'cannot be forcibly cancelled) — consider making '
|
|
13568
|
+
f'predict_func more responsive to avoid thread buildup.'
|
|
13569
|
+
)
|
|
13570
|
+
|
|
13571
|
+
await self.result_queue._mark_failed(request.request_id, 'timeout')
|
|
13572
|
+
|
|
13433
13573
|
except asyncio.CancelledError:
|
|
13434
|
-
# shutdown mid-prediction — mark failed and exit cleanly
|
|
13435
13574
|
await self.result_queue._mark_failed(request.request_id, 'cancelled')
|
|
13436
|
-
break
|
|
13575
|
+
break
|
|
13437
13576
|
|
|
13438
13577
|
except Exception as e:
|
|
13439
|
-
# Mark as failed
|
|
13440
13578
|
await self.result_queue._mark_failed(request.request_id, str(e))
|
|
13441
|
-
|
|
13579
|
+
consecutive_errors = 0 # per-request errors don't count
|
|
13580
|
+
# toward the outer circuit breaker
|
|
13581
|
+
|
|
13442
13582
|
except asyncio.CancelledError:
|
|
13443
13583
|
break
|
|
13584
|
+
|
|
13444
13585
|
except Exception as e:
|
|
13445
|
-
|
|
13446
|
-
|
|
13586
|
+
consecutive_errors += 1
|
|
13587
|
+
self._worker_error_counts[worker_idx] = consecutive_errors
|
|
13588
|
+
|
|
13589
|
+
logger.error(f'[=] Worker {worker_idx} error '
|
|
13590
|
+
f'({consecutive_errors}/{self.max_consecutive_errors}): {e}')
|
|
13591
|
+
|
|
13592
|
+
# circuit breaker with exponential backoff
|
|
13593
|
+
if consecutive_errors >= self.max_consecutive_errors:
|
|
13594
|
+
logger.error(
|
|
13595
|
+
f'[!!] Worker {worker_idx} exceeded '
|
|
13596
|
+
f'{self.max_consecutive_errors} consecutive errors — '
|
|
13597
|
+
f'stopping this worker to avoid runaway resource use. '
|
|
13598
|
+
f'Pool now running with reduced capacity.'
|
|
13599
|
+
)
|
|
13600
|
+
break
|
|
13601
|
+
|
|
13602
|
+
backoff = min(0.1 * (2 ** consecutive_errors), 5.0)
|
|
13603
|
+
await asyncio.sleep(backoff)
|
|
13604
|
+
|
|
13605
|
+
def get_health(self) -> Dict:
|
|
13606
|
+
"""visibility into worker pool health"""
|
|
13607
|
+
now = time.time()
|
|
13608
|
+
return {
|
|
13609
|
+
'num_workers' : self.num_workers,
|
|
13610
|
+
'active_workers' : sum(1 for t in self._workers if not t.done()),
|
|
13611
|
+
'dead_workers' : sum(1 for t in self._workers if t.done()),
|
|
13612
|
+
'worker_error_counts': list(self._worker_error_counts),
|
|
13613
|
+
'worker_idle_seconds': [
|
|
13614
|
+
round(now - t, 1) for t in self._worker_last_active
|
|
13615
|
+
],
|
|
13616
|
+
'is_running' : self._running,
|
|
13617
|
+
}
|
|
13618
|
+
|
|
13447
13619
|
|
|
13448
13620
|
class PipelineAsyncManager:
|
|
13449
13621
|
"""
|