jararaca 0.2.37a12__tar.gz → 0.3.0__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.
Potentially problematic release.
This version of jararaca might be problematic. Click here for more details.
- {jararaca-0.2.37a12 → jararaca-0.3.0}/PKG-INFO +3 -1
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/index.md +32 -0
- jararaca-0.3.0/docs/scheduler.md +216 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/pyproject.toml +3 -1
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/__init__.py +13 -4
- jararaca-0.3.0/src/jararaca/broker_backend/__init__.py +102 -0
- jararaca-0.3.0/src/jararaca/broker_backend/mapper.py +21 -0
- jararaca-0.3.0/src/jararaca/broker_backend/redis_broker_backend.py +162 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/cli.py +80 -19
- jararaca-0.3.0/src/jararaca/messagebus/__init__.py +3 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/messagebus/decorators.py +57 -21
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +51 -31
- jararaca-0.3.0/src/jararaca/messagebus/interceptors/publisher_interceptor.py +34 -0
- jararaca-0.3.0/src/jararaca/messagebus/message.py +27 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/messagebus/publisher.py +31 -2
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/messagebus/worker.py +12 -16
- jararaca-0.3.0/src/jararaca/messagebus/worker_v2.py +608 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/microservice.py +1 -1
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/scheduler/decorators.py +34 -1
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/scheduler/scheduler.py +16 -9
- jararaca-0.3.0/src/jararaca/scheduler/scheduler_v2.py +346 -0
- jararaca-0.3.0/src/jararaca/scheduler/types.py +7 -0
- jararaca-0.3.0/src/jararaca/tools/app_config/__init__.py +0 -0
- jararaca-0.3.0/src/jararaca/utils/__init__.py +0 -0
- jararaca-0.3.0/src/jararaca/utils/rabbitmq_utils.py +84 -0
- jararaca-0.2.37a12/src/jararaca/messagebus/__init__.py +0 -3
- jararaca-0.2.37a12/src/jararaca/messagebus/types.py +0 -30
- {jararaca-0.2.37a12 → jararaca-0.3.0}/LICENSE +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/README.md +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/CNAME +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/architecture.md +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.jpeg +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.webp +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/assets/tracing_example.png +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/messagebus.md +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/stylesheets/custom.css +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/docs/websocket.md +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/__main__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/common/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/core/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/core/providers.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/core/uow.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/di.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/files/entity.py.mako +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/lifecycle.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/messagebus/bus_message_controller.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/messagebus/interceptors → jararaca-0.3.0/src/jararaca/messagebus/consumers}/__init__.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/observability/providers → jararaca-0.3.0/src/jararaca/messagebus/interceptors}/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/observability/decorators.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/observability/interceptor.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/persistence/interceptors → jararaca-0.3.0/src/jararaca/observability/providers}/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/observability/providers/otel.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/persistence/base.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/persistence/exports.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/presentation → jararaca-0.3.0/src/jararaca/persistence/interceptors}/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/persistence/interceptors/aiosqa_interceptor.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/persistence/session.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/persistence/sort_filter.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/persistence/utilities.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/presentation/websocket → jararaca-0.3.0/src/jararaca/presentation}/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/decorators.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/hooks.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/http_microservice.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/server.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/rpc → jararaca-0.3.0/src/jararaca/presentation/websocket}/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/websocket/base_types.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/websocket/context.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/websocket/decorators.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/websocket/redis.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/websocket/types.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/presentation/websocket/websocket_interceptor.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/py.typed +0 -0
- {jararaca-0.2.37a12/src/jararaca/rpc/http → jararaca-0.3.0/src/jararaca/rpc}/__init__.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/rpc/http/backends → jararaca-0.3.0/src/jararaca/rpc/http}/__init__.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/scheduler → jararaca-0.3.0/src/jararaca/rpc/http/backends}/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/rpc/http/backends/httpx.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/rpc/http/backends/otel.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/rpc/http/decorators.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/rpc/http/httpx.py +0 -0
- {jararaca-0.2.37a12/src/jararaca/tools/app_config → jararaca-0.3.0/src/jararaca/scheduler}/__init__.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/tools/app_config/decorators.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/tools/app_config/interceptor.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/tools/metadata.py +0 -0
- {jararaca-0.2.37a12 → jararaca-0.3.0}/src/jararaca/tools/typescript/interface_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: jararaca
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A simple and fast API framework for Python
|
|
5
5
|
Author: Lucas S
|
|
6
6
|
Author-email: me@luscasleo.dev
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
12
12
|
Provides-Extra: docs
|
|
13
13
|
Provides-Extra: http
|
|
14
14
|
Provides-Extra: opentelemetry
|
|
15
|
+
Provides-Extra: watch
|
|
15
16
|
Requires-Dist: aio-pika (>=9.4.3,<10.0.0)
|
|
16
17
|
Requires-Dist: croniter (>=3.0.3,<4.0.0)
|
|
17
18
|
Requires-Dist: fastapi (>=0.113.0,<0.114.0)
|
|
@@ -27,6 +28,7 @@ Requires-Dist: types-croniter (>=3.0.3.20240731,<4.0.0.0)
|
|
|
27
28
|
Requires-Dist: types-redis (>=4.6.0.20240903,<5.0.0.0)
|
|
28
29
|
Requires-Dist: uvicorn (>=0.30.6,<0.31.0)
|
|
29
30
|
Requires-Dist: uvloop (>=0.20.0,<0.21.0)
|
|
31
|
+
Requires-Dist: watchdog (>=3.0.0,<4.0.0) ; extra == "watch"
|
|
30
32
|
Requires-Dist: websockets (>=13.0.1,<14.0.0)
|
|
31
33
|
Project-URL: Repository, https://github.com/LuscasLeo/jararaca
|
|
32
34
|
Description-Content-Type: text/markdown
|
|
@@ -40,6 +40,19 @@ Starts a message bus worker that processes asynchronous messages from a message
|
|
|
40
40
|
- `--queue`: Queue name (default: "jararaca_q")
|
|
41
41
|
- `--prefetch-count`: Number of messages to prefetch (default: 1)
|
|
42
42
|
|
|
43
|
+
### `worker_v2` - Enhanced Message Bus Worker
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
jararaca worker_v2 APP_PATH [OPTIONS]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Starts an enhanced version of the message bus worker with improved backend support.
|
|
50
|
+
|
|
51
|
+
**Options:**
|
|
52
|
+
|
|
53
|
+
- `--broker-url`: The URL for the message broker (required)
|
|
54
|
+
- `--backend-url`: The URL for the message broker backend (required)
|
|
55
|
+
|
|
43
56
|
### `server` - HTTP Server
|
|
44
57
|
|
|
45
58
|
```bash
|
|
@@ -83,6 +96,11 @@ uvicorn app_module:asgi_app
|
|
|
83
96
|
|
|
84
97
|
Starts a FastAPI HTTP server for your microservice.
|
|
85
98
|
|
|
99
|
+
**Options:**
|
|
100
|
+
|
|
101
|
+
- `--host`: Host to bind the server (default: "0.0.0.0")
|
|
102
|
+
- `--port`: Port to bind the server (default: 8000)
|
|
103
|
+
|
|
86
104
|
### `scheduler` - Task Scheduler
|
|
87
105
|
|
|
88
106
|
```bash
|
|
@@ -95,6 +113,20 @@ Runs scheduled tasks defined in your application using cron expressions.
|
|
|
95
113
|
|
|
96
114
|
- `--interval`: Polling interval in seconds (default: 1)
|
|
97
115
|
|
|
116
|
+
### `scheduler_v2` - Enhanced Task Scheduler
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
jararaca scheduler_v2 APP_PATH [OPTIONS]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Runs an enhanced version of the task scheduler with support for message broker backend integration.
|
|
123
|
+
|
|
124
|
+
**Options:**
|
|
125
|
+
|
|
126
|
+
- `--interval`: Polling interval in seconds (default: 1, required)
|
|
127
|
+
- `--broker-url`: The URL for the message broker (required)
|
|
128
|
+
- `--backend-url`: The URL for the message broker backend (required)
|
|
129
|
+
|
|
98
130
|
### `gen-tsi` - Generate TypeScript Interfaces
|
|
99
131
|
|
|
100
132
|
```bash
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Jararaca Scheduler System
|
|
2
|
+
|
|
3
|
+
The scheduler system in Jararaca provides robust task scheduling capabilities that allow you to run periodic tasks using cron expressions. This document explains how the scheduler works, its different implementations, and how to use it in your applications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Jararaca scheduler allows you to:
|
|
8
|
+
|
|
9
|
+
- Run background tasks at scheduled intervals
|
|
10
|
+
- Use cron expressions for flexible scheduling
|
|
11
|
+
- Control overlap behavior (whether to allow multiple instances of the same task)
|
|
12
|
+
- Distribute scheduled tasks across multiple instances
|
|
13
|
+
- Handle delayed message execution
|
|
14
|
+
|
|
15
|
+
The scheduler has two implementations:
|
|
16
|
+
1. **Basic Scheduler** - Simple scheduler for local execution
|
|
17
|
+
2. **Enhanced Scheduler (V2)** - Distributed scheduler with improved backend support and message broker integration
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
graph TD
|
|
21
|
+
A[Microservice] --> B[Scheduler System]
|
|
22
|
+
B --> C[Basic Scheduler]
|
|
23
|
+
B --> D[Enhanced Scheduler V2]
|
|
24
|
+
|
|
25
|
+
C --> E[Local Task Execution]
|
|
26
|
+
D --> F[Message Broker]
|
|
27
|
+
F --> G[Workers]
|
|
28
|
+
D --> H[Backend Store]
|
|
29
|
+
H --> I[Last Execution Time]
|
|
30
|
+
H --> J[Delayed Messages]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Using the Scheduler
|
|
34
|
+
|
|
35
|
+
### Defining Scheduled Tasks
|
|
36
|
+
|
|
37
|
+
You can define scheduled tasks using the `@ScheduledAction` decorator:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from jararaca import ScheduledAction
|
|
41
|
+
|
|
42
|
+
class TasksController:
|
|
43
|
+
@ScheduledAction("*/5 * * * *") # Run every 5 minutes
|
|
44
|
+
async def scheduled_task(self):
|
|
45
|
+
# Your task implementation
|
|
46
|
+
print("This runs every 5 minutes")
|
|
47
|
+
|
|
48
|
+
@ScheduledAction("0 */2 * * *", allow_overlap=False, timeout=60)
|
|
49
|
+
async def heavy_task(self):
|
|
50
|
+
# A heavier task that shouldn't overlap
|
|
51
|
+
print("This runs every 2 hours without overlap")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Scheduler Decorator Options
|
|
55
|
+
|
|
56
|
+
The `@ScheduledAction` decorator accepts several parameters:
|
|
57
|
+
|
|
58
|
+
- `cron`: A string representing the cron expression for the scheduled action
|
|
59
|
+
- `allow_overlap`: A boolean indicating if new executions should start even if the previous one is still running (default: `False`)
|
|
60
|
+
- `exclusive`: A boolean indicating if the scheduled action should be executed in only one instance of the application (requires a distributed backend, default: `True`)
|
|
61
|
+
- `timeout`: An integer representing the timeout for the scheduled action in seconds (default: `None`)
|
|
62
|
+
- `exception_handler`: A callable that will be called when an exception is raised during execution (default: `None`)
|
|
63
|
+
|
|
64
|
+
### Cron Expressions
|
|
65
|
+
|
|
66
|
+
Jararaca uses standard cron expressions for scheduling. Here are some examples:
|
|
67
|
+
|
|
68
|
+
- `* * * * *` - Run every minute
|
|
69
|
+
- `*/15 * * * *` - Run every 15 minutes
|
|
70
|
+
- `0 * * * *` - Run at the beginning of every hour
|
|
71
|
+
- `0 0 * * *` - Run at midnight every day
|
|
72
|
+
- `0 0 * * 0` - Run at midnight every Sunday
|
|
73
|
+
- `0 0 1 * *` - Run at midnight on the first day of every month
|
|
74
|
+
|
|
75
|
+
## Basic Scheduler Implementation
|
|
76
|
+
|
|
77
|
+
The basic scheduler is suitable for simpler applications where tasks run locally:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from jararaca import Microservice, ScheduledAction
|
|
81
|
+
|
|
82
|
+
app = Microservice(
|
|
83
|
+
# Your microservice configuration
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Run the scheduler
|
|
87
|
+
from jararaca.scheduler.scheduler import Scheduler
|
|
88
|
+
scheduler = Scheduler(app, interval=1)
|
|
89
|
+
scheduler.run()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Enhanced Scheduler V2 Implementation
|
|
93
|
+
|
|
94
|
+
The V2 scheduler adds support for distributed execution and message broker integration, making it ideal for more complex applications:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from jararaca import Microservice, ScheduledAction
|
|
98
|
+
from jararaca.scheduler.scheduler_v2 import SchedulerV2
|
|
99
|
+
|
|
100
|
+
app = Microservice(
|
|
101
|
+
# Your microservice configuration
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Run the enhanced scheduler
|
|
105
|
+
scheduler = SchedulerV2(
|
|
106
|
+
app=app,
|
|
107
|
+
interval=1,
|
|
108
|
+
broker_url="amqp://guest:guest@localhost:5672/?exchange=jararaca_ex",
|
|
109
|
+
backend_url="redis://localhost:6379",
|
|
110
|
+
)
|
|
111
|
+
scheduler.run()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Message Broker Integration
|
|
115
|
+
|
|
116
|
+
The V2 scheduler uses a message broker (currently supporting RabbitMQ) to distribute tasks:
|
|
117
|
+
|
|
118
|
+
1. The scheduler determines when a task should run based on its cron expression
|
|
119
|
+
2. Instead of executing the task directly, it sends a message to the message broker
|
|
120
|
+
3. A worker picks up the message and executes the task
|
|
121
|
+
4. The backend store (Redis) tracks execution state to prevent overlap when configured
|
|
122
|
+
|
|
123
|
+
This architecture allows for better scalability and reliability:
|
|
124
|
+
|
|
125
|
+
```mermaid
|
|
126
|
+
sequenceDiagram
|
|
127
|
+
participant S as SchedulerV2
|
|
128
|
+
participant B as Message Broker
|
|
129
|
+
participant R as Redis Backend
|
|
130
|
+
participant W as Worker
|
|
131
|
+
|
|
132
|
+
S->>R: Check last execution time
|
|
133
|
+
R-->>S: Return last execution time
|
|
134
|
+
S->>S: Determine if task should run
|
|
135
|
+
S->>B: Publish task message
|
|
136
|
+
S->>R: Update last execution time
|
|
137
|
+
B-->>W: Deliver task message
|
|
138
|
+
W->>R: Mark task as running
|
|
139
|
+
W->>W: Execute task
|
|
140
|
+
W->>R: Mark task as completed
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Delayed Message Queue
|
|
144
|
+
|
|
145
|
+
The V2 scheduler also supports delayed messages:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from jararaca import use_publisher
|
|
149
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
150
|
+
|
|
151
|
+
# Schedule a message to be published at a future time
|
|
152
|
+
async def schedule_reminder():
|
|
153
|
+
message = ReminderMessage(
|
|
154
|
+
user_id="123",
|
|
155
|
+
message="Don't forget your appointment!"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Current time + 1 hour in seconds
|
|
159
|
+
dispatch_time = int(time.time()) + 3600
|
|
160
|
+
|
|
161
|
+
# Get publisher
|
|
162
|
+
publisher = use_publisher()
|
|
163
|
+
|
|
164
|
+
# Schedule delayed message
|
|
165
|
+
await publisher.publish_delayed(
|
|
166
|
+
message,
|
|
167
|
+
dispatch_time=dispatch_time
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Redis Backend Implementation
|
|
172
|
+
|
|
173
|
+
The Redis backend implementation provides:
|
|
174
|
+
|
|
175
|
+
1. **Distributed Locking** - Ensures tasks only run on one instance when exclusivity is required
|
|
176
|
+
2. **Execution Tracking** - Tracks the running state of tasks to prevent overlap
|
|
177
|
+
3. **Delayed Message Queue** - Manages messages scheduled for future delivery
|
|
178
|
+
|
|
179
|
+
The implementation uses Redis data structures:
|
|
180
|
+
- Keys for last execution time and dispatch time
|
|
181
|
+
- Sorted sets for delayed message queue
|
|
182
|
+
- Hash sets for execution indicators
|
|
183
|
+
|
|
184
|
+
## Running the Scheduler
|
|
185
|
+
|
|
186
|
+
### CLI Command for Basic Scheduler
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
jararaca scheduler APP_PATH [OPTIONS]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Options:**
|
|
193
|
+
- `--interval`: Polling interval in seconds (default: 1)
|
|
194
|
+
|
|
195
|
+
### CLI Command for Enhanced Scheduler (V2)
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
jararaca scheduler_v2 APP_PATH [OPTIONS]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Options:**
|
|
202
|
+
- `--interval`: Polling interval in seconds (default: 1, required)
|
|
203
|
+
- `--broker-url`: The URL for the message broker (required)
|
|
204
|
+
- `--backend-url`: The URL for the message broker backend (required)
|
|
205
|
+
|
|
206
|
+
## Best Practices
|
|
207
|
+
|
|
208
|
+
1. **Task Duration** - Be mindful of task duration, especially for frequent tasks
|
|
209
|
+
2. **Error Handling** - Implement proper error handling in your tasks
|
|
210
|
+
3. **Overlap Control** - Use `allow_overlap=False` for resource-intensive tasks
|
|
211
|
+
4. **Timeouts** - Set appropriate timeouts to prevent stuck tasks
|
|
212
|
+
5. **Monitoring** - Log task execution for monitoring purposes
|
|
213
|
+
|
|
214
|
+
## Conclusion
|
|
215
|
+
|
|
216
|
+
The Jararaca scheduler system provides a powerful, flexible way to implement periodic tasks in your applications. With two implementations to choose from, you can select the one that best fits your application's requirements, from simple local scheduling to complex distributed task execution.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "jararaca"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "A simple and fast API framework for Python"
|
|
5
5
|
authors = ["Lucas S <me@luscasleo.dev>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -27,6 +27,7 @@ opentelemetry-exporter-otlp = { version = "^1.27.0", optional = true }
|
|
|
27
27
|
types-croniter = "^3.0.3.20240731"
|
|
28
28
|
types-redis = "^4.6.0.20240903"
|
|
29
29
|
mako = "^1.3.5"
|
|
30
|
+
watchdog = { version = "^3.0.0", optional = true }
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
[tool.poetry.extras]
|
|
@@ -39,6 +40,7 @@ opentelemetry = [
|
|
|
39
40
|
]
|
|
40
41
|
http = ["httptools", "httpx"]
|
|
41
42
|
docs = ["mkdocs-material"]
|
|
43
|
+
watch = ["watchdog"]
|
|
42
44
|
|
|
43
45
|
[tool.poetry.group.lint.dependencies]
|
|
44
46
|
mypy = "^1.11.2"
|
|
@@ -2,6 +2,7 @@ from importlib import import_module
|
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
|
+
from jararaca.broker_backend.redis_broker_backend import RedisMessageBrokerBackend
|
|
5
6
|
from jararaca.messagebus.bus_message_controller import (
|
|
6
7
|
ack,
|
|
7
8
|
nack,
|
|
@@ -60,10 +61,12 @@ if TYPE_CHECKING:
|
|
|
60
61
|
from .messagebus.interceptors.aiopika_publisher_interceptor import (
|
|
61
62
|
AIOPikaConnectionFactory,
|
|
62
63
|
GenericPoolConfig,
|
|
64
|
+
)
|
|
65
|
+
from .messagebus.interceptors.publisher_interceptor import (
|
|
63
66
|
MessageBusPublisherInterceptor,
|
|
64
67
|
)
|
|
68
|
+
from .messagebus.message import Message, MessageOf
|
|
65
69
|
from .messagebus.publisher import use_publisher
|
|
66
|
-
from .messagebus.types import Message, MessageOf
|
|
67
70
|
from .messagebus.worker import MessageBusWorker
|
|
68
71
|
from .microservice import Microservice, use_app_context, use_current_container
|
|
69
72
|
from .persistence.base import T_BASEMODEL, BaseEntity
|
|
@@ -115,6 +118,7 @@ if TYPE_CHECKING:
|
|
|
115
118
|
from .tools.app_config.interceptor import AppConfigurationInterceptor
|
|
116
119
|
|
|
117
120
|
__all__ = [
|
|
121
|
+
"RedisMessageBrokerBackend",
|
|
118
122
|
"FilterRuleApplier",
|
|
119
123
|
"SortRuleApplier",
|
|
120
124
|
"use_bus_message_controller",
|
|
@@ -216,6 +220,11 @@ if TYPE_CHECKING:
|
|
|
216
220
|
__SPEC_PARENT__: str = __spec__.parent # type: ignore
|
|
217
221
|
# A mapping of {<member name>: (package, <module name>)} defining dynamic imports
|
|
218
222
|
_dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
223
|
+
"RedisMessageBrokerBackend": (
|
|
224
|
+
__SPEC_PARENT__,
|
|
225
|
+
"broker_backend.redis_broker_backend",
|
|
226
|
+
None,
|
|
227
|
+
),
|
|
219
228
|
"FilterRuleApplier": (__SPEC_PARENT__, "persistence.sort_filter", None),
|
|
220
229
|
"SortRuleApplier": (__SPEC_PARENT__, "persistence.sort_filter", None),
|
|
221
230
|
"use_bus_message_controller": (
|
|
@@ -286,8 +295,8 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
286
295
|
),
|
|
287
296
|
"Identifiable": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
288
297
|
"IdentifiableEntity": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
289
|
-
"MessageOf": (__SPEC_PARENT__, "messagebus.
|
|
290
|
-
"Message": (__SPEC_PARENT__, "messagebus.
|
|
298
|
+
"MessageOf": (__SPEC_PARENT__, "messagebus.message", None),
|
|
299
|
+
"Message": (__SPEC_PARENT__, "messagebus.message", None),
|
|
291
300
|
"StringCriteria": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
292
301
|
"DateCriteria": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
293
302
|
"DateOrderedFilter": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
@@ -339,7 +348,7 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
339
348
|
),
|
|
340
349
|
"MessageBusPublisherInterceptor": (
|
|
341
350
|
__SPEC_PARENT__,
|
|
342
|
-
"messagebus.interceptors.
|
|
351
|
+
"messagebus.interceptors.publisher_interceptor",
|
|
343
352
|
None,
|
|
344
353
|
),
|
|
345
354
|
"RedisWebSocketConnectionBackend": (
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import AsyncContextManager, AsyncGenerator, Iterable
|
|
4
|
+
|
|
5
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MessageBrokerBackend(ABC):
|
|
9
|
+
|
|
10
|
+
def lock(self) -> AsyncContextManager[None]:
|
|
11
|
+
"""
|
|
12
|
+
Acquire a lock for the message broker backend.
|
|
13
|
+
This is used to ensure that only one instance of the scheduler is running at a time.
|
|
14
|
+
"""
|
|
15
|
+
raise NotImplementedError(f"lock() is not implemented by {self.__class__}.")
|
|
16
|
+
|
|
17
|
+
async def get_last_dispatch_time(self, action_name: str) -> int | None:
|
|
18
|
+
"""
|
|
19
|
+
Get the last dispatch time of the scheduled action.
|
|
20
|
+
This is used to determine if the scheduled action should be executed again
|
|
21
|
+
or if it should be skipped.
|
|
22
|
+
"""
|
|
23
|
+
raise NotImplementedError(
|
|
24
|
+
f"get_last_dispatch_time() is not implemented by {self.__class__}."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Set the last dispatch time of the scheduled action.
|
|
30
|
+
This is used to determine if the scheduled action should be executed again
|
|
31
|
+
or if it should be skipped.
|
|
32
|
+
"""
|
|
33
|
+
raise NotImplementedError(
|
|
34
|
+
f"set_last_dispatch_time() is not implemented by {self.__class__}."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
async def get_in_execution_count(self, action_name: str) -> int:
|
|
38
|
+
"""
|
|
39
|
+
Get the number of scheduled actions in execution.
|
|
40
|
+
This is used to determine if the scheduled action should be executed again
|
|
41
|
+
or if it should be skipped.
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError(
|
|
44
|
+
f"get_in_execution_count() is not implemented by {self.__class__}."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def in_execution(self, action_name: str) -> AsyncContextManager[None]:
|
|
48
|
+
"""
|
|
49
|
+
Acquire a lock for the scheduled action.
|
|
50
|
+
This is used to ensure that only one instance of the scheduled action is running at a time.
|
|
51
|
+
"""
|
|
52
|
+
raise NotImplementedError(
|
|
53
|
+
f"in_execution() is not implemented by {self.__class__}."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def dequeue_next_delayed_messages(
|
|
57
|
+
self, start_timestamp: int
|
|
58
|
+
) -> Iterable[DelayedMessageData]:
|
|
59
|
+
"""
|
|
60
|
+
Dequeue the next delayed messages from the message broker.
|
|
61
|
+
This is used to trigger the scheduled action.
|
|
62
|
+
"""
|
|
63
|
+
raise NotImplementedError(
|
|
64
|
+
f"dequeue_next_delayed_messages() is not implemented by {self.__class__}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def enqueue_delayed_message(
|
|
68
|
+
self, delayed_message: DelayedMessageData
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Enqueue a delayed message to the message broker.
|
|
72
|
+
This is used to trigger the scheduled action.
|
|
73
|
+
"""
|
|
74
|
+
raise NotImplementedError(
|
|
75
|
+
f"enqueue_delayed_message() is not implemented by {self.__class__}."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def dispose(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Dispose of the message broker backend.
|
|
81
|
+
This is used to clean up resources used by the message broker backend.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class NullBackend(MessageBrokerBackend):
|
|
86
|
+
"""
|
|
87
|
+
A null backend that does nothing.
|
|
88
|
+
This is used for testing purposes.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@asynccontextmanager
|
|
92
|
+
async def lock(self) -> AsyncGenerator[None, None]:
|
|
93
|
+
yield
|
|
94
|
+
|
|
95
|
+
async def get_last_dispatch_time(self, action_name: str) -> int:
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
async def dispose(self) -> None:
|
|
102
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_message_broker_backend_from_url(url: str) -> MessageBrokerBackend:
|
|
5
|
+
"""
|
|
6
|
+
Factory function to create a message broker backend instance from a URL.
|
|
7
|
+
Currently, only Redis is supported.
|
|
8
|
+
"""
|
|
9
|
+
if (
|
|
10
|
+
url.startswith("redis://")
|
|
11
|
+
or url.startswith("rediss://")
|
|
12
|
+
or url.startswith("redis-socket://")
|
|
13
|
+
or url.startswith("rediss+socket://")
|
|
14
|
+
):
|
|
15
|
+
from jararaca.broker_backend.redis_broker_backend import (
|
|
16
|
+
RedisMessageBrokerBackend,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return RedisMessageBrokerBackend(url)
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError(f"Unsupported message broker backend URL: {url}")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from typing import AsyncGenerator, Iterable
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import redis.asyncio
|
|
8
|
+
|
|
9
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
10
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RedisMessageBrokerBackend(MessageBrokerBackend):
|
|
16
|
+
def __init__(self, url: str) -> None:
|
|
17
|
+
self.redis = redis.asyncio.Redis.from_url(url)
|
|
18
|
+
self.last_dispatch_time_key = "last_dispatch_time:{action_name}"
|
|
19
|
+
self.last_execution_time_key = "last_execution_time:{action_name}"
|
|
20
|
+
self.execution_indicator_key = "in_execution:{action_name}:{timestamp}"
|
|
21
|
+
self.execution_indicator_expiration = 60 * 5
|
|
22
|
+
self.delayed_messages_key = "delayed_messages"
|
|
23
|
+
self.delayed_messages_metadata_key = "delayed_messages_metadata:{task_id}"
|
|
24
|
+
|
|
25
|
+
@asynccontextmanager
|
|
26
|
+
async def lock(self) -> AsyncGenerator[None, None]:
|
|
27
|
+
yield
|
|
28
|
+
|
|
29
|
+
async def get_last_dispatch_time(self, action_name: str) -> int | None:
|
|
30
|
+
|
|
31
|
+
key = self.last_dispatch_time_key.format(action_name=action_name)
|
|
32
|
+
last_execution_time = await self.redis.get(key)
|
|
33
|
+
if last_execution_time is None:
|
|
34
|
+
return None
|
|
35
|
+
return int(last_execution_time)
|
|
36
|
+
|
|
37
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
38
|
+
key = self.last_dispatch_time_key.format(action_name=action_name)
|
|
39
|
+
await self.redis.set(key, timestamp)
|
|
40
|
+
|
|
41
|
+
async def get_last_execution_time(self, action_name: str) -> int | None:
|
|
42
|
+
key = self.last_execution_time_key.format(action_name=action_name)
|
|
43
|
+
last_execution_time = await self.redis.get(key)
|
|
44
|
+
if last_execution_time is None:
|
|
45
|
+
return None
|
|
46
|
+
return int(last_execution_time)
|
|
47
|
+
|
|
48
|
+
async def set_last_execution_time(self, action_name: str, timestamp: int) -> None:
|
|
49
|
+
key = self.last_execution_time_key.format(action_name=action_name)
|
|
50
|
+
await self.redis.set(key, timestamp)
|
|
51
|
+
|
|
52
|
+
async def get_in_execution_count(self, action_name: str) -> int:
|
|
53
|
+
key = self.execution_indicator_key.format(
|
|
54
|
+
action_name=action_name, timestamp="*"
|
|
55
|
+
)
|
|
56
|
+
in_execution_count = await self.redis.keys(key)
|
|
57
|
+
if in_execution_count is None:
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
return len(in_execution_count)
|
|
61
|
+
|
|
62
|
+
@asynccontextmanager
|
|
63
|
+
async def in_execution(self, action_name: str) -> AsyncGenerator[None, None]:
|
|
64
|
+
"""
|
|
65
|
+
Acquire a lock for the scheduled action.
|
|
66
|
+
This is used to ensure that only one instance of the scheduled action is running at a time.
|
|
67
|
+
"""
|
|
68
|
+
key = self.execution_indicator_key.format(
|
|
69
|
+
action_name=action_name, timestamp=int(time.time())
|
|
70
|
+
)
|
|
71
|
+
await self.redis.set(key, 1, ex=self.execution_indicator_expiration)
|
|
72
|
+
try:
|
|
73
|
+
yield
|
|
74
|
+
finally:
|
|
75
|
+
await self.redis.delete(key)
|
|
76
|
+
|
|
77
|
+
async def enqueue_delayed_message(
|
|
78
|
+
self, delayed_message: DelayedMessageData
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Enqueue a delayed message to the message broker.
|
|
82
|
+
This is used to trigger the scheduled action.
|
|
83
|
+
"""
|
|
84
|
+
task_id = str(uuid4())
|
|
85
|
+
async with self.redis.pipeline() as pipe:
|
|
86
|
+
pipe.set(
|
|
87
|
+
self.delayed_messages_metadata_key.format(task_id=task_id),
|
|
88
|
+
delayed_message.model_dump_json().encode(),
|
|
89
|
+
)
|
|
90
|
+
pipe.zadd(
|
|
91
|
+
self.delayed_messages_key,
|
|
92
|
+
{task_id: delayed_message.dispatch_time},
|
|
93
|
+
nx=True,
|
|
94
|
+
)
|
|
95
|
+
await pipe.execute()
|
|
96
|
+
|
|
97
|
+
async def dequeue_next_delayed_messages(
|
|
98
|
+
self, start_timestamp: int
|
|
99
|
+
) -> Iterable[DelayedMessageData]:
|
|
100
|
+
"""
|
|
101
|
+
Dequeue the next delayed messages from the message broker.
|
|
102
|
+
This is used to trigger the scheduled action.
|
|
103
|
+
"""
|
|
104
|
+
tasks_ids = await self.redis.zrangebyscore(
|
|
105
|
+
name=self.delayed_messages_key,
|
|
106
|
+
max=start_timestamp,
|
|
107
|
+
min="-inf",
|
|
108
|
+
withscores=False,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not tasks_ids:
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
tasks_bytes_data: list[bytes] = []
|
|
115
|
+
|
|
116
|
+
for task_id_bytes in tasks_ids:
|
|
117
|
+
metadata = await self.redis.get(
|
|
118
|
+
self.delayed_messages_metadata_key.format(
|
|
119
|
+
task_id=task_id_bytes.decode()
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
if metadata is None:
|
|
123
|
+
logger.warning(
|
|
124
|
+
f"Delayed message metadata not found for task_id: {task_id_bytes.decode()}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
tasks_bytes_data.append(metadata)
|
|
130
|
+
|
|
131
|
+
async with self.redis.pipeline() as pipe:
|
|
132
|
+
for task_id_bytes in tasks_ids:
|
|
133
|
+
pipe.zrem(self.delayed_messages_key, task_id_bytes.decode())
|
|
134
|
+
pipe.delete(
|
|
135
|
+
self.delayed_messages_metadata_key.format(
|
|
136
|
+
task_id=task_id_bytes.decode()
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
await pipe.execute()
|
|
140
|
+
|
|
141
|
+
delayed_messages: list[DelayedMessageData] = []
|
|
142
|
+
|
|
143
|
+
for task_bytes_data in tasks_bytes_data:
|
|
144
|
+
try:
|
|
145
|
+
delayed_message = DelayedMessageData.model_validate_json(
|
|
146
|
+
task_bytes_data.decode()
|
|
147
|
+
)
|
|
148
|
+
delayed_messages.append(delayed_message)
|
|
149
|
+
except Exception:
|
|
150
|
+
logger.error(
|
|
151
|
+
f"Error parsing delayed message: {task_bytes_data.decode()}"
|
|
152
|
+
)
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
return delayed_messages
|
|
156
|
+
|
|
157
|
+
async def dispose(self) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Dispose of the message broker backend.
|
|
160
|
+
This is used to close the connection to the message broker.
|
|
161
|
+
"""
|
|
162
|
+
await self.redis.close()
|