dsw-command-queue 4.13.0__tar.gz → 4.14.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.
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/PKG-INFO +4 -4
- dsw_command_queue-4.14.0/dsw/command_queue/__init__.py +3 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw/command_queue/build_info.py +4 -4
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw/command_queue/command_queue.py +95 -34
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw/command_queue/query.py +22 -5
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/PKG-INFO +4 -4
- dsw_command_queue-4.14.0/dsw_command_queue.egg-info/requires.txt +2 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/pyproject.toml +4 -4
- dsw_command_queue-4.13.0/dsw/command_queue/__init__.py +0 -3
- dsw_command_queue-4.13.0/dsw_command_queue.egg-info/requires.txt +0 -2
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/LICENSE +0 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/README.md +0 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/SOURCES.txt +0 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/dependency_links.txt +0 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/not-zip-safe +0 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/top_level.txt +0 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/setup.cfg +0 -0
- {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dsw-command-queue
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.14.0
|
|
4
4
|
Summary: Library for working with command queue and persistent commands
|
|
5
5
|
Author-email: Marek Suchánek <marek.suchanek@ds-wizard.org>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -11,16 +11,16 @@ Keywords: dsw,subscriber,publisher,database,queue,processing
|
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
13
|
Classifier: Programming Language :: Python
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Topic :: Database
|
|
17
17
|
Classifier: Topic :: Text Processing
|
|
18
18
|
Classifier: Topic :: Utilities
|
|
19
|
-
Requires-Python: <4,>=3.
|
|
19
|
+
Requires-Python: <4,>=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: func-timeout
|
|
23
|
-
Requires-Dist: dsw-database==4.
|
|
23
|
+
Requires-Dist: dsw-database==4.14.0
|
|
24
24
|
|
|
25
25
|
# Data Stewardship Wizard: Command Queue
|
|
26
26
|
|
|
@@ -9,9 +9,9 @@ BuildInfo = namedtuple(
|
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
BUILD_INFO = BuildInfo(
|
|
12
|
-
version='v4.
|
|
13
|
-
built_at='
|
|
14
|
-
sha='
|
|
12
|
+
version='v4.14.0~213910f',
|
|
13
|
+
built_at='2025-01-07 08:17:30Z',
|
|
14
|
+
sha='213910ffb32a7cea98942ccd0f6c52cb6cf79128',
|
|
15
15
|
branch='HEAD',
|
|
16
|
-
tag='v4.
|
|
16
|
+
tag='v4.14.0',
|
|
17
17
|
)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import datetime
|
|
3
|
-
import func_timeout
|
|
4
3
|
import logging
|
|
5
4
|
import os
|
|
6
5
|
import platform
|
|
7
|
-
import psycopg
|
|
8
|
-
import psycopg.generators
|
|
9
6
|
import select
|
|
10
7
|
import signal
|
|
8
|
+
|
|
9
|
+
import func_timeout
|
|
10
|
+
import psycopg
|
|
11
|
+
import psycopg.generators
|
|
11
12
|
import tenacity
|
|
12
13
|
|
|
13
14
|
from dsw.database import Database
|
|
@@ -22,7 +23,6 @@ RETRY_QUERY_TRIES = 3
|
|
|
22
23
|
RETRY_QUEUE_MULTIPLIER = 0.5
|
|
23
24
|
RETRY_QUEUE_TRIES = 5
|
|
24
25
|
|
|
25
|
-
INTERRUPTED = False
|
|
26
26
|
IS_LINUX = platform == 'Linux'
|
|
27
27
|
|
|
28
28
|
if IS_LINUX:
|
|
@@ -30,20 +30,48 @@ if IS_LINUX:
|
|
|
30
30
|
signal.set_wakeup_fd(_QUEUE_PIPE_W)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
class CommandJobError(BaseException):
|
|
34
|
+
|
|
35
|
+
def __init__(self, job_id: str, message: str, try_again: bool,
|
|
36
|
+
exc: BaseException | None = None):
|
|
37
|
+
self.job_id = job_id
|
|
38
|
+
self.message = message
|
|
39
|
+
self.try_again = try_again
|
|
40
|
+
self.exc = exc
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return self.message
|
|
45
|
+
|
|
46
|
+
def log_message(self):
|
|
47
|
+
if self.exc is None:
|
|
48
|
+
return self.message
|
|
49
|
+
return f'{self.message} (caused by: [{type(self.exc).__name__}] {str(self.exc)})'
|
|
50
|
+
|
|
51
|
+
def db_message(self):
|
|
52
|
+
if self.exc is None:
|
|
53
|
+
return self.message
|
|
54
|
+
return f'{self.message}\n\n' \
|
|
55
|
+
f'Caused by: {type(self.exc).__name__}\n' \
|
|
56
|
+
f'{str(self.exc)}'
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def create(job_id: str, message: str, try_again: bool = True,
|
|
60
|
+
exc: BaseException | None = None):
|
|
61
|
+
if isinstance(exc, CommandJobError):
|
|
62
|
+
return exc
|
|
63
|
+
return CommandJobError(
|
|
64
|
+
job_id=job_id,
|
|
65
|
+
message=message,
|
|
66
|
+
try_again=try_again,
|
|
67
|
+
exc=exc,
|
|
68
|
+
)
|
|
41
69
|
|
|
42
70
|
|
|
43
71
|
class CommandWorker:
|
|
44
72
|
|
|
45
73
|
@abc.abstractmethod
|
|
46
|
-
def work(self,
|
|
74
|
+
def work(self, command: PersistentCommand):
|
|
47
75
|
pass
|
|
48
76
|
|
|
49
77
|
def process_timeout(self, e: BaseException):
|
|
@@ -55,7 +83,7 @@ class CommandWorker:
|
|
|
55
83
|
|
|
56
84
|
class CommandQueue:
|
|
57
85
|
|
|
58
|
-
def __init__(self, worker: CommandWorker, db: Database,
|
|
86
|
+
def __init__(self, *, worker: CommandWorker, db: Database,
|
|
59
87
|
channel: str, component: str, wait_timeout: float,
|
|
60
88
|
work_timeout: int | None = None):
|
|
61
89
|
self.worker = worker
|
|
@@ -66,6 +94,10 @@ class CommandQueue:
|
|
|
66
94
|
)
|
|
67
95
|
self.wait_timeout = wait_timeout
|
|
68
96
|
self.work_timeout = work_timeout
|
|
97
|
+
self._interrupted = False
|
|
98
|
+
|
|
99
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
|
100
|
+
signal.signal(signal.SIGABRT, self._signal_handler)
|
|
69
101
|
|
|
70
102
|
@tenacity.retry(
|
|
71
103
|
reraise=True,
|
|
@@ -92,19 +124,19 @@ class CommandQueue:
|
|
|
92
124
|
LOG.debug('Waiting for notifications')
|
|
93
125
|
w = select.select(fds, [], [], self.wait_timeout)
|
|
94
126
|
|
|
95
|
-
if
|
|
127
|
+
if self._interrupted:
|
|
96
128
|
LOG.debug('Interrupt signal received, ending...')
|
|
97
129
|
break
|
|
98
130
|
|
|
99
131
|
if w == ([], [], []):
|
|
100
|
-
LOG.debug(
|
|
101
|
-
|
|
132
|
+
LOG.debug('Nothing received in this cycle (timeout %s seconds)',
|
|
133
|
+
self.wait_timeout)
|
|
102
134
|
else:
|
|
103
135
|
notifications = 0
|
|
104
136
|
for n in psycopg.generators.notifies(queue_conn.connection.pgconn):
|
|
105
137
|
notifications += 1
|
|
106
138
|
LOG.debug(str(n))
|
|
107
|
-
LOG.info(
|
|
139
|
+
LOG.info('Notifications received (%s in total)', notifications)
|
|
108
140
|
LOG.debug('Exiting command queue')
|
|
109
141
|
|
|
110
142
|
@tenacity.retry(
|
|
@@ -123,11 +155,12 @@ class CommandQueue:
|
|
|
123
155
|
count = 0
|
|
124
156
|
while self.fetch_and_process():
|
|
125
157
|
count += 1
|
|
126
|
-
LOG.info(
|
|
158
|
+
LOG.info('There are no more commands to process (%s processed)',
|
|
159
|
+
count)
|
|
127
160
|
|
|
128
161
|
def accept_notification(self, payload: psycopg.Notify) -> bool:
|
|
129
|
-
LOG.debug(
|
|
130
|
-
|
|
162
|
+
LOG.debug('Accepting notification from channel "%s" (PID = %s) %s',
|
|
163
|
+
payload.channel, payload.pid, payload.payload)
|
|
131
164
|
LOG.debug('Trying to fetch a new job')
|
|
132
165
|
return self.fetch_and_process()
|
|
133
166
|
|
|
@@ -139,16 +172,25 @@ class CommandQueue:
|
|
|
139
172
|
)
|
|
140
173
|
result = cursor.fetchall()
|
|
141
174
|
if len(result) != 1:
|
|
142
|
-
LOG.debug(
|
|
175
|
+
LOG.debug('Fetched %s persistent commands', len(result))
|
|
143
176
|
return False
|
|
144
177
|
|
|
145
178
|
command = PersistentCommand.from_dict_row(result[0])
|
|
146
|
-
LOG.info(
|
|
147
|
-
LOG.debug(
|
|
148
|
-
LOG.debug(
|
|
149
|
-
LOG.debug(
|
|
150
|
-
attempt_number = command.attempts + 1
|
|
179
|
+
LOG.info('Retrieved persistent command %s for processing', command.uuid)
|
|
180
|
+
LOG.debug('Previous state: %s', command.state)
|
|
181
|
+
LOG.debug('Attempts: %s / %s', command.attempts, command.max_attempts)
|
|
182
|
+
LOG.debug('Last error: %s', command.last_error_message)
|
|
151
183
|
|
|
184
|
+
self._process(command)
|
|
185
|
+
|
|
186
|
+
LOG.debug('Committing transaction')
|
|
187
|
+
self.db.conn_query.connection.commit()
|
|
188
|
+
cursor.close()
|
|
189
|
+
LOG.info('Notification processing finished')
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def _process(self, command: PersistentCommand):
|
|
193
|
+
attempt_number = command.attempts + 1
|
|
152
194
|
try:
|
|
153
195
|
self.db.execute_query(
|
|
154
196
|
query=self.queries.query_command_start(),
|
|
@@ -165,7 +207,8 @@ class CommandQueue:
|
|
|
165
207
|
LOG.info('Processing (without any timeout set)')
|
|
166
208
|
work()
|
|
167
209
|
else:
|
|
168
|
-
LOG.info(
|
|
210
|
+
LOG.info('Processing (with timeout set to %s seconds)',
|
|
211
|
+
self.work_timeout)
|
|
169
212
|
func_timeout.func_timeout(
|
|
170
213
|
timeout=self.work_timeout,
|
|
171
214
|
func=work,
|
|
@@ -190,8 +233,27 @@ class CommandQueue:
|
|
|
190
233
|
updated_at=datetime.datetime.now(tz=datetime.UTC),
|
|
191
234
|
uuid=command.uuid,
|
|
192
235
|
)
|
|
236
|
+
except CommandJobError as e:
|
|
237
|
+
if e.try_again and attempt_number < command.max_attempts:
|
|
238
|
+
query = self.queries.query_command_error()
|
|
239
|
+
msg = f'Failed with job error: {e.message} (will try again)'
|
|
240
|
+
else:
|
|
241
|
+
query = self.queries.query_command_error_stop()
|
|
242
|
+
msg = f'Failed with job error: {e.message}'
|
|
243
|
+
LOG.warning(msg)
|
|
244
|
+
self.worker.process_exception(e)
|
|
245
|
+
self.db.execute_query(
|
|
246
|
+
query=query,
|
|
247
|
+
attempts=attempt_number,
|
|
248
|
+
error_message=msg,
|
|
249
|
+
updated_at=datetime.datetime.now(tz=datetime.UTC),
|
|
250
|
+
uuid=command.uuid,
|
|
251
|
+
)
|
|
193
252
|
except Exception as e:
|
|
194
|
-
|
|
253
|
+
if attempt_number < command.max_attempts:
|
|
254
|
+
msg = f'Failed with exception [{type(e).__name__}]: {str(e)} (will try again)'
|
|
255
|
+
else:
|
|
256
|
+
msg = f'Failed with exception [{type(e).__name__}]: {str(e)}'
|
|
195
257
|
LOG.warning(msg)
|
|
196
258
|
self.worker.process_exception(e)
|
|
197
259
|
self.db.execute_query(
|
|
@@ -202,8 +264,7 @@ class CommandQueue:
|
|
|
202
264
|
uuid=command.uuid,
|
|
203
265
|
)
|
|
204
266
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return True
|
|
267
|
+
def _signal_handler(self, recv_signal, frame):
|
|
268
|
+
LOG.warning('Received interrupt signal: %s (frame: %s)',
|
|
269
|
+
recv_signal, frame)
|
|
270
|
+
self._interrupted = True
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CommandState(enum.Enum):
|
|
2
5
|
NEW = 'NewPersistentCommandState'
|
|
3
6
|
DONE = 'DonePersistentCommandState'
|
|
4
7
|
ERROR = 'ErrorPersistentCommandState'
|
|
@@ -20,9 +23,11 @@ class CommandQueries:
|
|
|
20
23
|
FROM persistent_command
|
|
21
24
|
WHERE component = '{self.component}'
|
|
22
25
|
AND attempts < max_attempts
|
|
23
|
-
AND state != '{CommandState.DONE}'
|
|
24
|
-
AND state != '{CommandState.IGNORE}'
|
|
25
|
-
AND (updated_at AT TIME ZONE 'UTC')
|
|
26
|
+
AND state != '{CommandState.DONE.value}'
|
|
27
|
+
AND state != '{CommandState.IGNORE.value}'
|
|
28
|
+
AND (updated_at AT TIME ZONE 'UTC')
|
|
29
|
+
<
|
|
30
|
+
(%(now)s - ({exp} ^ attempts - 1) * INTERVAL '{interval}')
|
|
26
31
|
ORDER BY attempts ASC, updated_at DESC
|
|
27
32
|
LIMIT 1 FOR UPDATE SKIP LOCKED;
|
|
28
33
|
"""
|
|
@@ -32,6 +37,18 @@ class CommandQueries:
|
|
|
32
37
|
return f"""
|
|
33
38
|
UPDATE persistent_command
|
|
34
39
|
SET attempts = %(attempts)s,
|
|
40
|
+
last_error_message = %(error_message)s,
|
|
41
|
+
state = '{CommandState.ERROR.value}',
|
|
42
|
+
updated_at = %(updated_at)s
|
|
43
|
+
WHERE uuid = %(uuid)s;
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def query_command_error_stop() -> str:
|
|
48
|
+
return f"""
|
|
49
|
+
UPDATE persistent_command
|
|
50
|
+
SET attempts = %(attempts)s,
|
|
51
|
+
max_attempts = %(attempts)s,
|
|
35
52
|
last_error_message = %(error_message)s,
|
|
36
53
|
state = '{CommandState.ERROR}',
|
|
37
54
|
updated_at = %(updated_at)s
|
|
@@ -43,7 +60,7 @@ class CommandQueries:
|
|
|
43
60
|
return f"""
|
|
44
61
|
UPDATE persistent_command
|
|
45
62
|
SET attempts = %(attempts)s,
|
|
46
|
-
state = '{CommandState.DONE}',
|
|
63
|
+
state = '{CommandState.DONE.value}',
|
|
47
64
|
updated_at = %(updated_at)s
|
|
48
65
|
WHERE uuid = %(uuid)s;
|
|
49
66
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dsw-command-queue
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.14.0
|
|
4
4
|
Summary: Library for working with command queue and persistent commands
|
|
5
5
|
Author-email: Marek Suchánek <marek.suchanek@ds-wizard.org>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -11,16 +11,16 @@ Keywords: dsw,subscriber,publisher,database,queue,processing
|
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
13
|
Classifier: Programming Language :: Python
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Topic :: Database
|
|
17
17
|
Classifier: Topic :: Text Processing
|
|
18
18
|
Classifier: Topic :: Utilities
|
|
19
|
-
Requires-Python: <4,>=3.
|
|
19
|
+
Requires-Python: <4,>=3.11
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: func-timeout
|
|
23
|
-
Requires-Dist: dsw-database==4.
|
|
23
|
+
Requires-Dist: dsw-database==4.14.0
|
|
24
24
|
|
|
25
25
|
# Data Stewardship Wizard: Command Queue
|
|
26
26
|
|
|
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'dsw-command-queue'
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.14.0"
|
|
8
8
|
description = 'Library for working with command queue and persistent commands'
|
|
9
9
|
readme = 'README.md'
|
|
10
10
|
keywords = ['dsw', 'subscriber', 'publisher', 'database', 'queue', 'processing']
|
|
@@ -16,17 +16,17 @@ classifiers = [
|
|
|
16
16
|
'Development Status :: 5 - Production/Stable',
|
|
17
17
|
'License :: OSI Approved :: Apache Software License',
|
|
18
18
|
'Programming Language :: Python',
|
|
19
|
-
'Programming Language :: Python :: 3.10',
|
|
20
19
|
'Programming Language :: Python :: 3.11',
|
|
20
|
+
'Programming Language :: Python :: 3.12',
|
|
21
21
|
'Topic :: Database',
|
|
22
22
|
'Topic :: Text Processing',
|
|
23
23
|
'Topic :: Utilities',
|
|
24
24
|
]
|
|
25
|
-
requires-python = '>=3.
|
|
25
|
+
requires-python = '>=3.11, <4'
|
|
26
26
|
dependencies = [
|
|
27
27
|
'func-timeout',
|
|
28
28
|
# DSW
|
|
29
|
-
"dsw-database==4.
|
|
29
|
+
"dsw-database==4.14.0",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
{dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|