morp 7.0.0__py3-none-any.whl
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.
- morp/__init__.py +23 -0
- morp/compat.py +4 -0
- morp/config.py +173 -0
- morp/exception.py +35 -0
- morp/interface/__init__.py +81 -0
- morp/interface/base.py +441 -0
- morp/interface/dropfile.py +160 -0
- morp/interface/postgres.py +368 -0
- morp/interface/sqs.py +396 -0
- morp/message.py +354 -0
- morp-7.0.0.dist-info/METADATA +212 -0
- morp-7.0.0.dist-info/RECORD +15 -0
- morp-7.0.0.dist-info/WHEEL +5 -0
- morp-7.0.0.dist-info/licenses/LICENSE.txt +21 -0
- morp-7.0.0.dist-info/top_level.txt +2 -0
morp/interface/base.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager, AbstractAsyncContextManager
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Self
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
from datatypes import Datetime, logging
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from cryptography.fernet import Fernet
|
|
10
|
+
except ImportError:
|
|
11
|
+
Fernet = None
|
|
12
|
+
|
|
13
|
+
from ..compat import *
|
|
14
|
+
from ..exception import InterfaceError
|
|
15
|
+
from ..config import Connection
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InterfaceABC(object):
|
|
22
|
+
"""This abstract base class containing all the methods that need to be
|
|
23
|
+
implemented in a child interface class.
|
|
24
|
+
|
|
25
|
+
Child classes should extend Interface (which extends this class). Interface
|
|
26
|
+
contains the public API for using the interface, these methods are broken
|
|
27
|
+
out from Interface just for convenience of seeing all the methods that must
|
|
28
|
+
be implemented
|
|
29
|
+
"""
|
|
30
|
+
async def _connect(self, connection_config: Connection) -> None:
|
|
31
|
+
raise NotImplementedError()
|
|
32
|
+
|
|
33
|
+
async def _get_connection(self) -> Any:
|
|
34
|
+
"""Returns a connection
|
|
35
|
+
|
|
36
|
+
usually the body of this is as simple as `return self._connection` but
|
|
37
|
+
needs to be implemented because the interfaces can be much more
|
|
38
|
+
convoluted in how it creates and manages the connection
|
|
39
|
+
|
|
40
|
+
:returns: Any, the interface connection instance
|
|
41
|
+
"""
|
|
42
|
+
raise NotImplementedError()
|
|
43
|
+
|
|
44
|
+
async def _close(self) -> None:
|
|
45
|
+
raise NotImplementedError()
|
|
46
|
+
|
|
47
|
+
async def _send(
|
|
48
|
+
self,
|
|
49
|
+
name: str,
|
|
50
|
+
connection: Any,
|
|
51
|
+
body: bytes,
|
|
52
|
+
**kwargs,
|
|
53
|
+
) -> tuple[str, Any]:
|
|
54
|
+
"""similar to self.send() but this takes a body, which is the message
|
|
55
|
+
completely encoded and ready to be sent by the backend
|
|
56
|
+
|
|
57
|
+
:returns: tuple[str, Any], (_id, raw) where _id is the message unique
|
|
58
|
+
id and raw is the returned receipt from the backend
|
|
59
|
+
"""
|
|
60
|
+
raise NotImplementedError()
|
|
61
|
+
|
|
62
|
+
async def _count(self, name: str, connection: Any, **kwargs) -> int:
|
|
63
|
+
"""count how many messages are currently in the queue
|
|
64
|
+
|
|
65
|
+
:returns: int, the rough count, depending on the backend this might not
|
|
66
|
+
be exact
|
|
67
|
+
"""
|
|
68
|
+
raise NotImplementedError()
|
|
69
|
+
|
|
70
|
+
async def _recv(
|
|
71
|
+
self,
|
|
72
|
+
name: str,
|
|
73
|
+
connection: Any,
|
|
74
|
+
**kwargs,
|
|
75
|
+
) -> tuple[str, bytes, Any]:
|
|
76
|
+
"""
|
|
77
|
+
:returns: (_id, body, raw) where body is the body that
|
|
78
|
+
was originally passed into send, raw is the untouched object
|
|
79
|
+
returned from recv, and _id is the unique id of the message
|
|
80
|
+
"""
|
|
81
|
+
raise NotImplementedError()
|
|
82
|
+
|
|
83
|
+
async def _ack(
|
|
84
|
+
self,
|
|
85
|
+
name: str,
|
|
86
|
+
connection: Any,
|
|
87
|
+
fields: Mapping,
|
|
88
|
+
**kwargs,
|
|
89
|
+
) -> None:
|
|
90
|
+
raise NotImplementedError()
|
|
91
|
+
|
|
92
|
+
async def _release(
|
|
93
|
+
self,
|
|
94
|
+
name: str,
|
|
95
|
+
connection: Any,
|
|
96
|
+
fields: Mapping,
|
|
97
|
+
**kwargs,
|
|
98
|
+
) -> None:
|
|
99
|
+
raise NotImplementedError()
|
|
100
|
+
|
|
101
|
+
async def _clear(self, name: str, connection: Any, **kwargs) -> None:
|
|
102
|
+
raise NotImplementedError()
|
|
103
|
+
|
|
104
|
+
async def _delete(self, name: str, connection: Any, **kwargs) -> None:
|
|
105
|
+
raise NotImplementedError()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Interface(InterfaceABC):
|
|
109
|
+
"""base class for interfaces to messaging"""
|
|
110
|
+
|
|
111
|
+
connected = False
|
|
112
|
+
"""true if a connection has been established, false otherwise"""
|
|
113
|
+
|
|
114
|
+
connection_config = None
|
|
115
|
+
"""a config.Connection() instance"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, connection_config: Connection|None = None):
|
|
118
|
+
self.connection_config = connection_config
|
|
119
|
+
|
|
120
|
+
async def connect(self, connection_config: Connection|None = None) -> bool:
|
|
121
|
+
"""connect to the interface
|
|
122
|
+
|
|
123
|
+
this will set the raw db connection to self.connection
|
|
124
|
+
"""
|
|
125
|
+
if self.connected:
|
|
126
|
+
return self.connected
|
|
127
|
+
|
|
128
|
+
if connection_config:
|
|
129
|
+
self.connection_config = connection_config
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
self.connected = False
|
|
133
|
+
await self._connect(self.connection_config)
|
|
134
|
+
self.connected = True
|
|
135
|
+
logger.debug("Connected to %s interface", self.__class__.__name__)
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise self._raise_error(e)
|
|
139
|
+
|
|
140
|
+
return self.connected
|
|
141
|
+
|
|
142
|
+
async def close(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
close an open connection
|
|
145
|
+
"""
|
|
146
|
+
if not self.connected:
|
|
147
|
+
return;
|
|
148
|
+
|
|
149
|
+
await self._close()
|
|
150
|
+
self.connected = False
|
|
151
|
+
logger.debug(
|
|
152
|
+
"Closed Connection to %s interface", self.__class__.__name__,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@asynccontextmanager
|
|
156
|
+
async def connection(
|
|
157
|
+
self,
|
|
158
|
+
connection: Any = None,
|
|
159
|
+
**kwargs,
|
|
160
|
+
) -> AbstractAsyncContextManager:
|
|
161
|
+
try:
|
|
162
|
+
if not connection:
|
|
163
|
+
if not self.connected:
|
|
164
|
+
await self.connect()
|
|
165
|
+
|
|
166
|
+
connection = await self._get_connection()
|
|
167
|
+
|
|
168
|
+
yield connection
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self._raise_error(e)
|
|
172
|
+
|
|
173
|
+
def _fields_to_body(self, fields: Mapping) -> bytes:
|
|
174
|
+
"""This will prepare the fields passed from Message to Interface.send
|
|
175
|
+
|
|
176
|
+
prepare a message to be sent over the backend
|
|
177
|
+
|
|
178
|
+
:param fields: dict, all the fields that will be sent to the backend
|
|
179
|
+
:returns: bytes, the fully encoded fields
|
|
180
|
+
"""
|
|
181
|
+
serializer = self.connection_config.serializer
|
|
182
|
+
if serializer == "pickle":
|
|
183
|
+
ret = pickle.dumps(fields, pickle.HIGHEST_PROTOCOL)
|
|
184
|
+
|
|
185
|
+
elif serializer == "json":
|
|
186
|
+
ret = ByteString(json.dumps(fields))
|
|
187
|
+
|
|
188
|
+
key = self.connection_config.key
|
|
189
|
+
if key:
|
|
190
|
+
if Fernet is None:
|
|
191
|
+
logger.warning(
|
|
192
|
+
"Cannot encrypt because of missing dependencies",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
logger.debug("Encrypting fields")
|
|
197
|
+
f = Fernet(key)
|
|
198
|
+
ret = f.encrypt(ret)
|
|
199
|
+
|
|
200
|
+
return ret
|
|
201
|
+
|
|
202
|
+
def _send_to_fields(self, _id: str, fields: Mapping, raw: Any) -> Mapping:
|
|
203
|
+
"""This creates the value that is returned from .send()
|
|
204
|
+
|
|
205
|
+
:param _id: str, the unique id of the message, usually created by the
|
|
206
|
+
backend
|
|
207
|
+
:param fields: dict, the fields that were passed to .send()
|
|
208
|
+
:param raw: mixed: whatever the backend returned after sending body
|
|
209
|
+
:returns: dict: the fields populated with additional keys. If the key
|
|
210
|
+
begins with an underscore then that usually means it was populated
|
|
211
|
+
internally
|
|
212
|
+
"""
|
|
213
|
+
fields["_id"] = _id
|
|
214
|
+
fields["_raw_send"] = raw
|
|
215
|
+
return fields
|
|
216
|
+
|
|
217
|
+
async def send(self, name: str, fields: Mapping, **kwargs) -> Mapping:
|
|
218
|
+
"""send an interface message to the message queue
|
|
219
|
+
|
|
220
|
+
:param name: str, the queue name
|
|
221
|
+
:param fields: dict, the fields to send to the queue name
|
|
222
|
+
:param **kwargs: anything else, this gets passed to self.connection()
|
|
223
|
+
:returns: dict, see .send_to_fields() for what this returns
|
|
224
|
+
"""
|
|
225
|
+
if not fields:
|
|
226
|
+
raise ValueError("No fields to send")
|
|
227
|
+
|
|
228
|
+
async with self.connection(**kwargs) as connection:
|
|
229
|
+
kwargs["connection"] = connection
|
|
230
|
+
_id, raw = await self._send(
|
|
231
|
+
name=name,
|
|
232
|
+
body=self._fields_to_body(fields),
|
|
233
|
+
**kwargs
|
|
234
|
+
)
|
|
235
|
+
logger.debug(
|
|
236
|
+
"Message %s sent to %s -- %s",
|
|
237
|
+
_id,
|
|
238
|
+
name,
|
|
239
|
+
fields,
|
|
240
|
+
)
|
|
241
|
+
return self._send_to_fields(_id, fields, raw)
|
|
242
|
+
|
|
243
|
+
async def count(self, name: str, **kwargs) -> int:
|
|
244
|
+
"""count how many messages are in queue name
|
|
245
|
+
|
|
246
|
+
:returns: int, a rough count of the messages in the queue, this is
|
|
247
|
+
backend dependent and might not be completely accurate
|
|
248
|
+
"""
|
|
249
|
+
async with self.connection(**kwargs) as connection:
|
|
250
|
+
kwargs["connection"] = connection
|
|
251
|
+
return int(await self._count(name, **kwargs))
|
|
252
|
+
|
|
253
|
+
def _body_to_fields(self, body: bytes) -> Mapping:
|
|
254
|
+
"""This will prepare the body returned from the backend to be passed
|
|
255
|
+
to Message
|
|
256
|
+
|
|
257
|
+
This turns a backend body back to the original fields
|
|
258
|
+
|
|
259
|
+
:param body: bytes, the body returned from the backend that needs to be
|
|
260
|
+
converted back into a dict
|
|
261
|
+
:returns: dict, the fields of the original message
|
|
262
|
+
"""
|
|
263
|
+
key = self.connection_config.key
|
|
264
|
+
if key:
|
|
265
|
+
if Fernet is None:
|
|
266
|
+
logger.warning(
|
|
267
|
+
"Skipping decrypt because of missing dependencies",
|
|
268
|
+
)
|
|
269
|
+
ret = body
|
|
270
|
+
|
|
271
|
+
else:
|
|
272
|
+
logger.debug("Decoding encrypted body")
|
|
273
|
+
f = Fernet(key)
|
|
274
|
+
ret = f.decrypt(ByteString(body))
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
ret = body
|
|
278
|
+
|
|
279
|
+
serializer = self.connection_config.serializer
|
|
280
|
+
if serializer == "pickle":
|
|
281
|
+
ret = pickle.loads(ret)
|
|
282
|
+
|
|
283
|
+
elif serializer == "json":
|
|
284
|
+
ret = json.loads(ret)
|
|
285
|
+
|
|
286
|
+
return ret
|
|
287
|
+
|
|
288
|
+
def _recv_to_fields(self, _id: str, body: bytes, raw: Any) -> Mapping:
|
|
289
|
+
"""This creates the value that is returned from .recv()
|
|
290
|
+
|
|
291
|
+
:param _id: str, the unique id of the message, usually created by the
|
|
292
|
+
backend
|
|
293
|
+
:param body: bytes, the backend message body
|
|
294
|
+
:param raw: mixed: whatever the backend fetched from its receive method
|
|
295
|
+
:returns: dict: the fields populated with additional keys. If the key
|
|
296
|
+
begins with an underscore then that usually means it was populated
|
|
297
|
+
internally
|
|
298
|
+
"""
|
|
299
|
+
fields = self._body_to_fields(body)
|
|
300
|
+
fields["_id"] = _id
|
|
301
|
+
fields["_raw_recv"] = raw
|
|
302
|
+
fields.setdefault("_count", 0)
|
|
303
|
+
fields["_count"] += 1
|
|
304
|
+
return fields
|
|
305
|
+
|
|
306
|
+
async def recv(
|
|
307
|
+
self,
|
|
308
|
+
name: str,
|
|
309
|
+
timeout: float|int = 0.0,
|
|
310
|
+
**kwargs,
|
|
311
|
+
) -> Mapping:
|
|
312
|
+
"""receive a message from queue name
|
|
313
|
+
|
|
314
|
+
:param name: str, the queue name
|
|
315
|
+
:param timeout: integer, seconds to try and receive a message before
|
|
316
|
+
returning None, 0 means no timeout or max timeout if the interface
|
|
317
|
+
requires a timeout
|
|
318
|
+
:returns: dict, the fields that were sent via .send populated with
|
|
319
|
+
additional keys (additional keys will usually be prefixed with an
|
|
320
|
+
underscore), it will return None if it failed to fetch (ie, timeout
|
|
321
|
+
or error)
|
|
322
|
+
"""
|
|
323
|
+
async with self.connection(**kwargs) as connection:
|
|
324
|
+
kwargs["connection"] = connection
|
|
325
|
+
_id, body, raw = await self._recv(
|
|
326
|
+
name,
|
|
327
|
+
timeout=timeout,
|
|
328
|
+
**kwargs
|
|
329
|
+
)
|
|
330
|
+
fields = self._recv_to_fields(_id, body, raw) if body else None
|
|
331
|
+
if fields:
|
|
332
|
+
logger.log_for(
|
|
333
|
+
debug=(
|
|
334
|
+
"Message %s received from %s -- %s",
|
|
335
|
+
_id,
|
|
336
|
+
name,
|
|
337
|
+
fields
|
|
338
|
+
),
|
|
339
|
+
info=(
|
|
340
|
+
"Message %s recceived from %s -- %s",
|
|
341
|
+
_id,
|
|
342
|
+
name,
|
|
343
|
+
fields.keys(),
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return fields
|
|
348
|
+
|
|
349
|
+
async def ack(self, name: str, fields: Mapping, **kwargs) -> None:
|
|
350
|
+
"""this will acknowledge that the interface message was received
|
|
351
|
+
successfully
|
|
352
|
+
|
|
353
|
+
:param name: str, the queue name
|
|
354
|
+
:param fields: dict, these are the fields returned from .recv that have
|
|
355
|
+
additional fields that the backend will most likely need to ack the
|
|
356
|
+
message
|
|
357
|
+
"""
|
|
358
|
+
async with self.connection(**kwargs) as connection:
|
|
359
|
+
kwargs["connection"] = connection
|
|
360
|
+
await self._ack(name, fields=fields, **kwargs)
|
|
361
|
+
logger.debug("Message %s acked from %s", fields["_id"], name)
|
|
362
|
+
|
|
363
|
+
async def release(self, name: str, fields: Mapping, **kwargs) -> None:
|
|
364
|
+
"""release the message back into the queue, this is usually for when
|
|
365
|
+
processing the message has failed and so a new attempt to process the
|
|
366
|
+
message should be made
|
|
367
|
+
|
|
368
|
+
:param name: str, the queue name
|
|
369
|
+
:param fields: dict, these are the fields returned from .recv that have
|
|
370
|
+
additional fields that the backend will most likely need to release
|
|
371
|
+
the message
|
|
372
|
+
"""
|
|
373
|
+
async with self.connection(**kwargs) as connection:
|
|
374
|
+
kwargs["connection"] = connection
|
|
375
|
+
delay_seconds = max(kwargs.pop('delay_seconds', 0), 0)
|
|
376
|
+
count = fields.get("_count", 0)
|
|
377
|
+
|
|
378
|
+
if delay_seconds == 0:
|
|
379
|
+
if count:
|
|
380
|
+
max_timeout = self.connection_config.options.get(
|
|
381
|
+
"max_timeout"
|
|
382
|
+
)
|
|
383
|
+
backoff = self.connection_config.options.get(
|
|
384
|
+
"backoff_multiplier"
|
|
385
|
+
)
|
|
386
|
+
amplifier = self.connection_config.options.get(
|
|
387
|
+
"backoff_amplifier",
|
|
388
|
+
count
|
|
389
|
+
)
|
|
390
|
+
delay_seconds = min(
|
|
391
|
+
max_timeout,
|
|
392
|
+
(count * backoff) * amplifier
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
await self._release(
|
|
396
|
+
name,
|
|
397
|
+
fields=fields,
|
|
398
|
+
delay_seconds=delay_seconds,
|
|
399
|
+
**kwargs
|
|
400
|
+
)
|
|
401
|
+
logger.debug(
|
|
402
|
+
"Message %s released back to %s count %s, with delay %ss",
|
|
403
|
+
fields["_id"],
|
|
404
|
+
name,
|
|
405
|
+
count,
|
|
406
|
+
delay_seconds,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
async def unsafe_clear(self, name: str, **kwargs) -> None:
|
|
410
|
+
"""clear the queue name, clearing the queue removes all the messages
|
|
411
|
+
from the queue but doesn't delete the actual queue
|
|
412
|
+
|
|
413
|
+
:param name: str, the queue name to clear
|
|
414
|
+
"""
|
|
415
|
+
async with self.connection(**kwargs) as connection:
|
|
416
|
+
kwargs["connection"] = connection
|
|
417
|
+
await self._clear(name, **kwargs)
|
|
418
|
+
logger.debug("Messages cleared from %s", name)
|
|
419
|
+
|
|
420
|
+
async def unsafe_delete(self, name: str, **kwargs) -> None:
|
|
421
|
+
"""delete the queue, this removes messages and the queue
|
|
422
|
+
|
|
423
|
+
:param name: str, the queue name to delete
|
|
424
|
+
"""
|
|
425
|
+
async with self.connection(**kwargs) as connection:
|
|
426
|
+
kwargs["connection"] = connection
|
|
427
|
+
await self._delete(name, **kwargs)
|
|
428
|
+
logger.debug("Queue %s deleted", name)
|
|
429
|
+
|
|
430
|
+
def _raise_error(self, e: BaseException) -> None:
|
|
431
|
+
"""this is just a wrapper to make the passed in exception an
|
|
432
|
+
InterfaceError"""
|
|
433
|
+
if (
|
|
434
|
+
isinstance(e, InterfaceError)
|
|
435
|
+
or hasattr(builtins, e.__class__.__name__)
|
|
436
|
+
):
|
|
437
|
+
raise e
|
|
438
|
+
|
|
439
|
+
else:
|
|
440
|
+
raise InterfaceError(e) from e
|
|
441
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
import fcntl
|
|
3
|
+
import errno
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from datatypes import (
|
|
8
|
+
Dirpath,
|
|
9
|
+
Filepath,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from ..compat import *
|
|
13
|
+
from .base import Interface
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Dropfile(Interface):
|
|
17
|
+
"""Dropfile interface using local files as messages, great for quick
|
|
18
|
+
prototyping or passing messages from the frontend to the backend on the
|
|
19
|
+
same machine
|
|
20
|
+
"""
|
|
21
|
+
_connection = None
|
|
22
|
+
|
|
23
|
+
@contextmanager
|
|
24
|
+
def queue(self, name, connection, **kwargs):
|
|
25
|
+
yield connection.child_dir(name, touch=True)
|
|
26
|
+
|
|
27
|
+
async def _connect(self, connection_config):
|
|
28
|
+
self._connection = Dirpath(
|
|
29
|
+
connection_config.path,
|
|
30
|
+
__name__.split(".", 1)[0],
|
|
31
|
+
"queue",
|
|
32
|
+
touch=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
async def _get_connection(self):
|
|
36
|
+
return self._connection
|
|
37
|
+
|
|
38
|
+
async def _close(self):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
async def _send(self, name, connection, body, **kwargs):
|
|
42
|
+
with self.queue(name, connection) as queue:
|
|
43
|
+
now = time.time_ns()
|
|
44
|
+
_id = uuid.uuid4().hex
|
|
45
|
+
|
|
46
|
+
if delay_seconds := kwargs.get('delay_seconds', 0):
|
|
47
|
+
now += (delay_seconds * 1000000000)
|
|
48
|
+
|
|
49
|
+
message = queue.child_file(f"{now}-{_id}-1.txt")
|
|
50
|
+
message.write_bytes(body)
|
|
51
|
+
return _id, message
|
|
52
|
+
|
|
53
|
+
async def _count(self, name, connection, **kwargs):
|
|
54
|
+
with self.queue(name, connection) as queue:
|
|
55
|
+
return queue.files().count()
|
|
56
|
+
|
|
57
|
+
def _recv_to_fields(self, _id, body, raw):
|
|
58
|
+
fields = super()._recv_to_fields(_id, body, raw)
|
|
59
|
+
fields["_count"] = raw._count
|
|
60
|
+
fields["_body"] = body
|
|
61
|
+
#fields["_created"] = raw.stat[9]
|
|
62
|
+
return fields
|
|
63
|
+
|
|
64
|
+
async def _recv(self, name, connection, **kwargs):
|
|
65
|
+
_id = body = raw = None
|
|
66
|
+
timeout = kwargs.get('timeout', 0.0)
|
|
67
|
+
count = 0.0
|
|
68
|
+
|
|
69
|
+
with self.queue(name, connection) as queue:
|
|
70
|
+
while count <= timeout:
|
|
71
|
+
now = time.time_ns()
|
|
72
|
+
for message in queue.files().sort():
|
|
73
|
+
parts = message.fileroot.split("-")
|
|
74
|
+
then = int(parts[0])
|
|
75
|
+
if now > then:
|
|
76
|
+
fp = message.open("rb+")
|
|
77
|
+
try:
|
|
78
|
+
fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
79
|
+
|
|
80
|
+
body = fp.read()
|
|
81
|
+
if body:
|
|
82
|
+
_id = parts[1]
|
|
83
|
+
message.fp = fp
|
|
84
|
+
message._count = int(parts[2])
|
|
85
|
+
raw = message
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
# looks like another process got to this
|
|
90
|
+
# message first, so try and clean it up
|
|
91
|
+
self._cleanup(fp, message, truncate=False)
|
|
92
|
+
|
|
93
|
+
except OSError as e:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
if body:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
else:
|
|
100
|
+
count += 0.1
|
|
101
|
+
if count < timeout:
|
|
102
|
+
time.sleep(0.1)
|
|
103
|
+
|
|
104
|
+
return _id, body, raw
|
|
105
|
+
|
|
106
|
+
async def _ack(self, name, connection, fields, **kwargs):
|
|
107
|
+
message = fields["_raw_recv"]
|
|
108
|
+
self._cleanup(message.fp, message)
|
|
109
|
+
|
|
110
|
+
async def _release(self, name, connection, fields, **kwargs):
|
|
111
|
+
delay_seconds = kwargs.get('delay_seconds', 0)
|
|
112
|
+
|
|
113
|
+
_id = fields["_id"]
|
|
114
|
+
message = fields["_raw_recv"]
|
|
115
|
+
body = fields["_body"]
|
|
116
|
+
fp = message.fp
|
|
117
|
+
|
|
118
|
+
if delay_seconds:
|
|
119
|
+
now = time.time_ns() + (delay_seconds * 1000000000)
|
|
120
|
+
|
|
121
|
+
# let's copy the file body to the future and then delete the old
|
|
122
|
+
# message, sadly, because we've got a lock on the file we can't
|
|
123
|
+
# move or copy it, so we're just going to create a new file
|
|
124
|
+
count = fields["_count"] + 1
|
|
125
|
+
dest = Filepath(message.dirname, f"{now}-{_id}-{count}.txt")
|
|
126
|
+
dest.write_bytes(body)
|
|
127
|
+
self._cleanup(fp, message)
|
|
128
|
+
|
|
129
|
+
else:
|
|
130
|
+
# release the message back into the queue
|
|
131
|
+
self._cleanup(fp, message, truncate=False, delete=False)
|
|
132
|
+
|
|
133
|
+
async def _clear(self, name, connection, **kwargs):
|
|
134
|
+
with self.queue(name, connection) as queue:
|
|
135
|
+
queue.clear()
|
|
136
|
+
|
|
137
|
+
async def _delete(self, name, connection, **kwargs):
|
|
138
|
+
with self.queue(name, connection) as queue:
|
|
139
|
+
queue.delete()
|
|
140
|
+
|
|
141
|
+
def _cleanup(self, fp, message, **kwargs):
|
|
142
|
+
# clear the message so other get() requests will move on (this is the
|
|
143
|
+
# one thing we can do with an exclusive lock to tell other processes
|
|
144
|
+
# we have already looked at the file, I wish we could delete under an
|
|
145
|
+
# exclusive lock)
|
|
146
|
+
try:
|
|
147
|
+
if kwargs.get("truncate", True):
|
|
148
|
+
fp.truncate()
|
|
149
|
+
|
|
150
|
+
fcntl.flock(fp, fcntl.LOCK_UN)
|
|
151
|
+
fp.close()
|
|
152
|
+
|
|
153
|
+
except ValueError:
|
|
154
|
+
# .truncate - ValueError: truncate of closed file
|
|
155
|
+
# .flock - ValueError: I/O operation on closed file
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
if kwargs.get("delete", True):
|
|
159
|
+
message.delete()
|
|
160
|
+
|