stabilize 0.9.2__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.
- stabilize/__init__.py +29 -0
- stabilize/cli.py +1193 -0
- stabilize/context/__init__.py +7 -0
- stabilize/context/stage_context.py +170 -0
- stabilize/dag/__init__.py +15 -0
- stabilize/dag/graph.py +215 -0
- stabilize/dag/topological.py +199 -0
- stabilize/examples/__init__.py +1 -0
- stabilize/examples/docker-example.py +759 -0
- stabilize/examples/golden-standard-expected-result.txt +1 -0
- stabilize/examples/golden-standard.py +488 -0
- stabilize/examples/http-example.py +606 -0
- stabilize/examples/llama-example.py +662 -0
- stabilize/examples/python-example.py +731 -0
- stabilize/examples/shell-example.py +399 -0
- stabilize/examples/ssh-example.py +603 -0
- stabilize/handlers/__init__.py +53 -0
- stabilize/handlers/base.py +226 -0
- stabilize/handlers/complete_stage.py +209 -0
- stabilize/handlers/complete_task.py +75 -0
- stabilize/handlers/complete_workflow.py +150 -0
- stabilize/handlers/run_task.py +369 -0
- stabilize/handlers/start_stage.py +262 -0
- stabilize/handlers/start_task.py +74 -0
- stabilize/handlers/start_workflow.py +136 -0
- stabilize/launcher.py +307 -0
- stabilize/migrations/01KDQ4N9QPJ6Q4MCV3V9GHWPV4_initial_schema.sql +97 -0
- stabilize/migrations/01KDRK3TXW4R2GERC1WBCQYJGG_rag_embeddings.sql +25 -0
- stabilize/migrations/__init__.py +1 -0
- stabilize/models/__init__.py +15 -0
- stabilize/models/stage.py +389 -0
- stabilize/models/status.py +146 -0
- stabilize/models/task.py +125 -0
- stabilize/models/workflow.py +317 -0
- stabilize/orchestrator.py +113 -0
- stabilize/persistence/__init__.py +28 -0
- stabilize/persistence/connection.py +185 -0
- stabilize/persistence/factory.py +136 -0
- stabilize/persistence/memory.py +214 -0
- stabilize/persistence/postgres.py +655 -0
- stabilize/persistence/sqlite.py +674 -0
- stabilize/persistence/store.py +235 -0
- stabilize/queue/__init__.py +59 -0
- stabilize/queue/messages.py +377 -0
- stabilize/queue/processor.py +312 -0
- stabilize/queue/queue.py +526 -0
- stabilize/queue/sqlite_queue.py +354 -0
- stabilize/rag/__init__.py +19 -0
- stabilize/rag/assistant.py +459 -0
- stabilize/rag/cache.py +294 -0
- stabilize/stages/__init__.py +11 -0
- stabilize/stages/builder.py +253 -0
- stabilize/tasks/__init__.py +19 -0
- stabilize/tasks/interface.py +335 -0
- stabilize/tasks/registry.py +255 -0
- stabilize/tasks/result.py +283 -0
- stabilize-0.9.2.dist-info/METADATA +301 -0
- stabilize-0.9.2.dist-info/RECORD +61 -0
- stabilize-0.9.2.dist-info/WHEEL +4 -0
- stabilize-0.9.2.dist-info/entry_points.txt +2 -0
- stabilize-0.9.2.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Queue processor for handling messages.
|
|
3
|
+
|
|
4
|
+
This module provides the QueueProcessor class that polls messages from
|
|
5
|
+
the queue and dispatches them to appropriate handlers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import timedelta
|
|
17
|
+
from typing import Any, Generic, TypeVar
|
|
18
|
+
|
|
19
|
+
from stabilize.queue.messages import Message, get_message_type_name
|
|
20
|
+
from stabilize.queue.queue import Queue
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
M = TypeVar("M", bound=Message)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MessageHandler(Generic[M]):
|
|
28
|
+
"""
|
|
29
|
+
Base class for message handlers.
|
|
30
|
+
|
|
31
|
+
Each handler processes a specific type of message.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def message_type(self) -> type[M]:
|
|
36
|
+
"""Return the type of message this handler processes."""
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
def handle(self, message: M) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Handle a message.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
message: The message to handle
|
|
45
|
+
"""
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class QueueProcessorConfig:
|
|
51
|
+
"""Configuration for the queue processor."""
|
|
52
|
+
|
|
53
|
+
# How often to poll the queue (milliseconds)
|
|
54
|
+
poll_frequency_ms: int = 50
|
|
55
|
+
|
|
56
|
+
# Maximum number of concurrent message handlers
|
|
57
|
+
max_workers: int = 10
|
|
58
|
+
|
|
59
|
+
# Delay before reprocessing a failed message
|
|
60
|
+
retry_delay: timedelta = timedelta(seconds=15)
|
|
61
|
+
|
|
62
|
+
# Whether to stop on unhandled exceptions
|
|
63
|
+
stop_on_error: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class QueueProcessor:
|
|
67
|
+
"""
|
|
68
|
+
Processes messages from a queue using registered handlers.
|
|
69
|
+
|
|
70
|
+
The processor polls the queue at regular intervals and dispatches
|
|
71
|
+
messages to appropriate handlers. Handlers run in a thread pool
|
|
72
|
+
for concurrent processing.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
queue = InMemoryQueue()
|
|
76
|
+
processor = QueueProcessor(queue)
|
|
77
|
+
processor.register_handler(StartWorkflowHandler(queue, repository))
|
|
78
|
+
processor.start()
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
queue: Queue,
|
|
84
|
+
config: QueueProcessorConfig | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Initialize the queue processor.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
queue: The queue to process
|
|
91
|
+
config: Optional configuration
|
|
92
|
+
"""
|
|
93
|
+
self.queue = queue
|
|
94
|
+
self.config = config or QueueProcessorConfig()
|
|
95
|
+
self._handlers: dict[type[Message], MessageHandler[Any]] = {}
|
|
96
|
+
self._running = False
|
|
97
|
+
self._executor: ThreadPoolExecutor | None = None
|
|
98
|
+
self._poll_thread: threading.Thread | None = None
|
|
99
|
+
self._lock = threading.Lock()
|
|
100
|
+
self._active_count = 0
|
|
101
|
+
|
|
102
|
+
def register_handler(self, handler: MessageHandler[Any]) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Register a message handler.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
handler: The handler to register
|
|
108
|
+
"""
|
|
109
|
+
self._handlers[handler.message_type] = handler
|
|
110
|
+
logger.debug(f"Registered handler for {handler.message_type.__name__}")
|
|
111
|
+
|
|
112
|
+
def register_handler_func(
|
|
113
|
+
self,
|
|
114
|
+
message_type: type[M],
|
|
115
|
+
handler_func: Callable[[M], None],
|
|
116
|
+
) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Register a handler function for a message type.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
message_type: The type of message to handle
|
|
122
|
+
handler_func: Function to call with the message
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
class FuncHandler(MessageHandler[M]):
|
|
126
|
+
@property
|
|
127
|
+
def message_type(self) -> type[M]:
|
|
128
|
+
return message_type
|
|
129
|
+
|
|
130
|
+
def handle(self, message: M) -> None:
|
|
131
|
+
handler_func(message)
|
|
132
|
+
|
|
133
|
+
self.register_handler(FuncHandler())
|
|
134
|
+
|
|
135
|
+
def start(self) -> None:
|
|
136
|
+
"""Start the queue processor."""
|
|
137
|
+
if self._running:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
self._running = True
|
|
141
|
+
self._executor = ThreadPoolExecutor(max_workers=self.config.max_workers)
|
|
142
|
+
self._poll_thread = threading.Thread(target=self._poll_loop, daemon=True)
|
|
143
|
+
self._poll_thread.start()
|
|
144
|
+
logger.info("Queue processor started")
|
|
145
|
+
|
|
146
|
+
def stop(self, wait: bool = True) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Stop the queue processor.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
wait: Whether to wait for pending messages to complete
|
|
152
|
+
"""
|
|
153
|
+
self._running = False
|
|
154
|
+
|
|
155
|
+
if self._poll_thread:
|
|
156
|
+
self._poll_thread.join(timeout=5.0)
|
|
157
|
+
|
|
158
|
+
if self._executor:
|
|
159
|
+
self._executor.shutdown(wait=wait)
|
|
160
|
+
|
|
161
|
+
logger.info("Queue processor stopped")
|
|
162
|
+
|
|
163
|
+
def _poll_loop(self) -> None:
|
|
164
|
+
"""Main polling loop."""
|
|
165
|
+
poll_interval = self.config.poll_frequency_ms / 1000.0
|
|
166
|
+
|
|
167
|
+
while self._running:
|
|
168
|
+
try:
|
|
169
|
+
# Check if we have capacity
|
|
170
|
+
with self._lock:
|
|
171
|
+
if self._active_count >= self.config.max_workers:
|
|
172
|
+
time.sleep(poll_interval)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Try to get a message
|
|
176
|
+
message = self.queue.poll_one()
|
|
177
|
+
|
|
178
|
+
if message:
|
|
179
|
+
self._submit_message(message)
|
|
180
|
+
else:
|
|
181
|
+
time.sleep(poll_interval)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Error in poll loop: {e}", exc_info=True)
|
|
185
|
+
if self.config.stop_on_error:
|
|
186
|
+
self._running = False
|
|
187
|
+
break
|
|
188
|
+
time.sleep(poll_interval)
|
|
189
|
+
|
|
190
|
+
def _submit_message(self, message: Message) -> None:
|
|
191
|
+
"""Submit a message to the thread pool for processing."""
|
|
192
|
+
with self._lock:
|
|
193
|
+
self._active_count += 1
|
|
194
|
+
|
|
195
|
+
def process_and_ack() -> None:
|
|
196
|
+
try:
|
|
197
|
+
self._handle_message(message)
|
|
198
|
+
self.queue.ack(message)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.error(
|
|
201
|
+
f"Error handling {get_message_type_name(message)}: {e}",
|
|
202
|
+
exc_info=True,
|
|
203
|
+
)
|
|
204
|
+
# Message will be reprocessed after lock expires or reschedule
|
|
205
|
+
self.queue.reschedule(message, self.config.retry_delay)
|
|
206
|
+
finally:
|
|
207
|
+
with self._lock:
|
|
208
|
+
self._active_count -= 1
|
|
209
|
+
|
|
210
|
+
if self._executor is not None:
|
|
211
|
+
self._executor.submit(process_and_ack)
|
|
212
|
+
|
|
213
|
+
def _handle_message(self, message: Message) -> None:
|
|
214
|
+
"""Handle a message with the appropriate handler."""
|
|
215
|
+
message_type = type(message)
|
|
216
|
+
handler = self._handlers.get(message_type)
|
|
217
|
+
|
|
218
|
+
if handler is None:
|
|
219
|
+
logger.warning(f"No handler registered for {get_message_type_name(message)}")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
logger.debug(f"Handling {get_message_type_name(message)} (execution={getattr(message, 'execution_id', 'N/A')})")
|
|
223
|
+
|
|
224
|
+
handler.handle(message)
|
|
225
|
+
|
|
226
|
+
def process_one(self) -> bool:
|
|
227
|
+
"""
|
|
228
|
+
Process a single message synchronously.
|
|
229
|
+
|
|
230
|
+
Useful for testing and debugging.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if a message was processed, False otherwise
|
|
234
|
+
"""
|
|
235
|
+
message = self.queue.poll_one()
|
|
236
|
+
if message:
|
|
237
|
+
try:
|
|
238
|
+
self._handle_message(message)
|
|
239
|
+
self.queue.ack(message)
|
|
240
|
+
return True
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Error handling message: {e}", exc_info=True)
|
|
243
|
+
self.queue.reschedule(message, self.config.retry_delay)
|
|
244
|
+
raise
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
def process_all(self, timeout: float = 60.0) -> int:
|
|
248
|
+
"""
|
|
249
|
+
Process all messages synchronously until queue is empty.
|
|
250
|
+
|
|
251
|
+
Useful for testing.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
timeout: Maximum time to wait for processing
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Number of messages processed
|
|
258
|
+
"""
|
|
259
|
+
count = 0
|
|
260
|
+
start = time.time()
|
|
261
|
+
|
|
262
|
+
while time.time() - start < timeout:
|
|
263
|
+
if self.queue.size() == 0:
|
|
264
|
+
break
|
|
265
|
+
if self.process_one():
|
|
266
|
+
count += 1
|
|
267
|
+
else:
|
|
268
|
+
# No ready messages, wait a bit
|
|
269
|
+
time.sleep(0.01)
|
|
270
|
+
|
|
271
|
+
return count
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def is_running(self) -> bool:
|
|
275
|
+
"""Check if the processor is running."""
|
|
276
|
+
return self._running
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def active_count(self) -> int:
|
|
280
|
+
"""Get the number of actively processing messages."""
|
|
281
|
+
with self._lock:
|
|
282
|
+
return self._active_count
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class SynchronousQueueProcessor(QueueProcessor):
|
|
286
|
+
"""
|
|
287
|
+
A synchronous queue processor that processes messages immediately.
|
|
288
|
+
|
|
289
|
+
Useful for testing where you want deterministic execution order.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
def __init__(self, queue: Queue) -> None:
|
|
293
|
+
super().__init__(queue)
|
|
294
|
+
self._running = True
|
|
295
|
+
|
|
296
|
+
def start(self) -> None:
|
|
297
|
+
"""No-op for synchronous processor."""
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
def stop(self, wait: bool = True) -> None:
|
|
301
|
+
"""No-op for synchronous processor."""
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
def push_and_process(self, message: Message) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Push a message and process it immediately.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
message: The message to push and process
|
|
310
|
+
"""
|
|
311
|
+
self.queue.push(message)
|
|
312
|
+
self.process_all(timeout=5.0)
|