queuerPy 0.18.0__tar.gz → 0.20.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.
Files changed (53) hide show
  1. {queuerpy-0.18.0/queuerPy.egg-info → queuerpy-0.20.0}/PKG-INFO +55 -12
  2. {queuerpy-0.18.0 → queuerpy-0.20.0}/README.md +54 -11
  3. {queuerpy-0.18.0 → queuerpy-0.20.0}/_version.py +1 -1
  4. {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_job.py +25 -12
  5. {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/database.py +5 -0
  6. {queuerpy-0.18.0 → queuerpy-0.20.0}/pyproject.toml +2 -2
  7. {queuerpy-0.18.0 → queuerpy-0.20.0/queuerPy.egg-info}/PKG-INFO +55 -12
  8. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_global.py +1 -1
  9. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_master.py +25 -14
  10. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_worker.py +13 -0
  11. {queuerpy-0.18.0 → queuerpy-0.20.0}/sql/job.sql +58 -7
  12. {queuerpy-0.18.0 → queuerpy-0.20.0}/LICENSE +0 -0
  13. {queuerpy-0.18.0 → queuerpy-0.20.0}/MANIFEST.in +0 -0
  14. {queuerpy-0.18.0 → queuerpy-0.20.0}/__init__.py +0 -0
  15. {queuerpy-0.18.0 → queuerpy-0.20.0}/core/__init__.py +0 -0
  16. {queuerpy-0.18.0 → queuerpy-0.20.0}/core/broadcaster.py +0 -0
  17. {queuerpy-0.18.0 → queuerpy-0.20.0}/core/listener.py +0 -0
  18. {queuerpy-0.18.0 → queuerpy-0.20.0}/core/retryer.py +0 -0
  19. {queuerpy-0.18.0 → queuerpy-0.20.0}/core/runner.py +0 -0
  20. {queuerpy-0.18.0 → queuerpy-0.20.0}/core/scheduler.py +0 -0
  21. {queuerpy-0.18.0 → queuerpy-0.20.0}/core/ticker.py +0 -0
  22. {queuerpy-0.18.0 → queuerpy-0.20.0}/database/__init__.py +0 -0
  23. {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_listener.py +0 -0
  24. {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_master.py +0 -0
  25. {queuerpy-0.18.0 → queuerpy-0.20.0}/database/db_worker.py +0 -0
  26. {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/__init__.py +0 -0
  27. {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/error.py +0 -0
  28. {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/logging.py +0 -0
  29. {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/sql.py +0 -0
  30. {queuerpy-0.18.0 → queuerpy-0.20.0}/helper/task.py +0 -0
  31. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/__init__.py +0 -0
  32. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/batch_job.py +0 -0
  33. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/connection.py +0 -0
  34. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/job.py +0 -0
  35. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/master.py +0 -0
  36. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/options.py +0 -0
  37. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/options_on_error.py +0 -0
  38. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/task.py +0 -0
  39. {queuerpy-0.18.0 → queuerpy-0.20.0}/model/worker.py +0 -0
  40. {queuerpy-0.18.0 → queuerpy-0.20.0}/py.typed +0 -0
  41. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer.py +0 -0
  42. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/SOURCES.txt +0 -0
  43. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/dependency_links.txt +0 -0
  44. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/requires.txt +0 -0
  45. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuerPy.egg-info/top_level.txt +0 -0
  46. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_job.py +0 -0
  47. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_listener.py +0 -0
  48. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_next_interval.py +0 -0
  49. {queuerpy-0.18.0 → queuerpy-0.20.0}/queuer_task.py +0 -0
  50. {queuerpy-0.18.0 → queuerpy-0.20.0}/setup.cfg +0 -0
  51. {queuerpy-0.18.0 → queuerpy-0.20.0}/sql/master.sql +0 -0
  52. {queuerpy-0.18.0 → queuerpy-0.20.0}/sql/notify.sql +0 -0
  53. {queuerpy-0.18.0 → queuerpy-0.20.0}/sql/worker.sql +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queuerPy
3
- Version: 0.18.0
3
+ Version: 0.20.0
4
4
  Summary: A Python implementation of the queuer system - a job queuing and processing system with PostgreSQL backend
5
5
  Author-email: Simon Herrmann <siherrmann@users.noreply.github.com>
6
6
  Maintainer-email: Simon Herrmann <siherrmann@users.noreply.github.com>
@@ -58,9 +58,7 @@ The job table contains only queued, scheduled and running tasks. The ended jobs
58
58
 
59
59
  ---
60
60
 
61
- # 🛠️ Installation
62
-
63
- ## PyPI Installation (Recommended)
61
+ ## 🛠️ Installation
64
62
 
65
63
  ```bash
66
64
  pip install queuerPy
@@ -134,10 +132,10 @@ You can find a full example in the example folder.
134
132
  def new_queuer(name: str, max_concurrency: int, *options: OnError) -> Queuer
135
133
 
136
134
  def new_queuer_with_db(
137
- name: str,
138
- max_concurrency: int,
139
- encryption_key: str,
140
- db_config: DatabaseConfiguration,
135
+ name: str,
136
+ max_concurrency: int,
137
+ encryption_key: str,
138
+ db_config: DatabaseConfiguration,
141
139
  *options: OnError
142
140
  ) -> Queuer
143
141
  ```
@@ -149,6 +147,7 @@ def new_queuer_with_db(
149
147
  - `options`: Optional `OnError` configurations to apply to the worker.
150
148
 
151
149
  This function performs the following setup:
150
+
152
151
  - Initializes a logger.
153
152
  - Sets up the database connection using the provided `db_config` or environment variables.
154
153
  - Creates `JobDBHandler`, `WorkerDBHandler` instances for database interactions.
@@ -167,6 +166,7 @@ def start(self) -> None
167
166
  ```
168
167
 
169
168
  Upon calling `start`:
169
+
170
170
  - It performs a basic check to ensure internal listeners are initialized.
171
171
  - Database listeners are created to listen to job events (inserts, updates, deletes) via PostgreSQL NOTIFY/LISTEN.
172
172
  - It starts a poller to periodically poll the database for new jobs to process.
@@ -185,7 +185,50 @@ The `stop` method gracefully shuts down the Queuer instance, releasing resources
185
185
  def stop(self) -> None
186
186
  ```
187
187
 
188
- The `stop` method cancels all jobs, closes database listeners, and cleans up resources.
188
+ The `stop` method cancels all jobs, closes database listeners, and cleans up resources. **Note:** This method can only be used to stop the current worker instance that the code is running in. To stop other workers, use `stop_worker` or `stop_worker_gracefully`.
189
+
190
+ ---
191
+
192
+ ## stop_worker
193
+
194
+ The `stop_worker` method immediately stops a worker by setting its status to `STOPPED`. This will cancel all running jobs on that worker.
195
+
196
+ ```python
197
+ def stop_worker(self, worker_rid: UUID) -> None
198
+ ```
199
+
200
+ - `worker_rid`: The `UUID` identifying the worker to stop.
201
+
202
+ When a worker is stopped:
203
+
204
+ - The worker status is immediately set to `STOPPED` in the database
205
+ - The heartbeat ticker detects the `STOPPED` status and calls `stop()` on that worker
206
+ - All running jobs on that worker are cancelled immediately
207
+ - The worker will no longer accept new jobs
208
+
209
+ This method is useful for immediately shutting down a worker, for example in emergency situations or when you need to take a worker offline quickly.
210
+
211
+ ---
212
+
213
+ ## stop_worker_gracefully
214
+
215
+ The `stop_worker_gracefully` method gracefully stops a worker by setting its status to `STOPPING`. This allows currently running jobs to complete before the worker shuts down.
216
+
217
+ ```python
218
+ def stop_worker_gracefully(self, worker_rid: UUID) -> None
219
+ ```
220
+
221
+ - `worker_rid`: The `UUID` identifying the worker to stop gracefully.
222
+
223
+ When a worker is stopped gracefully:
224
+
225
+ - The worker status is set to `STOPPING` in the database
226
+ - The heartbeat ticker detects the `STOPPING` status and sets `max_concurrency` to `0`
227
+ - Currently running jobs are allowed to complete normally
228
+ - No new jobs will be accepted by this worker
229
+ - Once all running jobs have finished, the worker status is automatically set to `STOPPED` and the worker shuts down
230
+
231
+ This method is ideal for maintenance scenarios where you want to ensure all in-progress work completes before shutting down the worker.
189
232
 
190
233
  ---
191
234
 
@@ -212,8 +255,8 @@ The `add_job` method adds a new job to the queue for execution. Jobs are units o
212
255
 
213
256
  ```python
214
257
  def add_job(
215
- self,
216
- task: Union[Callable, str],
258
+ self,
259
+ task: Union[Callable, str],
217
260
  *parameters: Any,
218
261
  **parameters_keyed: Any
219
262
  ) -> Job
@@ -322,7 +365,7 @@ class RetryBackoff(str, Enum):
322
365
 
323
366
  - `RETRY_BACKOFF_NONE`: No backoff. The retry_delay remains constant for all retries.
324
367
  - `RETRY_BACKOFF_LINEAR`: The retry delay increases linearly with each attempt (e.g., delay, 2*delay, 3*delay).
325
- - `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2*2).
368
+ - `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2\*2).
326
369
 
327
370
  ---
328
371
 
@@ -15,9 +15,7 @@ The job table contains only queued, scheduled and running tasks. The ended jobs
15
15
 
16
16
  ---
17
17
 
18
- # 🛠️ Installation
19
-
20
- ## PyPI Installation (Recommended)
18
+ ## 🛠️ Installation
21
19
 
22
20
  ```bash
23
21
  pip install queuerPy
@@ -91,10 +89,10 @@ You can find a full example in the example folder.
91
89
  def new_queuer(name: str, max_concurrency: int, *options: OnError) -> Queuer
92
90
 
93
91
  def new_queuer_with_db(
94
- name: str,
95
- max_concurrency: int,
96
- encryption_key: str,
97
- db_config: DatabaseConfiguration,
92
+ name: str,
93
+ max_concurrency: int,
94
+ encryption_key: str,
95
+ db_config: DatabaseConfiguration,
98
96
  *options: OnError
99
97
  ) -> Queuer
100
98
  ```
@@ -106,6 +104,7 @@ def new_queuer_with_db(
106
104
  - `options`: Optional `OnError` configurations to apply to the worker.
107
105
 
108
106
  This function performs the following setup:
107
+
109
108
  - Initializes a logger.
110
109
  - Sets up the database connection using the provided `db_config` or environment variables.
111
110
  - Creates `JobDBHandler`, `WorkerDBHandler` instances for database interactions.
@@ -124,6 +123,7 @@ def start(self) -> None
124
123
  ```
125
124
 
126
125
  Upon calling `start`:
126
+
127
127
  - It performs a basic check to ensure internal listeners are initialized.
128
128
  - Database listeners are created to listen to job events (inserts, updates, deletes) via PostgreSQL NOTIFY/LISTEN.
129
129
  - It starts a poller to periodically poll the database for new jobs to process.
@@ -142,7 +142,50 @@ The `stop` method gracefully shuts down the Queuer instance, releasing resources
142
142
  def stop(self) -> None
143
143
  ```
144
144
 
145
- The `stop` method cancels all jobs, closes database listeners, and cleans up resources.
145
+ The `stop` method cancels all jobs, closes database listeners, and cleans up resources. **Note:** This method can only be used to stop the current worker instance that the code is running in. To stop other workers, use `stop_worker` or `stop_worker_gracefully`.
146
+
147
+ ---
148
+
149
+ ## stop_worker
150
+
151
+ The `stop_worker` method immediately stops a worker by setting its status to `STOPPED`. This will cancel all running jobs on that worker.
152
+
153
+ ```python
154
+ def stop_worker(self, worker_rid: UUID) -> None
155
+ ```
156
+
157
+ - `worker_rid`: The `UUID` identifying the worker to stop.
158
+
159
+ When a worker is stopped:
160
+
161
+ - The worker status is immediately set to `STOPPED` in the database
162
+ - The heartbeat ticker detects the `STOPPED` status and calls `stop()` on that worker
163
+ - All running jobs on that worker are cancelled immediately
164
+ - The worker will no longer accept new jobs
165
+
166
+ This method is useful for immediately shutting down a worker, for example in emergency situations or when you need to take a worker offline quickly.
167
+
168
+ ---
169
+
170
+ ## stop_worker_gracefully
171
+
172
+ The `stop_worker_gracefully` method gracefully stops a worker by setting its status to `STOPPING`. This allows currently running jobs to complete before the worker shuts down.
173
+
174
+ ```python
175
+ def stop_worker_gracefully(self, worker_rid: UUID) -> None
176
+ ```
177
+
178
+ - `worker_rid`: The `UUID` identifying the worker to stop gracefully.
179
+
180
+ When a worker is stopped gracefully:
181
+
182
+ - The worker status is set to `STOPPING` in the database
183
+ - The heartbeat ticker detects the `STOPPING` status and sets `max_concurrency` to `0`
184
+ - Currently running jobs are allowed to complete normally
185
+ - No new jobs will be accepted by this worker
186
+ - Once all running jobs have finished, the worker status is automatically set to `STOPPED` and the worker shuts down
187
+
188
+ This method is ideal for maintenance scenarios where you want to ensure all in-progress work completes before shutting down the worker.
146
189
 
147
190
  ---
148
191
 
@@ -169,8 +212,8 @@ The `add_job` method adds a new job to the queue for execution. Jobs are units o
169
212
 
170
213
  ```python
171
214
  def add_job(
172
- self,
173
- task: Union[Callable, str],
215
+ self,
216
+ task: Union[Callable, str],
174
217
  *parameters: Any,
175
218
  **parameters_keyed: Any
176
219
  ) -> Job
@@ -279,7 +322,7 @@ class RetryBackoff(str, Enum):
279
322
 
280
323
  - `RETRY_BACKOFF_NONE`: No backoff. The retry_delay remains constant for all retries.
281
324
  - `RETRY_BACKOFF_LINEAR`: The retry delay increases linearly with each attempt (e.g., delay, 2*delay, 3*delay).
282
- - `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2*2).
325
+ - `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2\*2).
283
326
 
284
327
  ---
285
328
 
@@ -1,6 +1,6 @@
1
1
  """Version information for queuerPy package."""
2
2
 
3
- __version__ = "0.18.0"
3
+ __version__ = "0.20.0"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # For compatibility
@@ -9,7 +9,7 @@ from typing import List, Optional, Tuple
9
9
  from uuid import UUID
10
10
  from psycopg.rows import dict_row
11
11
 
12
- from ..helper.database import Database
12
+ from ..helper.database import Database, DatabaseConfiguration
13
13
  from ..helper.error import QueuerError
14
14
  from ..helper.sql import SQLLoader, run_ddl
15
15
  from ..model.job import Job
@@ -25,7 +25,7 @@ class JobDBHandler:
25
25
  def __init__(
26
26
  self,
27
27
  db_connection: Database,
28
- with_table_drop: bool = False,
28
+ db_config: DatabaseConfiguration,
29
29
  encryption_key: str = "",
30
30
  ):
31
31
  """Initialize job database handler."""
@@ -35,12 +35,13 @@ class JobDBHandler:
35
35
  raise ValueError("Database connection is not established")
36
36
 
37
37
  self.encryption_key: str = encryption_key
38
+ self.with_timescale: bool = db_config.with_timescale
38
39
 
39
40
  sql_loader: SQLLoader = SQLLoader()
40
- sql_loader.load_notify_sql(self.db.instance, force=with_table_drop)
41
- sql_loader.load_job_sql(self.db.instance, force=with_table_drop)
41
+ sql_loader.load_notify_sql(self.db.instance, force=db_config.with_table_drop)
42
+ sql_loader.load_job_sql(self.db.instance, force=db_config.with_table_drop)
42
43
 
43
- if with_table_drop:
44
+ if db_config.with_table_drop:
44
45
  self.drop_tables()
45
46
 
46
47
  self.create_table()
@@ -72,7 +73,7 @@ class JobDBHandler:
72
73
  if self.db.instance is None:
73
74
  raise ValueError("Database connection is not established")
74
75
 
75
- run_ddl(self.db.instance, "SELECT init_job();")
76
+ run_ddl(self.db.instance, f"SELECT init_job({self.with_timescale});")
76
77
 
77
78
  def drop_tables(self) -> None:
78
79
  """Drop job tables with DDL deadlock protection."""
@@ -379,9 +380,15 @@ class JobDBHandler:
379
380
  if self.db.instance is None:
380
381
  raise ValueError("Database connection is not established")
381
382
 
382
- with self.db.instance.cursor() as cur:
383
- cur.execute("SELECT add_retention_archive(%s);", (days,))
384
- self.db.instance.commit()
383
+ try:
384
+ with self.db.instance.cursor() as cur:
385
+ cur.execute(
386
+ "SELECT add_retention_archive(%s, %s);", (days, self.with_timescale)
387
+ )
388
+ self.db.instance.commit()
389
+ except Exception as e:
390
+ self.db.instance.rollback()
391
+ raise e
385
392
 
386
393
  def remove_retention_archive(self) -> None:
387
394
  """
@@ -391,9 +398,15 @@ class JobDBHandler:
391
398
  if self.db.instance is None:
392
399
  raise ValueError("Database connection is not established")
393
400
 
394
- with self.db.instance.cursor() as cur:
395
- cur.execute("SELECT remove_retention_archive();")
396
- self.db.instance.commit()
401
+ try:
402
+ with self.db.instance.cursor() as cur:
403
+ cur.execute(
404
+ "SELECT remove_retention_archive(%s);", (self.with_timescale,)
405
+ )
406
+ self.db.instance.commit()
407
+ except Exception as e:
408
+ self.db.instance.rollback()
409
+ raise e
397
410
 
398
411
  def select_job_from_archive(self, rid: UUID) -> Optional[Job]:
399
412
  """
@@ -30,6 +30,7 @@ class DatabaseConfiguration:
30
30
  schema: str = "public"
31
31
  sslmode: str = "require"
32
32
  with_table_drop: bool = False
33
+ with_timescale: bool = True
33
34
 
34
35
  @classmethod
35
36
  def from_env(cls) -> "DatabaseConfiguration":
@@ -45,6 +46,9 @@ class DatabaseConfiguration:
45
46
  with_table_drop = (
46
47
  os.getenv("QUEUER_DB_WITH_TABLE_DROP", "false").lower() == "true"
47
48
  )
49
+ with_timescale = (
50
+ os.getenv("QUEUER_DB_WITH_TIMESCALE", "true").lower() != "false"
51
+ )
48
52
 
49
53
  # Validate required fields
50
54
  if not all(
@@ -72,6 +76,7 @@ class DatabaseConfiguration:
72
76
  schema=schema,
73
77
  sslmode=sslmode,
74
78
  with_table_drop=with_table_drop,
79
+ with_timescale=with_timescale,
75
80
  )
76
81
 
77
82
  def connection_string(self) -> str:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "queuerPy"
7
- version = "0.18.0"
7
+ version = "0.20.0"
8
8
  description = "A Python implementation of the queuer system - a job queuing and processing system with PostgreSQL backend"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -92,7 +92,7 @@ pythonVersion = "3.8"
92
92
  pythonPlatform = "All"
93
93
 
94
94
  [tool.mypy]
95
- python_version = "0.18.0"
95
+ python_version = "0.20.0"
96
96
  warn_return_any = true
97
97
  warn_unused_configs = true
98
98
  disallow_untyped_defs = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queuerPy
3
- Version: 0.18.0
3
+ Version: 0.20.0
4
4
  Summary: A Python implementation of the queuer system - a job queuing and processing system with PostgreSQL backend
5
5
  Author-email: Simon Herrmann <siherrmann@users.noreply.github.com>
6
6
  Maintainer-email: Simon Herrmann <siherrmann@users.noreply.github.com>
@@ -58,9 +58,7 @@ The job table contains only queued, scheduled and running tasks. The ended jobs
58
58
 
59
59
  ---
60
60
 
61
- # 🛠️ Installation
62
-
63
- ## PyPI Installation (Recommended)
61
+ ## 🛠️ Installation
64
62
 
65
63
  ```bash
66
64
  pip install queuerPy
@@ -134,10 +132,10 @@ You can find a full example in the example folder.
134
132
  def new_queuer(name: str, max_concurrency: int, *options: OnError) -> Queuer
135
133
 
136
134
  def new_queuer_with_db(
137
- name: str,
138
- max_concurrency: int,
139
- encryption_key: str,
140
- db_config: DatabaseConfiguration,
135
+ name: str,
136
+ max_concurrency: int,
137
+ encryption_key: str,
138
+ db_config: DatabaseConfiguration,
141
139
  *options: OnError
142
140
  ) -> Queuer
143
141
  ```
@@ -149,6 +147,7 @@ def new_queuer_with_db(
149
147
  - `options`: Optional `OnError` configurations to apply to the worker.
150
148
 
151
149
  This function performs the following setup:
150
+
152
151
  - Initializes a logger.
153
152
  - Sets up the database connection using the provided `db_config` or environment variables.
154
153
  - Creates `JobDBHandler`, `WorkerDBHandler` instances for database interactions.
@@ -167,6 +166,7 @@ def start(self) -> None
167
166
  ```
168
167
 
169
168
  Upon calling `start`:
169
+
170
170
  - It performs a basic check to ensure internal listeners are initialized.
171
171
  - Database listeners are created to listen to job events (inserts, updates, deletes) via PostgreSQL NOTIFY/LISTEN.
172
172
  - It starts a poller to periodically poll the database for new jobs to process.
@@ -185,7 +185,50 @@ The `stop` method gracefully shuts down the Queuer instance, releasing resources
185
185
  def stop(self) -> None
186
186
  ```
187
187
 
188
- The `stop` method cancels all jobs, closes database listeners, and cleans up resources.
188
+ The `stop` method cancels all jobs, closes database listeners, and cleans up resources. **Note:** This method can only be used to stop the current worker instance that the code is running in. To stop other workers, use `stop_worker` or `stop_worker_gracefully`.
189
+
190
+ ---
191
+
192
+ ## stop_worker
193
+
194
+ The `stop_worker` method immediately stops a worker by setting its status to `STOPPED`. This will cancel all running jobs on that worker.
195
+
196
+ ```python
197
+ def stop_worker(self, worker_rid: UUID) -> None
198
+ ```
199
+
200
+ - `worker_rid`: The `UUID` identifying the worker to stop.
201
+
202
+ When a worker is stopped:
203
+
204
+ - The worker status is immediately set to `STOPPED` in the database
205
+ - The heartbeat ticker detects the `STOPPED` status and calls `stop()` on that worker
206
+ - All running jobs on that worker are cancelled immediately
207
+ - The worker will no longer accept new jobs
208
+
209
+ This method is useful for immediately shutting down a worker, for example in emergency situations or when you need to take a worker offline quickly.
210
+
211
+ ---
212
+
213
+ ## stop_worker_gracefully
214
+
215
+ The `stop_worker_gracefully` method gracefully stops a worker by setting its status to `STOPPING`. This allows currently running jobs to complete before the worker shuts down.
216
+
217
+ ```python
218
+ def stop_worker_gracefully(self, worker_rid: UUID) -> None
219
+ ```
220
+
221
+ - `worker_rid`: The `UUID` identifying the worker to stop gracefully.
222
+
223
+ When a worker is stopped gracefully:
224
+
225
+ - The worker status is set to `STOPPING` in the database
226
+ - The heartbeat ticker detects the `STOPPING` status and sets `max_concurrency` to `0`
227
+ - Currently running jobs are allowed to complete normally
228
+ - No new jobs will be accepted by this worker
229
+ - Once all running jobs have finished, the worker status is automatically set to `STOPPED` and the worker shuts down
230
+
231
+ This method is ideal for maintenance scenarios where you want to ensure all in-progress work completes before shutting down the worker.
189
232
 
190
233
  ---
191
234
 
@@ -212,8 +255,8 @@ The `add_job` method adds a new job to the queue for execution. Jobs are units o
212
255
 
213
256
  ```python
214
257
  def add_job(
215
- self,
216
- task: Union[Callable, str],
258
+ self,
259
+ task: Union[Callable, str],
217
260
  *parameters: Any,
218
261
  **parameters_keyed: Any
219
262
  ) -> Job
@@ -322,7 +365,7 @@ class RetryBackoff(str, Enum):
322
365
 
323
366
  - `RETRY_BACKOFF_NONE`: No backoff. The retry_delay remains constant for all retries.
324
367
  - `RETRY_BACKOFF_LINEAR`: The retry delay increases linearly with each attempt (e.g., delay, 2*delay, 3*delay).
325
- - `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2*2).
368
+ - `RETRY_BACKOFF_EXPONENTIAL`: The retry delay increases exponentially with each attempt (e.g., delay, delay*2, delay*2\*2).
326
369
 
327
370
  ---
328
371
 
@@ -56,7 +56,7 @@ class QueuerGlobalMixin:
56
56
 
57
57
  # Database handlers
58
58
  self.db_job: JobDBHandler = JobDBHandler(
59
- self.database, db_config.with_table_drop, encryption_key
59
+ self.database, db_config, encryption_key
60
60
  )
61
61
  self.db_worker: WorkerDBHandler = WorkerDBHandler(
62
62
  self.database, db_config.with_table_drop
@@ -38,14 +38,27 @@ class QueuerMasterMixin(QueuerGlobalMixin):
38
38
 
39
39
  # Handle retention archive configuration
40
40
  try:
41
- if old_master.settings.retention_archive == 0:
42
- self.db_job.add_retention_archive(master_settings.retention_archive)
43
- elif (
44
- old_master.settings.retention_archive
45
- != master_settings.retention_archive
46
- ):
47
- self.db_job.remove_retention_archive()
48
- self.db_job.add_retention_archive(master_settings.retention_archive)
41
+ if master_settings.retention_archive > 0:
42
+ if old_master.settings.retention_archive == 0:
43
+ self.db_job.add_retention_archive(master_settings.retention_archive)
44
+ elif (
45
+ old_master.settings.retention_archive
46
+ != master_settings.retention_archive
47
+ ):
48
+ try:
49
+ self.db_job.remove_retention_archive()
50
+ except Exception as e:
51
+ logger.warning(
52
+ f"Could not remove old retention policy (may not exist): {e}"
53
+ )
54
+ self.db_job.add_retention_archive(master_settings.retention_archive)
55
+ elif old_master.settings.retention_archive > 0:
56
+ try:
57
+ self.db_job.remove_retention_archive()
58
+ except Exception as e:
59
+ logger.warning(
60
+ f"Could not remove retention policy (may not exist): {e}"
61
+ )
49
62
  except Exception as e:
50
63
  raise QueuerError("managing retention archive", e)
51
64
 
@@ -83,10 +96,9 @@ class QueuerMasterMixin(QueuerGlobalMixin):
83
96
  logger.error(f"Error in master ticker task: {e}")
84
97
 
85
98
  ticker = Ticker(
86
- task=ticker_task,
87
- name="_master_ticker",
88
99
  interval=timedelta(seconds=master_settings.master_poll_interval),
89
- func=ticker_task,
100
+ task=ticker_task,
101
+ use_mp=False, # Use threading instead of multiprocessing to avoid pickling issues
90
102
  )
91
103
  logger.info("Starting master ticker...")
92
104
  ticker.go()
@@ -150,10 +162,9 @@ class QueuerMasterMixin(QueuerGlobalMixin):
150
162
 
151
163
  # Create and start the polling ticker
152
164
  ticker = Ticker(
153
- task=ticker_task,
154
- name="_poll_master_ticker",
155
165
  interval=timedelta(seconds=master_settings.master_poll_interval),
156
- func=ticker_task,
166
+ task=ticker_task,
167
+ use_mp=False, # Use threading instead of multiprocessing to avoid pickling issues
157
168
  )
158
169
 
159
170
  logger.info("Starting master poll ticker...")
@@ -15,6 +15,19 @@ from .queuer_global import QueuerGlobalMixin
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
+ def get_current_worker_rid(self) -> Optional[UUID]:
19
+ """
20
+ Get the current worker's RID safely with mutex locking.
21
+ Mirrors Go's GetCurrentWorkerRID method.
22
+
23
+ :return: Worker RID or None if no worker
24
+ """
25
+ with self.worker_mutex:
26
+ if self.worker is not None:
27
+ return self.worker.rid
28
+ return None
29
+
30
+
18
31
  class QueuerWorkerMixin(QueuerGlobalMixin):
19
32
  """
20
33
  Mixin class containing worker-related methods for the Queuer.
@@ -1,7 +1,9 @@
1
- CREATE OR REPLACE FUNCTION init_job() RETURNS VOID AS $$
1
+ CREATE OR REPLACE FUNCTION init_job(enable_timescaledb BOOLEAN DEFAULT TRUE) RETURNS VOID AS $$
2
2
  BEGIN
3
3
  -- Create TimescaleDB extension
4
- CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
4
+ IF enable_timescaledb THEN
5
+ CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
6
+ END IF;
5
7
 
6
8
  -- Create pgcrypto extension
7
9
  CREATE EXTENSION IF NOT EXISTS pgcrypto;
@@ -37,7 +39,9 @@ BEGIN
37
39
  );
38
40
 
39
41
  -- Create hypertable (TimescaleDB feature)
40
- PERFORM create_hypertable('job_archive', by_range('updated_at'), if_not_exists => TRUE);
42
+ IF enable_timescaledb THEN
43
+ PERFORM create_hypertable('job_archive', by_range('updated_at'), if_not_exists => TRUE);
44
+ END IF;
41
45
 
42
46
  -- Create triggers for notifications
43
47
  CREATE OR REPLACE TRIGGER job_notify_event
@@ -698,17 +702,64 @@ BEGIN
698
702
  END;
699
703
  $$ LANGUAGE plpgsql;
700
704
 
701
- CREATE OR REPLACE FUNCTION add_retention_archive(input_retention_days INT)
705
+ CREATE OR REPLACE FUNCTION add_retention_archive(input_retention_days INT, enable_timescaledb BOOLEAN DEFAULT TRUE)
702
706
  RETURNS VOID AS $$
703
707
  BEGIN
704
- PERFORM add_retention_policy('job_archive', (input_retention_days * INTERVAL '1 day'));
708
+ IF enable_timescaledb THEN
709
+ -- Use TimescaleDB retention policy
710
+ PERFORM add_retention_policy('job_archive', (input_retention_days * INTERVAL '1 day'));
711
+ ELSE
712
+ -- Create sequence for tracking inserts
713
+ CREATE SEQUENCE IF NOT EXISTS job_archive_insert_counter;
714
+
715
+ -- Create trigger function that deletes old records every 10th insert
716
+ EXECUTE format('
717
+ CREATE OR REPLACE FUNCTION cleanup_job_archive_trigger()
718
+ RETURNS TRIGGER AS $func$
719
+ DECLARE
720
+ insert_count BIGINT;
721
+ deleted_count INT;
722
+ BEGIN
723
+ -- Only run cleanup every 10th insert
724
+ insert_count := nextval(''job_archive_insert_counter'');
725
+ IF insert_count %% 10 = 0 THEN
726
+ DELETE FROM job_archive
727
+ WHERE updated_at < (CURRENT_TIMESTAMP - INTERVAL ''%s days'');
728
+
729
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
730
+
731
+ -- Log cleanup if any rows were deleted (optional)
732
+ IF deleted_count > 0 THEN
733
+ RAISE NOTICE ''Cleaned up %% old job_archive records'', deleted_count;
734
+ END IF;
735
+ END IF;
736
+ RETURN NEW;
737
+ END;
738
+ $func$ LANGUAGE plpgsql;
739
+ ', input_retention_days);
740
+
741
+ -- Create trigger on job_archive
742
+ DROP TRIGGER IF EXISTS job_archive_cleanup_trigger ON job_archive;
743
+ CREATE TRIGGER job_archive_cleanup_trigger
744
+ AFTER INSERT ON job_archive
745
+ FOR EACH ROW
746
+ EXECUTE FUNCTION cleanup_job_archive_trigger();
747
+ END IF;
705
748
  END;
706
749
  $$ LANGUAGE plpgsql;
707
750
 
708
- CREATE OR REPLACE FUNCTION remove_retention_archive()
751
+ CREATE OR REPLACE FUNCTION remove_retention_archive(enable_timescaledb BOOLEAN DEFAULT TRUE)
709
752
  RETURNS VOID AS $$
710
753
  BEGIN
711
- PERFORM remove_retention_policy('job_archive');
754
+ IF enable_timescaledb THEN
755
+ -- Remove TimescaleDB retention policy
756
+ PERFORM remove_retention_policy('job_archive');
757
+ ELSE
758
+ -- Drop trigger and related objects
759
+ DROP TRIGGER IF EXISTS job_archive_cleanup_trigger ON job_archive;
760
+ DROP FUNCTION IF EXISTS cleanup_job_archive_trigger();
761
+ DROP SEQUENCE IF EXISTS job_archive_insert_counter;
762
+ END IF;
712
763
  END;
713
764
  $$ LANGUAGE plpgsql;
714
765
 
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
File without changes
File without changes
File without changes
File without changes