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.
Files changed (18) hide show
  1. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/PKG-INFO +4 -4
  2. dsw_command_queue-4.14.0/dsw/command_queue/__init__.py +3 -0
  3. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw/command_queue/build_info.py +4 -4
  4. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw/command_queue/command_queue.py +95 -34
  5. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw/command_queue/query.py +22 -5
  6. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/PKG-INFO +4 -4
  7. dsw_command_queue-4.14.0/dsw_command_queue.egg-info/requires.txt +2 -0
  8. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/pyproject.toml +4 -4
  9. dsw_command_queue-4.13.0/dsw/command_queue/__init__.py +0 -3
  10. dsw_command_queue-4.13.0/dsw_command_queue.egg-info/requires.txt +0 -2
  11. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/LICENSE +0 -0
  12. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/README.md +0 -0
  13. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/SOURCES.txt +0 -0
  14. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/dependency_links.txt +0 -0
  15. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/not-zip-safe +0 -0
  16. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/dsw_command_queue.egg-info/top_level.txt +0 -0
  17. {dsw_command_queue-4.13.0 → dsw_command_queue-4.14.0}/setup.cfg +0 -0
  18. {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.13.0
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.10
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.13.0
23
+ Requires-Dist: dsw-database==4.14.0
24
24
 
25
25
  # Data Stewardship Wizard: Command Queue
26
26
 
@@ -0,0 +1,3 @@
1
+ from .command_queue import CommandJobError, CommandQueue, CommandWorker
2
+
3
+ __all__ = ['CommandJobError', 'CommandQueue', 'CommandWorker']
@@ -9,9 +9,9 @@ BuildInfo = namedtuple(
9
9
  )
10
10
 
11
11
  BUILD_INFO = BuildInfo(
12
- version='v4.13.0~c8e1cb3',
13
- built_at='2024-12-05 08:07:11Z',
14
- sha='c8e1cb3b7e0cc93bdf509eef715cd0a926d907c3',
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.13.0',
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
- def signal_handler(recv_signal, frame):
34
- global INTERRUPTED
35
- LOG.warning(f'Received interrupt signal: {recv_signal}')
36
- INTERRUPTED = True
37
-
38
-
39
- signal.signal(signal.SIGINT, signal_handler)
40
- signal.signal(signal.SIGABRT, signal_handler)
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, payload: PersistentCommand):
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 INTERRUPTED:
127
+ if self._interrupted:
96
128
  LOG.debug('Interrupt signal received, ending...')
97
129
  break
98
130
 
99
131
  if w == ([], [], []):
100
- LOG.debug(f'Nothing received in this cycle '
101
- f'(timeouted after {self.wait_timeout} seconds)')
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(f'Notifications received ({notifications} in total)')
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(f'There are no more commands to process ({count} processed)')
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(f'Accepting notification from channel "{payload.channel}" '
130
- f'(PID = {payload.pid}) {payload.payload}')
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(f'Fetched {len(result)} persistent commands')
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(f'Retrieved persistent command {command.uuid} for processing')
147
- LOG.debug(f'Previous state: {command.state}')
148
- LOG.debug(f'Attempts: {command.attempts} / {command.max_attempts}')
149
- LOG.debug(f'Last error: {command.last_error_message}')
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(f'Processing (with timeout set to {self.work_timeout} seconds)')
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
- msg = f'Failed with exception: {str(e)} ({type(e).__name__})'
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
- LOG.debug('Committing transaction')
206
- self.db.conn_query.connection.commit()
207
- cursor.close()
208
- LOG.info('Notification processing finished')
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
- class CommandState:
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') < (%(now)s - ({exp} ^ attempts - 1) * INTERVAL '{interval}')
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.13.0
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.10
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.13.0
23
+ Requires-Dist: dsw-database==4.14.0
24
24
 
25
25
  # Data Stewardship Wizard: Command Queue
26
26
 
@@ -0,0 +1,2 @@
1
+ func-timeout
2
+ dsw-database==4.14.0
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'dsw-command-queue'
7
- version = "4.13.0"
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.10, <4'
25
+ requires-python = '>=3.11, <4'
26
26
  dependencies = [
27
27
  'func-timeout',
28
28
  # DSW
29
- "dsw-database==4.13.0",
29
+ "dsw-database==4.14.0",
30
30
  ]
31
31
 
32
32
  [project.urls]
@@ -1,3 +0,0 @@
1
- from .command_queue import CommandQueue, CommandWorker
2
-
3
- __all__ = ['CommandQueue', 'CommandWorker']
@@ -1,2 +0,0 @@
1
- func-timeout
2
- dsw-database==4.13.0