mersal 0.1.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.
- mersal/__init__.py +1 -0
- mersal/_activation/builtin_handler_activator.py +102 -0
- mersal/_activation/handler_activator.py +78 -0
- mersal/activation/__init__.py +14 -0
- mersal/app.py +338 -0
- mersal/configuration/__init__.py +3 -0
- mersal/configuration/default_plugin.py +161 -0
- mersal/configuration/dependency_resolver.py +55 -0
- mersal/configuration/standard_configurator.py +67 -0
- mersal/exceptions/__init__.py +11 -0
- mersal/exceptions/base_exceptions.py +49 -0
- mersal/handlers/__init__.py +3 -0
- mersal/handlers/message_handler.py +6 -0
- mersal/idempotency/__init__.py +11 -0
- mersal/idempotency/config.py +25 -0
- mersal/idempotency/const.py +4 -0
- mersal/idempotency/idempotency_checker_step.py +38 -0
- mersal/idempotency/message_tracker.py +17 -0
- mersal/idempotency/plugin.py +35 -0
- mersal/lifespan/__init__.py +4 -0
- mersal/lifespan/autosubscribe/__init__.py +3 -0
- mersal/lifespan/autosubscribe/autosubscribe_plugin.py +54 -0
- mersal/lifespan/default_lifespan_handler.py +15 -0
- mersal/lifespan/lifespan_handler.py +15 -0
- mersal/lifespan/lifespan_hooks_registration_plugin.py +48 -0
- mersal/messages/__init__.py +7 -0
- mersal/messages/batch_message.py +8 -0
- mersal/messages/logical_message.py +18 -0
- mersal/messages/message_completed_event.py +9 -0
- mersal/messages/message_completed_event_publisher.py +31 -0
- mersal/messages/message_headers.py +27 -0
- mersal/messages/transport_message.py +18 -0
- mersal/outbox/__init__.py +12 -0
- mersal/outbox/config.py +13 -0
- mersal/outbox/in_memory/__init__.py +5 -0
- mersal/outbox/in_memory/in_memory_outbox_storage.py +44 -0
- mersal/outbox/outbox_forwarder.py +86 -0
- mersal/outbox/outbox_incoming_step.py +20 -0
- mersal/outbox/outbox_message.py +32 -0
- mersal/outbox/outbox_message_batch.py +33 -0
- mersal/outbox/outbox_storage.py +44 -0
- mersal/outbox/outbox_transport_decorator.py +54 -0
- mersal/outbox/plugin.py +77 -0
- mersal/persistence/in_memory/__init__.py +15 -0
- mersal/persistence/in_memory/in_memory_message_tracker.py +19 -0
- mersal/persistence/in_memory/in_memory_saga_storage.py +98 -0
- mersal/persistence/in_memory/in_memory_subscription_storage.py +58 -0
- mersal/persistence/not_implemented/__init__.py +3 -0
- mersal/persistence/not_implemented/not_implemented_subscription_storage.py +21 -0
- mersal/pipeline/__init__.py +42 -0
- mersal/pipeline/default_pipeline.py +41 -0
- mersal/pipeline/incoming_step.py +11 -0
- mersal/pipeline/incoming_step_context.py +20 -0
- mersal/pipeline/iterative_pipeline_invoker.py +44 -0
- mersal/pipeline/message_context.py +48 -0
- mersal/pipeline/outgoing_step.py +11 -0
- mersal/pipeline/outgoing_step_context.py +20 -0
- mersal/pipeline/pipeline.py +23 -0
- mersal/pipeline/pipeline_injector.py +68 -0
- mersal/pipeline/pipeline_invoker.py +10 -0
- mersal/pipeline/receive/activate_handlers_step.py +70 -0
- mersal/pipeline/receive/deserialize_incoming_message_step.py +21 -0
- mersal/pipeline/receive/dispatch_incoming_message_step.py +19 -0
- mersal/pipeline/receive/handler_invoker.py +32 -0
- mersal/pipeline/receive/handler_invokers.py +24 -0
- mersal/pipeline/receive/saga_handler_invoker.py +26 -0
- mersal/pipeline/recursive_pipeline_invoker.py +38 -0
- mersal/pipeline/send/destination_addresses.py +12 -0
- mersal/pipeline/send/flow_correlation_step.py +59 -0
- mersal/pipeline/send/send_outgoing_message_step.py +23 -0
- mersal/pipeline/send/serialize_outgoing_message_step.py +19 -0
- mersal/pipeline/send/set_default_headers_step.py +33 -0
- mersal/pipeline/step_context.py +27 -0
- mersal/plugins/__init__.py +7 -0
- mersal/plugins/generic_registration_plugin.py +13 -0
- mersal/plugins/plugin.py +9 -0
- mersal/py.typed +0 -0
- mersal/retry/__init__.py +27 -0
- mersal/retry/default_retry_step.py +91 -0
- mersal/retry/default_retry_strategy.py +29 -0
- mersal/retry/error_handling/deadletter_queue_error_handler.py +29 -0
- mersal/retry/error_handling/error_handler.py +15 -0
- mersal/retry/error_tracking/error_tracker.py +16 -0
- mersal/retry/error_tracking/in_memory_error_tracker.py +29 -0
- mersal/retry/fail_fast/default_fail_fast_checker.py +23 -0
- mersal/retry/fail_fast/fail_fast_checker.py +8 -0
- mersal/retry/retry_step.py +8 -0
- mersal/retry/retry_strategy.py +9 -0
- mersal/retry/retry_strategy_settings.py +9 -0
- mersal/routing/__init__.py +7 -0
- mersal/routing/default/__init__.py +9 -0
- mersal/routing/default/config.py +15 -0
- mersal/routing/default/default_router.py +32 -0
- mersal/routing/default/plugin.py +29 -0
- mersal/routing/router.py +9 -0
- mersal/sagas/__init__.py +13 -0
- mersal/sagas/config.py +17 -0
- mersal/sagas/correlation_error_handler.py +21 -0
- mersal/sagas/correlation_property.py +15 -0
- mersal/sagas/correlator.py +32 -0
- mersal/sagas/default_correlation_error_handler.py +29 -0
- mersal/sagas/load_saga_data_step.py +134 -0
- mersal/sagas/plugin.py +55 -0
- mersal/sagas/saga.py +61 -0
- mersal/sagas/saga_data.py +15 -0
- mersal/sagas/saga_storage.py +41 -0
- mersal/serialization/__init__.py +9 -0
- mersal/serialization/identity_serializer/__init__.py +3 -0
- mersal/serialization/identity_serializer/identity_object_serializer.py +13 -0
- mersal/serialization/message_serializer.py +17 -0
- mersal/serialization/serializers.py +13 -0
- mersal/subscription/__init__.py +3 -0
- mersal/subscription/subscription_storage.py +14 -0
- mersal/threading/__init__.py +13 -0
- mersal/threading/anyio/anyio_periodic_async_task.py +49 -0
- mersal/threading/anyio/anyio_periodic_async_task_factory.py +22 -0
- mersal/threading/periodic_async_task.py +9 -0
- mersal/threading/periodic_async_task_factory.py +11 -0
- mersal/topic/__init__.py +4 -0
- mersal/topic/default_topic_name_convention.py +6 -0
- mersal/topic/topic_name_convention.py +7 -0
- mersal/transport/__init__.py +19 -0
- mersal/transport/ambient_context.py +20 -0
- mersal/transport/base_transport.py +40 -0
- mersal/transport/default_transaction_context.py +158 -0
- mersal/transport/default_transaction_context_with_app.py +16 -0
- mersal/transport/in_memory/__init__.py +10 -0
- mersal/transport/in_memory/in_memory_network.py +32 -0
- mersal/transport/in_memory/in_memory_transport.py +60 -0
- mersal/transport/in_memory/in_memory_transport_plugin.py +49 -0
- mersal/transport/outgoing_message.py +15 -0
- mersal/transport/transaction_context.py +32 -0
- mersal/transport/transaction_scope.py +46 -0
- mersal/transport/transport.py +21 -0
- mersal/transport/transport_bridge.py +46 -0
- mersal/transport/transport_decorator_plugin.py +23 -0
- mersal/types/__init__.py +22 -0
- mersal/types/callable_types.py +13 -0
- mersal/types/helper_types.py +6 -0
- mersal/unit_of_work/__init__.py +17 -0
- mersal/unit_of_work/config.py +49 -0
- mersal/unit_of_work/plugin.py +37 -0
- mersal/unit_of_work/unit_of_work_step.py +86 -0
- mersal/utils/__init__.py +9 -0
- mersal/utils/predicates.py +28 -0
- mersal/utils/retrier.py +24 -0
- mersal/utils/singleton.py +17 -0
- mersal/utils/sync.py +73 -0
- mersal/workers/__init__.py +3 -0
- mersal/workers/anyio/__init__.py +4 -0
- mersal/workers/anyio/anyio_worker.py +117 -0
- mersal/workers/anyio/anyio_worker_factory.py +34 -0
- mersal/workers/worker.py +23 -0
- mersal/workers/worker_factory.py +16 -0
- mersal-0.1.0.dist-info/METADATA +13 -0
- mersal-0.1.0.dist-info/RECORD +187 -0
- mersal-0.1.0.dist-info/WHEEL +4 -0
- mersal_testing/_internal/conftest.py +70 -0
- mersal_testing/_internal/docker-compose.yml +17 -0
- mersal_testing/app_runner_helper.py +22 -0
- mersal_testing/counter.py +41 -0
- mersal_testing/message_handlers/__init__.py +9 -0
- mersal_testing/message_handlers/message_handler_that_counts.py +14 -0
- mersal_testing/message_handlers/message_handler_that_sends_local_messages.py +32 -0
- mersal_testing/message_handlers/message_handler_that_stores_the_message.py +15 -0
- mersal_testing/message_handlers/message_handler_that_throws.py +13 -0
- mersal_testing/message_handlers/null_message_handler.py +10 -0
- mersal_testing/messages.py +12 -0
- mersal_testing/pipeline/pipeline_invoker_tests.py +137 -0
- mersal_testing/py.typed +2 -0
- mersal_testing/retry/error_tracker_base_tests.py +89 -0
- mersal_testing/test_doubles/__init__.py +31 -0
- mersal_testing/test_doubles/messages/__init__.py +0 -0
- mersal_testing/test_doubles/messages/logical_message_builder.py +52 -0
- mersal_testing/test_doubles/messages/transport_message_builder.py +19 -0
- mersal_testing/test_doubles/outbox/outbox_storage_test_double.py +61 -0
- mersal_testing/test_doubles/retry/error_handler_spy.py +20 -0
- mersal_testing/test_doubles/retry/error_tracker_test_double.py +28 -0
- mersal_testing/test_doubles/serialization/serializer_test_double.py +21 -0
- mersal_testing/test_doubles/transport/outgoing_message_builder.py +10 -0
- mersal_testing/test_doubles/transport/transaction_context_test_double.py +49 -0
- mersal_testing/test_doubles/transport/transport_spy.py +26 -0
- mersal_testing/test_doubles/transport/transport_test_double.py +25 -0
- mersal_testing/test_doubles/unit_of_work/unit_of_work_test_helper.py +24 -0
- mersal_testing/testing_utils.py +11 -0
- mersal_testing/transport/basic_transport_tests.py +170 -0
- mersal_testing/transport/transport_decorator_helper.py +36 -0
mersal/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import TYPE_CHECKING, TypeVar, cast
|
|
5
|
+
|
|
6
|
+
from mersal.pipeline import MessageContext
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from mersal._activation.handler_activator import HandlerFactory
|
|
12
|
+
from mersal.app import Mersal
|
|
13
|
+
from mersal.handlers import MessageHandler
|
|
14
|
+
from mersal.transport import TransactionContext
|
|
15
|
+
|
|
16
|
+
# Define locally to avoid import cycle with handler_activator.py
|
|
17
|
+
MessageT = TypeVar("MessageT")
|
|
18
|
+
|
|
19
|
+
__all__ = ("BuiltinHandlerActivator",)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BuiltinHandlerActivator:
|
|
23
|
+
"""Default implementation of the handler activator pattern.
|
|
24
|
+
|
|
25
|
+
This class manages the registration and activation of message handlers
|
|
26
|
+
based on message types. It stores handler factories indexed by message type
|
|
27
|
+
and instantiates the appropriate handlers when a message needs to be processed.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
"""Initialize a new instance of the BuiltinHandlerActivator."""
|
|
32
|
+
self._handler_factories: dict[type, list[HandlerFactory]] = defaultdict(list)
|
|
33
|
+
self._app: Mersal | None = None
|
|
34
|
+
|
|
35
|
+
async def get_handlers(
|
|
36
|
+
self,
|
|
37
|
+
message: MessageT,
|
|
38
|
+
transaction_context: TransactionContext,
|
|
39
|
+
) -> Sequence[MessageHandler[MessageT]]:
|
|
40
|
+
"""Get handlers for the specified message.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
message: The message to get handlers for
|
|
44
|
+
transaction_context: The current transaction context
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A sequence of message handlers that can process the message
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
Exception: If called outside of a message context
|
|
51
|
+
"""
|
|
52
|
+
message_context = MessageContext.current()
|
|
53
|
+
if not message_context:
|
|
54
|
+
raise Exception(
|
|
55
|
+
"BuiltinHandlerActivator get_handlers called outside of a transaction.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return [x(message_context, self.app) for x in self._handler_factories.get(type(message), [])]
|
|
59
|
+
|
|
60
|
+
def register(
|
|
61
|
+
self,
|
|
62
|
+
message_type: type[MessageT],
|
|
63
|
+
factory: HandlerFactory[MessageT],
|
|
64
|
+
) -> BuiltinHandlerActivator:
|
|
65
|
+
"""Register a handler factory for a specific message type.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
message_type: The type of message to register a handler for
|
|
69
|
+
factory: A factory function that creates a message handler
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The handler activator instance for method chaining
|
|
73
|
+
"""
|
|
74
|
+
self._handler_factories[message_type].append(factory)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def registered_message_types(self) -> set[type]:
|
|
79
|
+
"""Get the set of message types that have registered handlers.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A set containing all registered message types
|
|
83
|
+
"""
|
|
84
|
+
return set(self._handler_factories.keys())
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def app(self) -> Mersal:
|
|
88
|
+
"""Get the associated application instance.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The Mersal application instance
|
|
92
|
+
"""
|
|
93
|
+
return cast("Mersal", self._app)
|
|
94
|
+
|
|
95
|
+
@app.setter
|
|
96
|
+
def app(self, value: Mersal) -> None:
|
|
97
|
+
"""Set the associated application instance.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
value: The Mersal application instance
|
|
101
|
+
"""
|
|
102
|
+
self._app = value
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from typing import TYPE_CHECKING, Protocol, TypeAlias, TypeVar
|
|
5
|
+
|
|
6
|
+
from mersal.handlers import MessageHandler
|
|
7
|
+
from mersal.pipeline import MessageContext
|
|
8
|
+
|
|
9
|
+
# Define a type variable for messages
|
|
10
|
+
MessageT = TypeVar("MessageT")
|
|
11
|
+
|
|
12
|
+
# A factory function that creates message handlers
|
|
13
|
+
HandlerFactory: TypeAlias = Callable[
|
|
14
|
+
[MessageContext, "Mersal"],
|
|
15
|
+
MessageHandler[MessageT],
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from mersal.app import Mersal
|
|
20
|
+
from mersal.transport import TransactionContext
|
|
21
|
+
|
|
22
|
+
__all__ = ("HandlerActivator",)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HandlerActivator(Protocol):
|
|
26
|
+
"""Protocol defining the interface for activating message handlers.
|
|
27
|
+
|
|
28
|
+
The HandlerActivator is responsible for registering message handlers
|
|
29
|
+
and returning the appropriate handlers for a given message type during
|
|
30
|
+
message processing.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
async def get_handlers(
|
|
34
|
+
self,
|
|
35
|
+
message: MessageT,
|
|
36
|
+
transaction_context: TransactionContext,
|
|
37
|
+
) -> Sequence[MessageHandler[MessageT]]:
|
|
38
|
+
"""Retrieve handlers for the given message.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
message: The message to get handlers for
|
|
42
|
+
transaction_context: The current transaction context
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A sequence of message handlers that can process the message
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def register(
|
|
50
|
+
self,
|
|
51
|
+
message_type: type[MessageT],
|
|
52
|
+
factory: HandlerFactory[MessageT],
|
|
53
|
+
) -> HandlerActivator:
|
|
54
|
+
"""Register a handler factory for a specific message type.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
message_type: The type of message to register a handler for
|
|
58
|
+
factory: A factory function that creates a message handler
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The handler activator instance for method chaining
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def registered_message_types(self) -> set[type]:
|
|
67
|
+
"""Get the set of message types that have registered handlers.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A set containing all registered message types
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def app(self) -> Mersal: ...
|
|
76
|
+
|
|
77
|
+
@app.setter
|
|
78
|
+
def app(self, value: Mersal) -> None: ...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Message handler activation module.
|
|
2
|
+
|
|
3
|
+
This module provides classes for registering and activating message handlers
|
|
4
|
+
based on message types. The handler activator pattern is core to how Mersal
|
|
5
|
+
dispatches messages to the appropriate handlers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from mersal._activation.builtin_handler_activator import BuiltinHandlerActivator
|
|
9
|
+
from mersal._activation.handler_activator import HandlerActivator
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"BuiltinHandlerActivator",
|
|
13
|
+
"HandlerActivator",
|
|
14
|
+
)
|
mersal/app.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Mapping, Sequence
|
|
3
|
+
from contextlib import AsyncExitStack
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from mersal.activation import HandlerActivator
|
|
9
|
+
from mersal.configuration.default_plugin import DefaultPlugin
|
|
10
|
+
from mersal.configuration.standard_configurator import (
|
|
11
|
+
InvalidConfigurationError,
|
|
12
|
+
StandardConfigurator,
|
|
13
|
+
)
|
|
14
|
+
from mersal.idempotency import IdempotencyConfig, IdempotencyPlugin
|
|
15
|
+
from mersal.lifespan import LifespanHandler
|
|
16
|
+
from mersal.lifespan.autosubscribe import AutosubscribeConfig
|
|
17
|
+
from mersal.messages import LogicalMessage, MessageHeaders
|
|
18
|
+
from mersal.outbox.config import OutboxConfig
|
|
19
|
+
from mersal.outbox.plugin import OutboxPlugin
|
|
20
|
+
from mersal.persistence.in_memory import InMemorySagaStorage
|
|
21
|
+
from mersal.pipeline import (
|
|
22
|
+
DestinationAddresses,
|
|
23
|
+
MessageIdGenerator,
|
|
24
|
+
OutgoingStepContext,
|
|
25
|
+
PipelineInvoker,
|
|
26
|
+
)
|
|
27
|
+
from mersal.plugins import Plugin, generic_registration_plugin
|
|
28
|
+
from mersal.retry import (
|
|
29
|
+
ErrorHandler,
|
|
30
|
+
ErrorTracker,
|
|
31
|
+
FailFastChecker,
|
|
32
|
+
RetryStrategySettings,
|
|
33
|
+
)
|
|
34
|
+
from mersal.retry.fail_fast.default_fail_fast_checker import (
|
|
35
|
+
DefaultFailFastCheckerExceptionsContainer,
|
|
36
|
+
)
|
|
37
|
+
from mersal.routing import Router
|
|
38
|
+
from mersal.routing.default import DefaultRouterRegistrationConfig
|
|
39
|
+
from mersal.routing.default.plugin import DefaultRouterRegistrationPlugin
|
|
40
|
+
from mersal.sagas import SagaConfig
|
|
41
|
+
from mersal.sagas.plugin import SagaPlugin
|
|
42
|
+
from mersal.serialization import (
|
|
43
|
+
MessageBodySerializer,
|
|
44
|
+
MessageHeadersSerializer,
|
|
45
|
+
Serializer,
|
|
46
|
+
)
|
|
47
|
+
from mersal.subscription import SubscriptionStorage
|
|
48
|
+
from mersal.topic import TopicNameConvention
|
|
49
|
+
from mersal.transport import (
|
|
50
|
+
AmbientContext,
|
|
51
|
+
DefaultTransactionContextWithOwningApp,
|
|
52
|
+
TransactionContext,
|
|
53
|
+
Transport,
|
|
54
|
+
)
|
|
55
|
+
from mersal.types import Empty, EmptyType, LifespanHook
|
|
56
|
+
from mersal.unit_of_work import UnitOfWorkConfig
|
|
57
|
+
from mersal.unit_of_work.plugin import UnitOfWorkPlugin
|
|
58
|
+
from mersal.utils.sync import AsyncCallable
|
|
59
|
+
from mersal.workers import WorkerFactory
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from mersal.workers.worker import Worker
|
|
63
|
+
|
|
64
|
+
__all__ = ("Mersal",)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Mersal:
|
|
68
|
+
def __init__( # noqa: C901
|
|
69
|
+
self,
|
|
70
|
+
name: str,
|
|
71
|
+
handler_activator: HandlerActivator,
|
|
72
|
+
transport: Transport | None = None,
|
|
73
|
+
pipeline_invoker: PipelineInvoker | None = None,
|
|
74
|
+
router: Router | None = None,
|
|
75
|
+
worker_factory: WorkerFactory | None = None,
|
|
76
|
+
subscription_storage: SubscriptionStorage | None = None,
|
|
77
|
+
topic_name_convention: TopicNameConvention | None = None,
|
|
78
|
+
on_startup_hooks: Sequence[LifespanHook] | None = None,
|
|
79
|
+
on_shutdown_hooks: Sequence[LifespanHook] | None = None,
|
|
80
|
+
retry_strategy_settings: RetryStrategySettings | None = None,
|
|
81
|
+
error_tracker: ErrorTracker | None = None,
|
|
82
|
+
error_handler: ErrorHandler | None = None,
|
|
83
|
+
fail_fast_checker: FailFastChecker | None = None,
|
|
84
|
+
fail_fast_exceptions: Sequence[type[Exception]] | None = None,
|
|
85
|
+
plugins: Sequence[Plugin] | None = None,
|
|
86
|
+
idempotency: IdempotencyConfig | None = None,
|
|
87
|
+
saga: SagaConfig | EmptyType | None = None,
|
|
88
|
+
serializer: Serializer | None = None,
|
|
89
|
+
message_body_serializer: MessageBodySerializer | None = None,
|
|
90
|
+
message_headers_serializer: MessageHeadersSerializer | None = None,
|
|
91
|
+
default_router_registration: DefaultRouterRegistrationConfig | None = None,
|
|
92
|
+
autosubscribe: AutosubscribeConfig | EmptyType | None = None,
|
|
93
|
+
unit_of_work: UnitOfWorkConfig | None = None,
|
|
94
|
+
outbox: OutboxConfig | None = None,
|
|
95
|
+
pdb_on_exception: bool | None = None,
|
|
96
|
+
message_id_generator: MessageIdGenerator | None = None,
|
|
97
|
+
):
|
|
98
|
+
if router and default_router_registration:
|
|
99
|
+
raise InvalidConfigurationError("Cannot provide both router and default_router_registration")
|
|
100
|
+
|
|
101
|
+
if fail_fast_checker and fail_fast_exceptions:
|
|
102
|
+
raise InvalidConfigurationError("Cannot provide both fail_fast_exceptions and fail_fast_checker")
|
|
103
|
+
self.name = name
|
|
104
|
+
self.handler_activator = handler_activator
|
|
105
|
+
self.configurator = StandardConfigurator()
|
|
106
|
+
|
|
107
|
+
plugins = list(plugins or [])
|
|
108
|
+
plugins.append(generic_registration_plugin(handler_activator, HandlerActivator))
|
|
109
|
+
if transport is not None:
|
|
110
|
+
plugins.append(generic_registration_plugin(transport, Transport))
|
|
111
|
+
|
|
112
|
+
if pipeline_invoker is not None:
|
|
113
|
+
plugins.append(generic_registration_plugin(pipeline_invoker, PipelineInvoker))
|
|
114
|
+
if router is not None:
|
|
115
|
+
plugins.append(generic_registration_plugin(router, Router))
|
|
116
|
+
if worker_factory is not None:
|
|
117
|
+
plugins.append(generic_registration_plugin(worker_factory, WorkerFactory))
|
|
118
|
+
if subscription_storage is not None:
|
|
119
|
+
plugins.append(generic_registration_plugin(subscription_storage, SubscriptionStorage))
|
|
120
|
+
if topic_name_convention is not None:
|
|
121
|
+
plugins.append(generic_registration_plugin(topic_name_convention, TopicNameConvention))
|
|
122
|
+
if retry_strategy_settings is not None:
|
|
123
|
+
plugins.append(generic_registration_plugin(retry_strategy_settings, RetryStrategySettings))
|
|
124
|
+
if error_tracker is not None:
|
|
125
|
+
plugins.append(generic_registration_plugin(error_tracker, ErrorTracker))
|
|
126
|
+
if error_handler is not None:
|
|
127
|
+
plugins.append(generic_registration_plugin(error_handler, ErrorHandler))
|
|
128
|
+
if fail_fast_checker is not None:
|
|
129
|
+
plugins.append(generic_registration_plugin(fail_fast_checker, FailFastChecker))
|
|
130
|
+
if fail_fast_exceptions is not None:
|
|
131
|
+
plugins.append(
|
|
132
|
+
generic_registration_plugin(
|
|
133
|
+
DefaultFailFastCheckerExceptionsContainer(exceptions=fail_fast_exceptions),
|
|
134
|
+
DefaultFailFastCheckerExceptionsContainer,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if serializer is not None:
|
|
139
|
+
plugins.append(generic_registration_plugin(serializer, Serializer))
|
|
140
|
+
if message_body_serializer is not None:
|
|
141
|
+
plugins.append(generic_registration_plugin(message_body_serializer, MessageBodySerializer))
|
|
142
|
+
if message_body_serializer is not None:
|
|
143
|
+
plugins.append(generic_registration_plugin(message_body_serializer, MessageHeadersSerializer))
|
|
144
|
+
if idempotency is not None:
|
|
145
|
+
plugins.append(IdempotencyPlugin(idempotency))
|
|
146
|
+
|
|
147
|
+
if saga is not None and saga is not Empty:
|
|
148
|
+
plugins.append(SagaPlugin(saga)) # type: ignore[arg-type]
|
|
149
|
+
elif saga is not Empty:
|
|
150
|
+
plugins.append(SagaPlugin(SagaConfig(storage=InMemorySagaStorage())))
|
|
151
|
+
|
|
152
|
+
if default_router_registration:
|
|
153
|
+
plugins.append(DefaultRouterRegistrationPlugin(default_router_registration))
|
|
154
|
+
|
|
155
|
+
if isinstance(autosubscribe, AutosubscribeConfig):
|
|
156
|
+
autosubscribe_plugin = autosubscribe.plugin
|
|
157
|
+
plugins.append(autosubscribe_plugin)
|
|
158
|
+
elif autosubscribe is not Empty and self.configurator.is_registered(SubscriptionStorage):
|
|
159
|
+
plugins.append(AutosubscribeConfig().plugin)
|
|
160
|
+
|
|
161
|
+
if unit_of_work is not None:
|
|
162
|
+
plugins.append(UnitOfWorkPlugin(unit_of_work))
|
|
163
|
+
|
|
164
|
+
if outbox is not None:
|
|
165
|
+
plugins.append(OutboxPlugin(outbox))
|
|
166
|
+
if message_id_generator is not None:
|
|
167
|
+
plugins.append(generic_registration_plugin(message_id_generator, MessageIdGenerator))
|
|
168
|
+
|
|
169
|
+
self.on_startup_hooks = list(on_startup_hooks or [])
|
|
170
|
+
self.on_shutdown_hooks = list(on_shutdown_hooks or [])
|
|
171
|
+
|
|
172
|
+
self.handler_activator.app = self
|
|
173
|
+
self.configurator.mersal = self
|
|
174
|
+
plugins.append(DefaultPlugin(pdb_on_exception=bool(pdb_on_exception)))
|
|
175
|
+
for plugin in plugins:
|
|
176
|
+
plugin(self.configurator)
|
|
177
|
+
|
|
178
|
+
lifespan_handler = self.configurator.get(LifespanHandler) # type: ignore[type-abstract]
|
|
179
|
+
self.on_startup_hooks.extend(lifespan_handler.on_startup_hooks)
|
|
180
|
+
self.on_shutdown_hooks.extend(lifespan_handler.on_shutdown_hooks)
|
|
181
|
+
|
|
182
|
+
self.configurator.resolve()
|
|
183
|
+
self.router = self.configurator.get(Router) # type: ignore[type-abstract]
|
|
184
|
+
self.logger = logging.getLogger("mersal")
|
|
185
|
+
self.transport = self.configurator.get(Transport) # type: ignore[type-abstract]
|
|
186
|
+
self.worker_factory = self.configurator.get(WorkerFactory) # type: ignore[type-abstract]
|
|
187
|
+
self.worker_factory.app = self
|
|
188
|
+
self.subscription_storage = self.configurator.get(SubscriptionStorage) # type: ignore[type-abstract]
|
|
189
|
+
self.topic_name_convention = self.configurator.get(TopicNameConvention) # type: ignore[type-abstract]
|
|
190
|
+
self.pipeline_invoker = self.configurator.get(PipelineInvoker) # type: ignore[type-abstract]
|
|
191
|
+
self.worker: Worker
|
|
192
|
+
self._create_worker()
|
|
193
|
+
self._exit_stack: AsyncExitStack | None = None
|
|
194
|
+
|
|
195
|
+
async def start(self) -> None:
|
|
196
|
+
for hook in self.on_startup_hooks:
|
|
197
|
+
await AsyncCallable(hook)()
|
|
198
|
+
self._exit_stack = AsyncExitStack()
|
|
199
|
+
await self._exit_stack.enter_async_context(self.worker)
|
|
200
|
+
|
|
201
|
+
async def stop(self) -> None:
|
|
202
|
+
if self._exit_stack:
|
|
203
|
+
await self._exit_stack.aclose()
|
|
204
|
+
|
|
205
|
+
self._exit_stack = None
|
|
206
|
+
for hook in self.on_shutdown_hooks:
|
|
207
|
+
await AsyncCallable(hook)()
|
|
208
|
+
|
|
209
|
+
async def __aenter__(self) -> Self:
|
|
210
|
+
await self.start()
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
async def __aexit__(
|
|
214
|
+
self,
|
|
215
|
+
exc_type: type[BaseException] | None,
|
|
216
|
+
exc_val: BaseException | None,
|
|
217
|
+
exc_tb: Any | None,
|
|
218
|
+
) -> None:
|
|
219
|
+
await self.stop()
|
|
220
|
+
|
|
221
|
+
async def send(
|
|
222
|
+
self,
|
|
223
|
+
command_message: Any,
|
|
224
|
+
headers: Mapping[str, Any] | None = None,
|
|
225
|
+
) -> None:
|
|
226
|
+
logical_message = self._create_message(command_message, headers)
|
|
227
|
+
|
|
228
|
+
destination_address = await self.router.get_destination_address(logical_message)
|
|
229
|
+
self.logger.info(
|
|
230
|
+
"The app named '%r' is about to send %r to %r",
|
|
231
|
+
self.name,
|
|
232
|
+
type(command_message),
|
|
233
|
+
destination_address,
|
|
234
|
+
)
|
|
235
|
+
addresses = [destination_address]
|
|
236
|
+
await self._send(set(addresses), logical_message)
|
|
237
|
+
|
|
238
|
+
async def send_local(
|
|
239
|
+
self,
|
|
240
|
+
command_message: Any,
|
|
241
|
+
headers: Mapping[str, Any] | None = None,
|
|
242
|
+
) -> None:
|
|
243
|
+
self.logger.info(
|
|
244
|
+
"The app named '%r' is about to send %r locally",
|
|
245
|
+
self.name,
|
|
246
|
+
type(command_message),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
destination_address = self.transport.address
|
|
250
|
+
logical_message = self._create_message(command_message, headers)
|
|
251
|
+
|
|
252
|
+
addresses = [destination_address]
|
|
253
|
+
await self._send(set(addresses), logical_message)
|
|
254
|
+
|
|
255
|
+
async def publish(self, event_message: Any, headers: Mapping[str, Any] | None = None) -> None:
|
|
256
|
+
topic = self.topic_name_convention.get_topic_name(type(event_message))
|
|
257
|
+
|
|
258
|
+
await self._inner_publish(topic, event_message, headers)
|
|
259
|
+
|
|
260
|
+
async def subscribe(self, event_type: type) -> None:
|
|
261
|
+
topic = self.topic_name_convention.get_topic_name(event_type)
|
|
262
|
+
await self._subscribe(topic)
|
|
263
|
+
|
|
264
|
+
async def _send(
|
|
265
|
+
self,
|
|
266
|
+
destination_addresses: set[str],
|
|
267
|
+
logical_message: LogicalMessage,
|
|
268
|
+
) -> None:
|
|
269
|
+
transaction_context = self._get_transaction_context()
|
|
270
|
+
|
|
271
|
+
if not transaction_context:
|
|
272
|
+
async with DefaultTransactionContextWithOwningApp(self) as transaction_context:
|
|
273
|
+
await self._invoke_send(destination_addresses, logical_message, transaction_context)
|
|
274
|
+
transaction_context.set_result(commit=True, ack=True)
|
|
275
|
+
try:
|
|
276
|
+
await transaction_context.complete()
|
|
277
|
+
except: # noqa: E722
|
|
278
|
+
self.logger.exception(
|
|
279
|
+
"Exception while trying to complete the transaction context when sending %r",
|
|
280
|
+
logical_message.message_label,
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
await self._invoke_send(destination_addresses, logical_message, transaction_context)
|
|
284
|
+
|
|
285
|
+
def _get_transaction_context(self) -> TransactionContext | None:
|
|
286
|
+
ambient_transaction_context = AmbientContext().current
|
|
287
|
+
|
|
288
|
+
if not ambient_transaction_context:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
not isinstance(ambient_transaction_context, DefaultTransactionContextWithOwningApp)
|
|
293
|
+
or ambient_transaction_context.app is not self
|
|
294
|
+
):
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
return ambient_transaction_context
|
|
298
|
+
|
|
299
|
+
async def _invoke_send(
|
|
300
|
+
self,
|
|
301
|
+
destination_addresses: set[str],
|
|
302
|
+
logical_message: LogicalMessage,
|
|
303
|
+
transaction_context: TransactionContext,
|
|
304
|
+
) -> None:
|
|
305
|
+
self.logger.info(
|
|
306
|
+
"The app named '%r' is about to send %r with id: %r to destination addresses %r",
|
|
307
|
+
self.name,
|
|
308
|
+
str(type(logical_message.body)),
|
|
309
|
+
str(logical_message.headers.get("message_id", "N/A")),
|
|
310
|
+
str(destination_addresses),
|
|
311
|
+
)
|
|
312
|
+
context = OutgoingStepContext(
|
|
313
|
+
logical_message,
|
|
314
|
+
transaction_context,
|
|
315
|
+
DestinationAddresses(destination_addresses),
|
|
316
|
+
)
|
|
317
|
+
await self.pipeline_invoker(context)
|
|
318
|
+
|
|
319
|
+
async def _inner_publish(self, topic: str, event_message: Any, headers: Mapping[str, Any] | None) -> None:
|
|
320
|
+
message = self._create_message(event_message, headers)
|
|
321
|
+
subscriber_addresses = await self.subscription_storage.get_subscriber_addresses(topic)
|
|
322
|
+
await self._send(subscriber_addresses, message)
|
|
323
|
+
|
|
324
|
+
async def _subscribe(self, topic: str) -> None:
|
|
325
|
+
subscriber_address = self.transport.address
|
|
326
|
+
if self.subscription_storage.is_centralized:
|
|
327
|
+
await self.subscription_storage.register_subscriber(topic, subscriber_address)
|
|
328
|
+
else:
|
|
329
|
+
raise NotImplementedError("Can't handle transports that handle pub/sub natively yet")
|
|
330
|
+
|
|
331
|
+
def _create_message(self, body: Any, headers: Mapping[str, Any] | None) -> LogicalMessage:
|
|
332
|
+
_headers = MessageHeaders(headers) if headers else MessageHeaders()
|
|
333
|
+
|
|
334
|
+
return LogicalMessage(body, _headers)
|
|
335
|
+
|
|
336
|
+
def _create_worker(self) -> None:
|
|
337
|
+
self.logger.info("The app named '%r' is creating a worker", self.name)
|
|
338
|
+
self.worker = self.worker_factory.create_worker(self.name)
|