beanqueue 1.1.5__tar.gz → 1.1.7__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.
- beanqueue-1.1.7/.gitignore +161 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/PKG-INFO +40 -47
- {beanqueue-1.1.5 → beanqueue-1.1.7}/README.md +28 -29
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/app.py +17 -4
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/config.py +14 -3
- beanqueue-1.1.7/pyproject.toml +35 -0
- beanqueue-1.1.5/pyproject.toml +0 -32
- {beanqueue-1.1.5 → beanqueue-1.1.7}/LICENSE +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/__init__.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/__init__.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/cli.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/create_tables.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/environment.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/main.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/process.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/submit.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/utils.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/constants.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/db/__init__.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/db/base.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/db/session.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/events.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/__init__.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/event.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/helpers.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/task.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/worker.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/__init__.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/processor.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/registry.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/retry_policies.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/services/__init__.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/services/dispatch.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/services/worker.py +0 -0
- {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/utils.py +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
env/
|
|
126
|
+
venv/
|
|
127
|
+
ENV/
|
|
128
|
+
env.bak/
|
|
129
|
+
venv.bak/
|
|
130
|
+
|
|
131
|
+
# Spyder project settings
|
|
132
|
+
.spyderproject
|
|
133
|
+
.spyproject
|
|
134
|
+
|
|
135
|
+
# Rope project settings
|
|
136
|
+
.ropeproject
|
|
137
|
+
|
|
138
|
+
# mkdocs documentation
|
|
139
|
+
/site
|
|
140
|
+
|
|
141
|
+
# mypy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Pyre type checker
|
|
147
|
+
.pyre/
|
|
148
|
+
|
|
149
|
+
# pytype static type analyzer
|
|
150
|
+
.pytype/
|
|
151
|
+
|
|
152
|
+
# Cython debug symbols
|
|
153
|
+
cython_debug/
|
|
154
|
+
|
|
155
|
+
# PyCharm
|
|
156
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
157
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
158
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
159
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
160
|
+
#.idea/
|
|
161
|
+
.idea
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: beanqueue
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.7
|
|
4
4
|
Summary: BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Requires-Python:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist: pg-activity (>=3.5.1,<4.0.0)
|
|
16
|
-
Requires-Dist: pydantic-settings (>=2.2.1,<3.0.0)
|
|
17
|
-
Requires-Dist: rich (>=13.7.1,<14.0.0)
|
|
18
|
-
Requires-Dist: sqlalchemy (>=2.0.30,<3.0.0)
|
|
19
|
-
Requires-Dist: venusian (>=3.1.0,<4.0.0)
|
|
5
|
+
Author-email: Fang-Pen Lin <fangpen@launchplatform.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: ~=3.11
|
|
9
|
+
Requires-Dist: blinker<2,>=1.8.2
|
|
10
|
+
Requires-Dist: click<9,>=8.1.7
|
|
11
|
+
Requires-Dist: pydantic-settings<3,>=2.2.1
|
|
12
|
+
Requires-Dist: rich<14,>=13.7.1
|
|
13
|
+
Requires-Dist: sqlalchemy<3,>=2.0.30
|
|
14
|
+
Requires-Dist: venusian<4,>=3.1.0
|
|
20
15
|
Description-Content-Type: text/markdown
|
|
21
16
|
|
|
22
|
-
# BeanQueue
|
|
17
|
+
# BeanQueue [](https://dl.circleci.com/status-badge/redirect/gh/LaunchPlatform/beanhub-extract/tree/master)
|
|
18
|
+
|
|
23
19
|
BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https://www.sqlalchemy.org/), PostgreSQL [SKIP LOCKED queries](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/) and [NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) / [LISTEN](https://www.postgresql.org/docs/current/sql-listen.html) statements.
|
|
24
20
|
|
|
25
21
|
**Notice**: Still in its early stage, we built this for [BeanHub](https://beanhub.io)'s internal usage. May change rapidly. Use at your own risk for now.
|
|
@@ -27,14 +23,14 @@ BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https
|
|
|
27
23
|
## Features
|
|
28
24
|
|
|
29
25
|
- **Super lightweight**: Under 1K lines
|
|
30
|
-
- **Easy-to-deploy**: Only
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
26
|
+
- **Easy-to-deploy**: Only relies on PostgreSQL
|
|
27
|
+
- **Transactional**: Commit your tasks with other database entries altogether without worrying about data inconsistencies
|
|
28
|
+
- **Easy-to-use**: Built-in command line tools for processing tasks and helpers for generating task models
|
|
29
|
+
- **Auto-notify**: Automatic generation of NOTIFY statements for new or updated tasks, ensuring fast task processing
|
|
30
|
+
- **Retry**: Built-in and customizable retry policies
|
|
31
|
+
- **Schedule**: Schedule tasks to run later
|
|
35
32
|
- **Worker heartbeat and auto-reschedule**: Each worker keeps updating heartbeat, if one is found dead, the others will reschedule the tasks
|
|
36
|
-
- **Customizable**: Use it as
|
|
37
|
-
- **Native DB operations**: Commit your tasks with other db entries altogether without worrying about data inconsistent issue
|
|
33
|
+
- **Customizable**: Custom Task, Worker and Event models. Use it as a library and build your own work queue
|
|
38
34
|
|
|
39
35
|
## Install
|
|
40
36
|
|
|
@@ -44,7 +40,7 @@ pip install beanqueue
|
|
|
44
40
|
|
|
45
41
|
## Usage
|
|
46
42
|
|
|
47
|
-
You can define a task processor like this
|
|
43
|
+
You can define a basic task processor like this
|
|
48
44
|
|
|
49
45
|
```python
|
|
50
46
|
from sqlalchemy.orm import Session
|
|
@@ -164,20 +160,20 @@ delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
|
|
|
164
160
|
|
|
165
161
|
@app.processor(channel="images", retry_policy=delay_retry)
|
|
166
162
|
def resize_image(db: Session, task: bq.Task, width: int, height: int):
|
|
167
|
-
# resize
|
|
163
|
+
# resize image here ...
|
|
168
164
|
pass
|
|
169
165
|
```
|
|
170
166
|
|
|
171
167
|
Currently, we provide some simple common retry policies such as `DelayRetry` and `ExponentialBackoffRetry`.
|
|
172
|
-
|
|
168
|
+
You can define your retry policy easily by making a function that returns an optional object at the next scheduled time for retry.
|
|
173
169
|
|
|
174
170
|
```python
|
|
175
171
|
def my_retry_policy(task: bq.Task) -> typing.Any:
|
|
176
|
-
#
|
|
172
|
+
# Calculate delay based on task model ...
|
|
177
173
|
return func.now() + datetime.timedelta(seconds=delay)
|
|
178
174
|
```
|
|
179
175
|
|
|
180
|
-
To cap how many attempts
|
|
176
|
+
To cap how many attempts are allowed, you can also use `LimitAttempt` like this:
|
|
181
177
|
|
|
182
178
|
```python
|
|
183
179
|
delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
|
|
@@ -185,7 +181,7 @@ capped_delay_retry = bq.LimitAttempt(3, delay_retry)
|
|
|
185
181
|
|
|
186
182
|
@app.processor(channel="images", retry_policy=capped_delay_retry)
|
|
187
183
|
def resize_image(db: Session, task: bq.Task, width: int, height: int):
|
|
188
|
-
#
|
|
184
|
+
# Resize image here ...
|
|
189
185
|
pass
|
|
190
186
|
```
|
|
191
187
|
|
|
@@ -198,7 +194,7 @@ You can also retry only for specific exception classes with the `retry_exception
|
|
|
198
194
|
retry_exceptions=ValueError,
|
|
199
195
|
)
|
|
200
196
|
def resize_image(db: Session, task: bq.Task, width: int, height: int):
|
|
201
|
-
# resize
|
|
197
|
+
# resize image here ...
|
|
202
198
|
pass
|
|
203
199
|
```
|
|
204
200
|
|
|
@@ -216,8 +212,6 @@ For example:
|
|
|
216
212
|
import bq
|
|
217
213
|
from .my_config import config
|
|
218
214
|
|
|
219
|
-
container = bq.Container()
|
|
220
|
-
container.wire(packages=[bq])
|
|
221
215
|
config = bq.Config(
|
|
222
216
|
PROCESSOR_PACKAGES=["my_pkgs.processors"],
|
|
223
217
|
DATABASE_URL=config.DATABASE_URL,
|
|
@@ -241,9 +235,9 @@ app.process_tasks(channels=("images",))
|
|
|
241
235
|
### Define your own tables
|
|
242
236
|
|
|
243
237
|
BeanQueue is designed to be as customizable as much as possible.
|
|
244
|
-
|
|
238
|
+
One of its key features is that you can define your own SQLAlchemy model instead of using the ones we provided.
|
|
245
239
|
|
|
246
|
-
To make defining your own `Task`, `Worker` or `Event` model much easier,
|
|
240
|
+
To make defining your own `Task`, `Worker` or `Event` model much easier, use bq's mixin classes:
|
|
247
241
|
|
|
248
242
|
- `bq.TaskModelMixin`: provides task model columns
|
|
249
243
|
- `bq.TaskModelRefWorkerMixin`: provides foreign key column and relationship to `bq.Worker`
|
|
@@ -259,12 +253,13 @@ Here's an example for defining your own Task model:
|
|
|
259
253
|
```python
|
|
260
254
|
import uuid
|
|
261
255
|
|
|
262
|
-
import bq
|
|
263
256
|
from sqlalchemy import ForeignKey
|
|
264
257
|
from sqlalchemy.dialects.postgresql import UUID
|
|
265
258
|
from sqlalchemy.orm import Mapped
|
|
266
259
|
from sqlalchemy.orm import mapped_column
|
|
267
260
|
from sqlalchemy.orm import relationship
|
|
261
|
+
import bq
|
|
262
|
+
from bq.models.task import listen_events
|
|
268
263
|
|
|
269
264
|
from .base_class import Base
|
|
270
265
|
|
|
@@ -281,15 +276,14 @@ class Task(bq.TaskModelMixin, Base):
|
|
|
281
276
|
worker: Mapped["Worker"] = relationship(
|
|
282
277
|
"Worker", back_populates="tasks", uselist=False
|
|
283
278
|
)
|
|
284
|
-
```
|
|
285
279
|
|
|
286
|
-
To make task insert and update with state changing to `PENDING` send out NOTIFY "channel" statement automatically, you can also use `bq.models.task.listen_events` helper to register our SQLAlchemy event handlers automatically like this
|
|
287
|
-
|
|
288
|
-
```python
|
|
289
|
-
from bq.models.task import listen_events
|
|
290
280
|
listen_events(Task)
|
|
291
281
|
```
|
|
292
282
|
|
|
283
|
+
For task insertion and updates to notify workers, we need to register any custom task types with `bq.models.task.listen_events`.
|
|
284
|
+
In the example above, this is done right after the Task model definition.
|
|
285
|
+
For more details and advanced usage, see the definition of `bq.models.task.listen_events`.
|
|
286
|
+
|
|
293
287
|
You just see how easy it is to define your Task model. Now, here's an example for defining your own Worker model:
|
|
294
288
|
|
|
295
289
|
```python
|
|
@@ -362,7 +356,7 @@ With this approach, we don't need to worry about workers picking up the task too
|
|
|
362
356
|
However, there's another drawback.
|
|
363
357
|
If step 3 for pushing a new task to the work queue fails, the newly inserted `images` row will never be processed.
|
|
364
358
|
There are many solutions to this problem, but these are all caused by inconsistent data views between the database and the work queue storage.
|
|
365
|
-
Things
|
|
359
|
+
Things would be much easier if we had a work queue that shared the same consistent view as the database.
|
|
366
360
|
|
|
367
361
|
By using a database as the data storage, all the problems are gone.
|
|
368
362
|
You can simply do the following:
|
|
@@ -383,10 +377,10 @@ However, things have changed since the [introduction of the SKIP LOCKED](https:/
|
|
|
383
377
|
This project is inspired by many of the SKIP-LOCKED-based work queue successors.
|
|
384
378
|
Why don't we just use those existing tools?
|
|
385
379
|
Well, because while they work great as work queue solutions, they don't take advantage of writing tasks and their relative data into the database in a transaction.
|
|
386
|
-
Many provide an abstraction function or gRPC method
|
|
380
|
+
Many provide an abstraction function or gRPC method for pushing tasks into the database, rather than allowing users to directly insert rows and commit them together.
|
|
387
381
|
|
|
388
|
-
|
|
389
|
-
Instead,
|
|
382
|
+
BeanQueue doesn't overly abstract the logic of publishing a new task into the queue.
|
|
383
|
+
Instead, you insert rows directly, choosing when and what to commit as tasks.
|
|
390
384
|
|
|
391
385
|
## Sponsor
|
|
392
386
|
|
|
@@ -406,4 +400,3 @@ A modern accounting book service based on the most popular open source version c
|
|
|
406
400
|
- [PgQueuer](https://github.com/janbjorge/PgQueuer)
|
|
407
401
|
- [hatchet](https://github.com/hatchet-dev/hatchet)
|
|
408
402
|
- [procrastinate](https://github.com/procrastinate-org/procrastinate)
|
|
409
|
-
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# BeanQueue
|
|
1
|
+
# BeanQueue [](https://dl.circleci.com/status-badge/redirect/gh/LaunchPlatform/beanhub-extract/tree/master)
|
|
2
|
+
|
|
2
3
|
BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https://www.sqlalchemy.org/), PostgreSQL [SKIP LOCKED queries](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/) and [NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) / [LISTEN](https://www.postgresql.org/docs/current/sql-listen.html) statements.
|
|
3
4
|
|
|
4
5
|
**Notice**: Still in its early stage, we built this for [BeanHub](https://beanhub.io)'s internal usage. May change rapidly. Use at your own risk for now.
|
|
@@ -6,14 +7,14 @@ BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https
|
|
|
6
7
|
## Features
|
|
7
8
|
|
|
8
9
|
- **Super lightweight**: Under 1K lines
|
|
9
|
-
- **Easy-to-deploy**: Only
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
10
|
+
- **Easy-to-deploy**: Only relies on PostgreSQL
|
|
11
|
+
- **Transactional**: Commit your tasks with other database entries altogether without worrying about data inconsistencies
|
|
12
|
+
- **Easy-to-use**: Built-in command line tools for processing tasks and helpers for generating task models
|
|
13
|
+
- **Auto-notify**: Automatic generation of NOTIFY statements for new or updated tasks, ensuring fast task processing
|
|
14
|
+
- **Retry**: Built-in and customizable retry policies
|
|
15
|
+
- **Schedule**: Schedule tasks to run later
|
|
14
16
|
- **Worker heartbeat and auto-reschedule**: Each worker keeps updating heartbeat, if one is found dead, the others will reschedule the tasks
|
|
15
|
-
- **Customizable**: Use it as
|
|
16
|
-
- **Native DB operations**: Commit your tasks with other db entries altogether without worrying about data inconsistent issue
|
|
17
|
+
- **Customizable**: Custom Task, Worker and Event models. Use it as a library and build your own work queue
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
@@ -23,7 +24,7 @@ pip install beanqueue
|
|
|
23
24
|
|
|
24
25
|
## Usage
|
|
25
26
|
|
|
26
|
-
You can define a task processor like this
|
|
27
|
+
You can define a basic task processor like this
|
|
27
28
|
|
|
28
29
|
```python
|
|
29
30
|
from sqlalchemy.orm import Session
|
|
@@ -143,20 +144,20 @@ delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
|
|
|
143
144
|
|
|
144
145
|
@app.processor(channel="images", retry_policy=delay_retry)
|
|
145
146
|
def resize_image(db: Session, task: bq.Task, width: int, height: int):
|
|
146
|
-
# resize
|
|
147
|
+
# resize image here ...
|
|
147
148
|
pass
|
|
148
149
|
```
|
|
149
150
|
|
|
150
151
|
Currently, we provide some simple common retry policies such as `DelayRetry` and `ExponentialBackoffRetry`.
|
|
151
|
-
|
|
152
|
+
You can define your retry policy easily by making a function that returns an optional object at the next scheduled time for retry.
|
|
152
153
|
|
|
153
154
|
```python
|
|
154
155
|
def my_retry_policy(task: bq.Task) -> typing.Any:
|
|
155
|
-
#
|
|
156
|
+
# Calculate delay based on task model ...
|
|
156
157
|
return func.now() + datetime.timedelta(seconds=delay)
|
|
157
158
|
```
|
|
158
159
|
|
|
159
|
-
To cap how many attempts
|
|
160
|
+
To cap how many attempts are allowed, you can also use `LimitAttempt` like this:
|
|
160
161
|
|
|
161
162
|
```python
|
|
162
163
|
delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
|
|
@@ -164,7 +165,7 @@ capped_delay_retry = bq.LimitAttempt(3, delay_retry)
|
|
|
164
165
|
|
|
165
166
|
@app.processor(channel="images", retry_policy=capped_delay_retry)
|
|
166
167
|
def resize_image(db: Session, task: bq.Task, width: int, height: int):
|
|
167
|
-
#
|
|
168
|
+
# Resize image here ...
|
|
168
169
|
pass
|
|
169
170
|
```
|
|
170
171
|
|
|
@@ -177,7 +178,7 @@ You can also retry only for specific exception classes with the `retry_exception
|
|
|
177
178
|
retry_exceptions=ValueError,
|
|
178
179
|
)
|
|
179
180
|
def resize_image(db: Session, task: bq.Task, width: int, height: int):
|
|
180
|
-
# resize
|
|
181
|
+
# resize image here ...
|
|
181
182
|
pass
|
|
182
183
|
```
|
|
183
184
|
|
|
@@ -195,8 +196,6 @@ For example:
|
|
|
195
196
|
import bq
|
|
196
197
|
from .my_config import config
|
|
197
198
|
|
|
198
|
-
container = bq.Container()
|
|
199
|
-
container.wire(packages=[bq])
|
|
200
199
|
config = bq.Config(
|
|
201
200
|
PROCESSOR_PACKAGES=["my_pkgs.processors"],
|
|
202
201
|
DATABASE_URL=config.DATABASE_URL,
|
|
@@ -220,9 +219,9 @@ app.process_tasks(channels=("images",))
|
|
|
220
219
|
### Define your own tables
|
|
221
220
|
|
|
222
221
|
BeanQueue is designed to be as customizable as much as possible.
|
|
223
|
-
|
|
222
|
+
One of its key features is that you can define your own SQLAlchemy model instead of using the ones we provided.
|
|
224
223
|
|
|
225
|
-
To make defining your own `Task`, `Worker` or `Event` model much easier,
|
|
224
|
+
To make defining your own `Task`, `Worker` or `Event` model much easier, use bq's mixin classes:
|
|
226
225
|
|
|
227
226
|
- `bq.TaskModelMixin`: provides task model columns
|
|
228
227
|
- `bq.TaskModelRefWorkerMixin`: provides foreign key column and relationship to `bq.Worker`
|
|
@@ -238,12 +237,13 @@ Here's an example for defining your own Task model:
|
|
|
238
237
|
```python
|
|
239
238
|
import uuid
|
|
240
239
|
|
|
241
|
-
import bq
|
|
242
240
|
from sqlalchemy import ForeignKey
|
|
243
241
|
from sqlalchemy.dialects.postgresql import UUID
|
|
244
242
|
from sqlalchemy.orm import Mapped
|
|
245
243
|
from sqlalchemy.orm import mapped_column
|
|
246
244
|
from sqlalchemy.orm import relationship
|
|
245
|
+
import bq
|
|
246
|
+
from bq.models.task import listen_events
|
|
247
247
|
|
|
248
248
|
from .base_class import Base
|
|
249
249
|
|
|
@@ -260,15 +260,14 @@ class Task(bq.TaskModelMixin, Base):
|
|
|
260
260
|
worker: Mapped["Worker"] = relationship(
|
|
261
261
|
"Worker", back_populates="tasks", uselist=False
|
|
262
262
|
)
|
|
263
|
-
```
|
|
264
263
|
|
|
265
|
-
To make task insert and update with state changing to `PENDING` send out NOTIFY "channel" statement automatically, you can also use `bq.models.task.listen_events` helper to register our SQLAlchemy event handlers automatically like this
|
|
266
|
-
|
|
267
|
-
```python
|
|
268
|
-
from bq.models.task import listen_events
|
|
269
264
|
listen_events(Task)
|
|
270
265
|
```
|
|
271
266
|
|
|
267
|
+
For task insertion and updates to notify workers, we need to register any custom task types with `bq.models.task.listen_events`.
|
|
268
|
+
In the example above, this is done right after the Task model definition.
|
|
269
|
+
For more details and advanced usage, see the definition of `bq.models.task.listen_events`.
|
|
270
|
+
|
|
272
271
|
You just see how easy it is to define your Task model. Now, here's an example for defining your own Worker model:
|
|
273
272
|
|
|
274
273
|
```python
|
|
@@ -341,7 +340,7 @@ With this approach, we don't need to worry about workers picking up the task too
|
|
|
341
340
|
However, there's another drawback.
|
|
342
341
|
If step 3 for pushing a new task to the work queue fails, the newly inserted `images` row will never be processed.
|
|
343
342
|
There are many solutions to this problem, but these are all caused by inconsistent data views between the database and the work queue storage.
|
|
344
|
-
Things
|
|
343
|
+
Things would be much easier if we had a work queue that shared the same consistent view as the database.
|
|
345
344
|
|
|
346
345
|
By using a database as the data storage, all the problems are gone.
|
|
347
346
|
You can simply do the following:
|
|
@@ -362,10 +361,10 @@ However, things have changed since the [introduction of the SKIP LOCKED](https:/
|
|
|
362
361
|
This project is inspired by many of the SKIP-LOCKED-based work queue successors.
|
|
363
362
|
Why don't we just use those existing tools?
|
|
364
363
|
Well, because while they work great as work queue solutions, they don't take advantage of writing tasks and their relative data into the database in a transaction.
|
|
365
|
-
Many provide an abstraction function or gRPC method
|
|
364
|
+
Many provide an abstraction function or gRPC method for pushing tasks into the database, rather than allowing users to directly insert rows and commit them together.
|
|
366
365
|
|
|
367
|
-
|
|
368
|
-
Instead,
|
|
366
|
+
BeanQueue doesn't overly abstract the logic of publishing a new task into the queue.
|
|
367
|
+
Instead, you insert rows directly, choosing when and what to commit as tasks.
|
|
369
368
|
|
|
370
369
|
## Sponsor
|
|
371
370
|
|
|
@@ -5,7 +5,6 @@ import logging
|
|
|
5
5
|
import platform
|
|
6
6
|
import sys
|
|
7
7
|
import threading
|
|
8
|
-
import time
|
|
9
8
|
import typing
|
|
10
9
|
from importlib.metadata import PackageNotFoundError
|
|
11
10
|
from importlib.metadata import version
|
|
@@ -63,6 +62,9 @@ class BeanQueue:
|
|
|
63
62
|
self.worker_service_cls = worker_service_cls
|
|
64
63
|
self.dispatch_service_cls = dispatch_service_cls
|
|
65
64
|
self._engine = engine
|
|
65
|
+
self._worker_update_shutdown_event: threading.Event = threading.Event()
|
|
66
|
+
# noop if metrics thread is not started yet, shutdown if it is started
|
|
67
|
+
self._metrics_server_shutdown: typing.Callable[[], None] = lambda: None
|
|
66
68
|
|
|
67
69
|
def create_default_engine(self):
|
|
68
70
|
return create_engine(
|
|
@@ -184,7 +186,12 @@ class BeanQueue:
|
|
|
184
186
|
)
|
|
185
187
|
sys.exit(0)
|
|
186
188
|
|
|
187
|
-
|
|
189
|
+
do_shutdown = self._worker_update_shutdown_event.wait(
|
|
190
|
+
self.config.WORKER_HEARTBEAT_PERIOD
|
|
191
|
+
)
|
|
192
|
+
if do_shutdown:
|
|
193
|
+
return
|
|
194
|
+
|
|
188
195
|
current_worker.last_heartbeat = func.now()
|
|
189
196
|
db.add(current_worker)
|
|
190
197
|
db.commit()
|
|
@@ -244,6 +251,8 @@ class BeanQueue:
|
|
|
244
251
|
functools.partial(self._serve_http_request, worker_id),
|
|
245
252
|
handler_class=WSGIRequestHandlerWithLogger,
|
|
246
253
|
) as httpd:
|
|
254
|
+
# expose graceful shutdown to the main thread
|
|
255
|
+
self._metrics_server_shutdown = httpd.shutdown
|
|
247
256
|
logger.info("Run metrics HTTP server on %s:%s", host, port)
|
|
248
257
|
httpd.serve_forever()
|
|
249
258
|
|
|
@@ -307,7 +316,7 @@ class BeanQueue:
|
|
|
307
316
|
events.worker_init.send(self, worker=worker)
|
|
308
317
|
|
|
309
318
|
logger.info("Processing tasks in channels = %s ...", channels)
|
|
310
|
-
|
|
319
|
+
# Graceful shutdown of worker update event on exit of the worker
|
|
311
320
|
worker_update_thread = threading.Thread(
|
|
312
321
|
target=functools.partial(
|
|
313
322
|
self.update_workers,
|
|
@@ -357,9 +366,13 @@ class BeanQueue:
|
|
|
357
366
|
except (SystemExit, KeyboardInterrupt):
|
|
358
367
|
db.rollback()
|
|
359
368
|
logger.info("Shutting down ...")
|
|
369
|
+
self._worker_update_shutdown_event.set()
|
|
360
370
|
worker_update_thread.join(5)
|
|
361
371
|
if metrics_server_thread is not None:
|
|
362
|
-
|
|
372
|
+
# set a threading event, waits until server is shutdown
|
|
373
|
+
# serve the ongoing requests
|
|
374
|
+
self._metrics_server_shutdown()
|
|
375
|
+
metrics_server_thread.join(1)
|
|
363
376
|
|
|
364
377
|
worker.state = models.WorkerState.SHUTDOWN
|
|
365
378
|
db.add(worker)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
+
from pydantic import Field
|
|
3
4
|
from pydantic import field_validator
|
|
4
5
|
from pydantic import PostgresDsn
|
|
5
6
|
from pydantic import ValidationInfo
|
|
@@ -10,7 +11,7 @@ from pydantic_settings import SettingsConfigDict
|
|
|
10
11
|
|
|
11
12
|
class Config(BaseSettings):
|
|
12
13
|
# Packages to scan for processor functions
|
|
13
|
-
PROCESSOR_PACKAGES: list[str] =
|
|
14
|
+
PROCESSOR_PACKAGES: list[str] = Field(default_factory=list)
|
|
14
15
|
|
|
15
16
|
# Size of tasks batch to fetch each time from the database
|
|
16
17
|
BATCH_SIZE: int = 1
|
|
@@ -58,8 +59,18 @@ class Config(BaseSettings):
|
|
|
58
59
|
) -> typing.Any:
|
|
59
60
|
if isinstance(v, str):
|
|
60
61
|
return v
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
# Notice: Older Pydantic version (2.7), PostgresDsn is an annotated MultiHostUrl object,
|
|
63
|
+
# we cannot use isinstance with PostgresDsn directly. We need to check and see if PostgresDsn
|
|
64
|
+
# is an annotated type or not before we decide how to check if the passed in object is an
|
|
65
|
+
# PostgresDsn or not.
|
|
66
|
+
if typing.get_origin(PostgresDsn) is typing.Annotated:
|
|
67
|
+
if isinstance(v, MultiHostUrl):
|
|
68
|
+
return v
|
|
69
|
+
else:
|
|
70
|
+
if isinstance(v, PostgresDsn):
|
|
71
|
+
return v
|
|
72
|
+
if v is not None:
|
|
73
|
+
raise ValueError("Unexpected DATABASE_URL type")
|
|
63
74
|
return PostgresDsn.build(
|
|
64
75
|
scheme="postgresql",
|
|
65
76
|
username=info.data.get("POSTGRES_USER"),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "beanqueue"
|
|
3
|
+
version = "1.1.7"
|
|
4
|
+
description = "BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library"
|
|
5
|
+
authors = [{ name = "Fang-Pen Lin", email = "fangpen@launchplatform.com" }]
|
|
6
|
+
requires-python = "~=3.11"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"sqlalchemy>=2.0.30,<3",
|
|
11
|
+
"venusian>=3.1.0,<4",
|
|
12
|
+
"click>=8.1.7,<9",
|
|
13
|
+
"pydantic-settings>=2.2.1,<3",
|
|
14
|
+
"blinker>=1.8.2,<2",
|
|
15
|
+
"rich>=13.7.1,<14",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
bq = "bq.cmds.main:cli"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"psycopg2-binary>=2.9.9,<3",
|
|
24
|
+
"pytest-factoryboy>=2.7.0,<3",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.sdist]
|
|
28
|
+
include = ["bq"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
include = ["bq"]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
beanqueue-1.1.5/pyproject.toml
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "beanqueue"
|
|
3
|
-
version = "1.1.5"
|
|
4
|
-
description = "BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library"
|
|
5
|
-
authors = ["Fang-Pen Lin <fangpen@launchplatform.com>"]
|
|
6
|
-
license = "MIT"
|
|
7
|
-
readme = "README.md"
|
|
8
|
-
packages = [
|
|
9
|
-
{ include = "bq" },
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
[tool.poetry.scripts]
|
|
13
|
-
bq = "bq.cmds.main:cli"
|
|
14
|
-
|
|
15
|
-
[tool.poetry.dependencies]
|
|
16
|
-
python = "^3.11"
|
|
17
|
-
sqlalchemy = "^2.0.30"
|
|
18
|
-
venusian = "^3.1.0"
|
|
19
|
-
click = "^8.1.7"
|
|
20
|
-
pydantic-settings = "^2.2.1"
|
|
21
|
-
pg-activity = "^3.5.1"
|
|
22
|
-
blinker = "^1.8.2"
|
|
23
|
-
rich = "^13.7.1"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
[tool.poetry.group.dev.dependencies]
|
|
27
|
-
psycopg2-binary = "^2.9.9"
|
|
28
|
-
pytest-factoryboy = "^2.7.0"
|
|
29
|
-
|
|
30
|
-
[build-system]
|
|
31
|
-
requires = ["poetry-core"]
|
|
32
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|