xtr-orm-messenger 6.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.
- xtr_orm_messenger/.agents/skills/xtr-orm-messenger/SKILL.md +108 -0
- xtr_orm_messenger/__init__.py +60 -0
- xtr_orm_messenger/exception/__init__.py +9 -0
- xtr_orm_messenger/exception/invalid_orm_dsn_error.py +34 -0
- xtr_orm_messenger/exception/orm_messenger_error.py +15 -0
- xtr_orm_messenger/exception/transport_error.py +32 -0
- xtr_orm_messenger/py.typed +0 -0
- xtr_orm_messenger/recipe/__init__.py +15 -0
- xtr_orm_messenger/recipe/manifest.toml +21 -0
- xtr_orm_messenger/transport/__init__.py +38 -0
- xtr_orm_messenger/transport/_database_errors.py +71 -0
- xtr_orm_messenger/transport/connection.py +438 -0
- xtr_orm_messenger/transport/messages_table.py +51 -0
- xtr_orm_messenger/transport/messages_table_driver.py +39 -0
- xtr_orm_messenger/transport/messages_tables.py +41 -0
- xtr_orm_messenger/transport/orm_options.py +145 -0
- xtr_orm_messenger/transport/orm_received_stamp.py +22 -0
- xtr_orm_messenger/transport/orm_receiver.py +214 -0
- xtr_orm_messenger/transport/orm_sender.py +51 -0
- xtr_orm_messenger/transport/orm_transport.py +170 -0
- xtr_orm_messenger/transport/orm_transport_factory.py +139 -0
- xtr_orm_messenger/transport/postgresql_notifier.py +105 -0
- xtr_orm_messenger/transport/stored_message.py +38 -0
- xtr_orm_messenger-6.0.0.dist-info/METADATA +280 -0
- xtr_orm_messenger-6.0.0.dist-info/RECORD +28 -0
- xtr_orm_messenger-6.0.0.dist-info/WHEEL +4 -0
- xtr_orm_messenger-6.0.0.dist-info/entry_points.txt +3 -0
- xtr_orm_messenger-6.0.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xtr-orm-messenger
|
|
3
|
+
description: How to queue xtr-messenger messages in a database table with xtr-orm-messenger — an orm DSN naming an xtr-orm connection, OrmTransportFactory registered by the orm bundle, queues, delays, redelivery of messages a dead worker held, PostgreSQL notifications, sending in the unit of work's transaction, the messages table in migrations, and counting, listing or finding queued messages. Use when an application wants a message queue without a broker, installs xtr-messenger[orm], configures an orm transport, routes messages to the database, runs a worker on a table, migrates the messenger_messages table, or tests code that sends through the orm transport.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xtr-orm-messenger
|
|
7
|
+
|
|
8
|
+
A transport for xtr-messenger whose messages live in a table on a connection xtr-orm
|
|
9
|
+
configures. It ships no bundle: with the messenger and orm bundles active, the orm bundle
|
|
10
|
+
registers it once it is installed. For buses, routing and workers load the `xtr-messenger`
|
|
11
|
+
skill; for connections and migrations, `xtr-orm`.
|
|
12
|
+
|
|
13
|
+
## Quick reference
|
|
14
|
+
|
|
15
|
+
- DSN — `orm://<connection>`, `orm://` for the default connection; queue with
|
|
16
|
+
`TransportConfig(dsn, queue="jobs")` or `?queue=jobs`, `default` otherwise.
|
|
17
|
+
- Options (DSN query or `options`) — `table_name` (`messenger_messages`), `redeliver_timeout`
|
|
18
|
+
(`3600` s), `auto_setup` (`true`), `poll_interval` (`1.0` s), `transactional` (`false`),
|
|
19
|
+
`use_notify` (`true`), `check_delayed_interval` / `get_notify_timeout` (`60000` ms).
|
|
20
|
+
- `OrmTransportFactory(registry, serializer, clock=None)` — not discovered by scheme: the orm
|
|
21
|
+
bundle registers it with the messenger's envelope serializer, or you pass it to
|
|
22
|
+
`MessageBusFactory` / `WorkerFactory`.
|
|
23
|
+
- Migrations — each connection's migrator adds the table of every `orm://` transport on it, so
|
|
24
|
+
`orm:migrations:diff` writes it; `messages_tables(bus_config, connection, default)` names them.
|
|
25
|
+
- Settling deletes the row; a row handed out longer than `redeliver_timeout` is claimed again.
|
|
26
|
+
- A `DelayStamp` makes the message due later, rounded up to a whole second.
|
|
27
|
+
|
|
28
|
+
## Send and consume without a container
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from xtr_messenger import (
|
|
32
|
+
JsonSerializer,
|
|
33
|
+
MessageBusConfig,
|
|
34
|
+
MessageBusFactory,
|
|
35
|
+
TransportConfig,
|
|
36
|
+
WorkerFactory,
|
|
37
|
+
)
|
|
38
|
+
from xtr_orm import ConnectionRegistry
|
|
39
|
+
from xtr_orm_messenger import OrmTransportFactory
|
|
40
|
+
from xtr_serializer import JsonEncoder, PydanticNormalizer, Serializer
|
|
41
|
+
|
|
42
|
+
registry = ConnectionRegistry()
|
|
43
|
+
registry.register("default", engine=engine, migrator=migrator, database=database)
|
|
44
|
+
wire = JsonSerializer(Serializer([PydanticNormalizer()], [JsonEncoder()]))
|
|
45
|
+
factory = OrmTransportFactory(registry, wire)
|
|
46
|
+
config = MessageBusConfig(
|
|
47
|
+
transports={"jobs": TransportConfig("orm://default", queue="jobs")},
|
|
48
|
+
routing={IngestDocument: "jobs"},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await MessageBusFactory(config, [factory]).bus().dispatch(IngestDocument(document_id=1))
|
|
52
|
+
await WorkerFactory(config, [factory]).worker(["jobs"]).run()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Pass the same factory to the bus and the worker: it keeps one transport per name.
|
|
56
|
+
|
|
57
|
+
## Send with the handler's changes
|
|
58
|
+
|
|
59
|
+
`TransportConfig("orm://default?transactional=true")` writes the message in the session of the
|
|
60
|
+
unit of work under way, so it is sent only when that unit commits. List `orm_transaction` in the
|
|
61
|
+
bus middleware, or commit the session yourself; outside a unit the message commits at once.
|
|
62
|
+
|
|
63
|
+
## Look into a queue
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
transport = factory.transport("jobs", config.transports["jobs"])
|
|
67
|
+
await transport.count()
|
|
68
|
+
messages = [envelope async for envelope in transport.all(limit=10)]
|
|
69
|
+
await transport.find(message_id)
|
|
70
|
+
await transport.keepalive(envelope, seconds=600) # a handler running past redeliver_timeout
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Testing
|
|
74
|
+
|
|
75
|
+
Run against a SQLite file per test (`sqlite+aiosqlite:///{tmp_path / 'app.sqlite'}`) and freeze
|
|
76
|
+
a `MockClock`: `OrmTransportFactory(registry, wire, clock=clock)` makes delays and redelivery happen
|
|
77
|
+
when the test moves the clock. A worker's `get()` never runs dry — stop the worker from a
|
|
78
|
+
handler, or run it in a task and call `stop()`. `in-memory://` stays the transport for tests that
|
|
79
|
+
do not care where messages are kept.
|
|
80
|
+
|
|
81
|
+
## Use in an application
|
|
82
|
+
|
|
83
|
+
1. With `MessengerBundle` and `OrmBundle` already active, `uv add "xtr-messenger[orm]"`. Nothing
|
|
84
|
+
to activate: the orm bundle registers the transport. No driver to add either: the transport
|
|
85
|
+
uses the connection's, notifications included when that driver is asyncpg.
|
|
86
|
+
2. In `<app>/config/packages/messenger.py`, name a transport `orm://default` and route messages
|
|
87
|
+
to it.
|
|
88
|
+
3. Run `<script> orm:migrations:diff` — the next revision creates the messages table. Add
|
|
89
|
+
`auto_setup=false` to the DSN to leave the table to the migrations alone.
|
|
90
|
+
4. Run a worker with `<script> messenger:consume <transport>`.
|
|
91
|
+
5. Check with `<script> debug:config messenger` — the `orm://` transports are listed.
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
- `OrmMessengerError` — the base, itself a `MessageBusError`.
|
|
96
|
+
- `TransportError` — the database refused a send, claim, settle or listing, or an envelope was
|
|
97
|
+
settled on a transport it was not received from; `transport`, `operation`, `reason`.
|
|
98
|
+
- `InvalidOrmDsnError` — the DSN names a connection the registry does not hold; a `LookupError`.
|
|
99
|
+
- `UnknownTransportOptionError` / `InvalidTransportOptionError` — from xtr-messenger, for a
|
|
100
|
+
setting the transport does not take or a value it cannot read.
|
|
101
|
+
|
|
102
|
+
## Do not
|
|
103
|
+
|
|
104
|
+
- Do not put a database URL in the DSN: it names a connection, configured in the orm.
|
|
105
|
+
- Do not rely on `transactional=true` without something committing the unit's session.
|
|
106
|
+
- Do not set `redeliver_timeout` below the time a handler takes, unless it calls `keepalive`.
|
|
107
|
+
- Do not expect notifications through a PostgreSQL driver other than asyncpg: it polls.
|
|
108
|
+
- Do not look for an `OrmMessengerBundle`: there is none; list `OrmBundle` and `MessengerBundle`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""A database transport for xtr-messenger, on the connections xtr-orm configures.
|
|
2
|
+
|
|
3
|
+
Messages are kept in a table on one of the application's connections, named by
|
|
4
|
+
the DSN — ``orm://default``. A worker claims the oldest one of its queue that
|
|
5
|
+
is due, a delayed one falls due when its delay has passed, a message whose
|
|
6
|
+
worker died is handed out again, and on PostgreSQL a waiting worker is woken
|
|
7
|
+
the moment a message is sent.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
13
|
+
|
|
14
|
+
from .exception import InvalidOrmDsnError, OrmMessengerError, TransportError
|
|
15
|
+
from .transport import (
|
|
16
|
+
DEFAULT_QUEUE,
|
|
17
|
+
DEFAULT_TABLE_NAME,
|
|
18
|
+
ORM_OPTIONS,
|
|
19
|
+
ORM_SCHEME,
|
|
20
|
+
Connection,
|
|
21
|
+
MessagesTableDriver,
|
|
22
|
+
OrmOptions,
|
|
23
|
+
OrmReceivedStamp,
|
|
24
|
+
OrmReceiver,
|
|
25
|
+
OrmSender,
|
|
26
|
+
OrmTransport,
|
|
27
|
+
OrmTransportFactory,
|
|
28
|
+
StoredMessage,
|
|
29
|
+
messages_table,
|
|
30
|
+
messages_tables,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
__version__ = version("xtr-orm-messenger")
|
|
35
|
+
except PackageNotFoundError: # pragma: no cover
|
|
36
|
+
# Running from a source tree with no installed metadata to read; having no
|
|
37
|
+
# version is better than refusing to import.
|
|
38
|
+
__version__ = "0+unknown"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"DEFAULT_QUEUE",
|
|
42
|
+
"DEFAULT_TABLE_NAME",
|
|
43
|
+
"ORM_OPTIONS",
|
|
44
|
+
"ORM_SCHEME",
|
|
45
|
+
"Connection",
|
|
46
|
+
"InvalidOrmDsnError",
|
|
47
|
+
"MessagesTableDriver",
|
|
48
|
+
"OrmMessengerError",
|
|
49
|
+
"OrmOptions",
|
|
50
|
+
"OrmReceivedStamp",
|
|
51
|
+
"OrmReceiver",
|
|
52
|
+
"OrmSender",
|
|
53
|
+
"OrmTransport",
|
|
54
|
+
"OrmTransportFactory",
|
|
55
|
+
"StoredMessage",
|
|
56
|
+
"TransportError",
|
|
57
|
+
"__version__",
|
|
58
|
+
"messages_table",
|
|
59
|
+
"messages_tables",
|
|
60
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Everything this library raises, each one an :class:`OrmMessengerError`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .invalid_orm_dsn_error import InvalidOrmDsnError
|
|
6
|
+
from .orm_messenger_error import OrmMessengerError
|
|
7
|
+
from .transport_error import TransportError
|
|
8
|
+
|
|
9
|
+
__all__ = ["InvalidOrmDsnError", "OrmMessengerError", "TransportError"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""An ``orm://`` DSN names a connection the application does not configure."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .orm_messenger_error import OrmMessengerError
|
|
6
|
+
|
|
7
|
+
__all__ = ["InvalidOrmDsnError"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InvalidOrmDsnError(OrmMessengerError, LookupError):
|
|
11
|
+
"""An ``orm://`` DSN names a connection the application does not configure.
|
|
12
|
+
|
|
13
|
+
Also a :class:`LookupError`, as a missing key in any other mapping is.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
transport: The transport whose DSN names it.
|
|
17
|
+
connection: The connection named.
|
|
18
|
+
known: Every connection that is configured.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
transport: str
|
|
22
|
+
connection: str
|
|
23
|
+
known: tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
def __init__(self, transport: str, connection: str, known: tuple[str, ...]) -> None:
|
|
26
|
+
"""Record the transport, the connection it named and the ones that exist."""
|
|
27
|
+
self.transport = transport
|
|
28
|
+
self.connection = connection
|
|
29
|
+
self.known = known
|
|
30
|
+
names = ", ".join(f'"{each}"' for each in known) or "none"
|
|
31
|
+
super().__init__(
|
|
32
|
+
f'The "{transport}" transport names the connection "{connection}", which is not '
|
|
33
|
+
f"configured; the connections are: {names}."
|
|
34
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The root every error in this library derives from."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from xtr_messenger import MessageBusError
|
|
6
|
+
|
|
7
|
+
__all__ = ["OrmMessengerError"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OrmMessengerError(MessageBusError):
|
|
11
|
+
"""Base class for every error raised by this library.
|
|
12
|
+
|
|
13
|
+
Also a :class:`~xtr_messenger.MessageBusError`, so code catching what the
|
|
14
|
+
bus can go wrong with catches this transport's failures too.
|
|
15
|
+
"""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The database refused what a transport asked of it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .orm_messenger_error import OrmMessengerError
|
|
6
|
+
|
|
7
|
+
__all__ = ["TransportError"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TransportError(OrmMessengerError):
|
|
11
|
+
"""The database refused what a transport asked of it.
|
|
12
|
+
|
|
13
|
+
Raised from the driver's own error, which stays the cause, so nothing it
|
|
14
|
+
said is lost. The message names the transport and the operation, never the
|
|
15
|
+
connection's URL, whose credentials a traceback would otherwise carry.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
transport: The transport the operation was for.
|
|
19
|
+
operation: What was asked — ``send``, ``get``, ``ack``, ...
|
|
20
|
+
reason: What went wrong.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
transport: str
|
|
24
|
+
operation: str
|
|
25
|
+
reason: str
|
|
26
|
+
|
|
27
|
+
def __init__(self, transport: str, operation: str, reason: str) -> None:
|
|
28
|
+
"""Record which transport failed, at what, and why."""
|
|
29
|
+
self.transport = transport
|
|
30
|
+
self.operation = operation
|
|
31
|
+
self.reason = reason
|
|
32
|
+
super().__init__(f'The "{transport}" transport could not {operation}: {reason}.')
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""This directory holds the package's recipe: its manifest.
|
|
2
|
+
|
|
3
|
+
The recipe is what *Use in an application* in the README lists, written as
|
|
4
|
+
data: the steps only a person can take — naming an ``orm://`` transport in the
|
|
5
|
+
message bus configuration and routing messages to it. An application applies it
|
|
6
|
+
with ``xtr-recipes recipes:sync``, which finds this module through the
|
|
7
|
+
``xtr_recipes`` entry point.
|
|
8
|
+
|
|
9
|
+
Nothing is importable from here. The manifest is read as a file, so a recipe
|
|
10
|
+
is the same from a wheel as from a working tree.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# The recipe for xtr-orm-messenger: what *Use in an application* in the README
|
|
2
|
+
# takes, as data. Keep the two in step — the README is the source.
|
|
3
|
+
#
|
|
4
|
+
# No [bundles]: the package ships none — the orm bundle registers the transport
|
|
5
|
+
# factory when the messenger bundle is active too. No [files] and no [env]: a
|
|
6
|
+
# transport is named in the message bus configuration, which is the messenger's,
|
|
7
|
+
# and its DSN names a connection the orm already configures.
|
|
8
|
+
|
|
9
|
+
[gitignore]
|
|
10
|
+
# Nothing: the messages live in the database.
|
|
11
|
+
lines = []
|
|
12
|
+
|
|
13
|
+
[notes]
|
|
14
|
+
steps = [
|
|
15
|
+
"Name a transport orm://default in config/packages/messenger.py and route your messages to it: TransportConfig(\"orm://default\", queue=\"jobs\").",
|
|
16
|
+
"Run orm:migrations:diff: it writes the messages table of every orm:// transport on that connection. Add auto_setup=false to the DSN to leave the table to the migrations alone.",
|
|
17
|
+
]
|
|
18
|
+
check = [
|
|
19
|
+
"<script> debug:bundles — messenger and orm are listed and active",
|
|
20
|
+
]
|
|
21
|
+
run = ["<script> messenger:consume <transport>"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""The ``orm://`` transport: messages kept in a table on an xtr-orm connection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .connection import LISTING_PAGE_SIZE, Connection
|
|
6
|
+
from .messages_table import messages_table
|
|
7
|
+
from .messages_table_driver import MessagesTableDriver
|
|
8
|
+
from .messages_tables import messages_tables
|
|
9
|
+
from .orm_options import DEFAULT_QUEUE, DEFAULT_TABLE_NAME, ORM_OPTIONS, ORM_SCHEME, OrmOptions
|
|
10
|
+
from .orm_received_stamp import OrmReceivedStamp
|
|
11
|
+
from .orm_receiver import MAX_RETRIES, RECONNECT_TIMEOUT, OrmReceiver
|
|
12
|
+
from .orm_sender import OrmSender
|
|
13
|
+
from .orm_transport import OrmTransport
|
|
14
|
+
from .orm_transport_factory import OrmTransportFactory
|
|
15
|
+
from .postgresql_notifier import PostgreSqlNotifier
|
|
16
|
+
from .stored_message import StoredMessage
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DEFAULT_QUEUE",
|
|
20
|
+
"DEFAULT_TABLE_NAME",
|
|
21
|
+
"LISTING_PAGE_SIZE",
|
|
22
|
+
"MAX_RETRIES",
|
|
23
|
+
"ORM_OPTIONS",
|
|
24
|
+
"ORM_SCHEME",
|
|
25
|
+
"RECONNECT_TIMEOUT",
|
|
26
|
+
"Connection",
|
|
27
|
+
"MessagesTableDriver",
|
|
28
|
+
"OrmOptions",
|
|
29
|
+
"OrmReceivedStamp",
|
|
30
|
+
"OrmReceiver",
|
|
31
|
+
"OrmSender",
|
|
32
|
+
"OrmTransport",
|
|
33
|
+
"OrmTransportFactory",
|
|
34
|
+
"PostgreSqlNotifier",
|
|
35
|
+
"StoredMessage",
|
|
36
|
+
"messages_table",
|
|
37
|
+
"messages_tables",
|
|
38
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Telling apart what a driver's error means, whichever database raised it.
|
|
2
|
+
|
|
3
|
+
Each database reports a deadlock, a lock it gave up waiting for or a missing
|
|
4
|
+
table its own way — PostgreSQL with an SQLSTATE, MySQL with an error number,
|
|
5
|
+
SQLite only in its message — and the driver's error is all SQLAlchemy hands
|
|
6
|
+
back. Reading it in one place keeps every caller to one question.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Final
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from sqlalchemy.exc import DBAPIError
|
|
15
|
+
|
|
16
|
+
__all__ = ["is_missing_table", "is_retryable"]
|
|
17
|
+
|
|
18
|
+
_RETRYABLE_SQLSTATES: Final = frozenset({"40001", "40P01", "55P03"})
|
|
19
|
+
"""Serialization failure, deadlock, lock not available."""
|
|
20
|
+
|
|
21
|
+
_RETRYABLE_MYSQL_CODES: Final = frozenset({1205, 1213})
|
|
22
|
+
"""Lock wait timeout, deadlock."""
|
|
23
|
+
|
|
24
|
+
_RETRYABLE_MESSAGES: Final = ("database is locked", "database table is locked")
|
|
25
|
+
"""SQLite, whose errors carry no code a driver exposes."""
|
|
26
|
+
|
|
27
|
+
_MISSING_TABLE_SQLSTATE: Final = "42P01"
|
|
28
|
+
_MISSING_TABLE_MYSQL_CODE: Final = 1146
|
|
29
|
+
_MISSING_TABLE_MESSAGE: Final = "no such table"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_retryable(error: DBAPIError) -> bool:
|
|
33
|
+
"""Tell whether ``error`` is a conflict with another transaction, worth trying again.
|
|
34
|
+
|
|
35
|
+
Workers claiming messages from one table run into each other: a deadlock,
|
|
36
|
+
a lock they waited too long for. Neither says anything about the message,
|
|
37
|
+
so the same statement a moment later goes through.
|
|
38
|
+
"""
|
|
39
|
+
original = error.orig
|
|
40
|
+
return (
|
|
41
|
+
_sqlstate(original) in _RETRYABLE_SQLSTATES
|
|
42
|
+
or _mysql_code(original) in _RETRYABLE_MYSQL_CODES
|
|
43
|
+
or any(text in str(original) for text in _RETRYABLE_MESSAGES)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_missing_table(error: DBAPIError) -> bool:
|
|
48
|
+
"""Tell whether ``error`` says the table the statement names does not exist."""
|
|
49
|
+
original = error.orig
|
|
50
|
+
return (
|
|
51
|
+
_sqlstate(original) == _MISSING_TABLE_SQLSTATE
|
|
52
|
+
or _mysql_code(original) == _MISSING_TABLE_MYSQL_CODE
|
|
53
|
+
or _MISSING_TABLE_MESSAGE in str(original)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sqlstate(original: BaseException | None) -> str | None:
|
|
58
|
+
"""Return the SQLSTATE a PostgreSQL driver put on its error, if any."""
|
|
59
|
+
state: object = getattr(original, "sqlstate", None)
|
|
60
|
+
return state if isinstance(state, str) else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _mysql_code(original: BaseException | None) -> int | None:
|
|
64
|
+
"""Return the error number a MySQL driver put first in its error's arguments, if any."""
|
|
65
|
+
if original is None:
|
|
66
|
+
return None
|
|
67
|
+
arguments: tuple[object, ...] = original.args
|
|
68
|
+
if not arguments:
|
|
69
|
+
return None
|
|
70
|
+
first = arguments[0]
|
|
71
|
+
return first if isinstance(first, int) and not isinstance(first, bool) else None
|