jararaca 0.3.12a4__tar.gz → 0.3.12a5__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.

Potentially problematic release.


This version of jararaca might be problematic. Click here for more details.

Files changed (86) hide show
  1. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/PKG-INFO +1 -1
  2. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/pyproject.toml +1 -1
  3. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/__init__.py +10 -4
  4. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/worker.py +102 -84
  5. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/microservice.py +42 -0
  6. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/server.py +57 -11
  7. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/LICENSE +0 -0
  8. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/README.md +0 -0
  9. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/CNAME +0 -0
  10. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/architecture.md +0 -0
  11. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.jpeg +0 -0
  12. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.webp +0 -0
  13. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/assets/tracing_example.png +0 -0
  14. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/http-rpc.md +0 -0
  15. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/index.md +0 -0
  16. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/interceptors.md +0 -0
  17. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/messagebus.md +0 -0
  18. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/retry.md +0 -0
  19. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/scheduler.md +0 -0
  20. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/stylesheets/custom.css +0 -0
  21. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/docs/websocket.md +0 -0
  22. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/__main__.py +0 -0
  23. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/broker_backend/__init__.py +0 -0
  24. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/broker_backend/mapper.py +0 -0
  25. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/broker_backend/redis_broker_backend.py +0 -0
  26. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/cli.py +0 -0
  27. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/common/__init__.py +0 -0
  28. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/core/__init__.py +0 -0
  29. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/core/providers.py +0 -0
  30. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/core/uow.py +0 -0
  31. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/di.py +0 -0
  32. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/files/entity.py.mako +0 -0
  33. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/lifecycle.py +0 -0
  34. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/__init__.py +0 -0
  35. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/bus_message_controller.py +0 -0
  36. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/consumers/__init__.py +0 -0
  37. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/decorators.py +0 -0
  38. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/interceptors/__init__.py +0 -0
  39. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +0 -0
  40. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/interceptors/publisher_interceptor.py +0 -0
  41. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/message.py +0 -0
  42. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/messagebus/publisher.py +0 -0
  43. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/observability/decorators.py +0 -0
  44. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/observability/interceptor.py +0 -0
  45. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/observability/providers/__init__.py +0 -0
  46. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/observability/providers/otel.py +0 -0
  47. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/persistence/base.py +0 -0
  48. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/persistence/exports.py +0 -0
  49. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/persistence/interceptors/__init__.py +0 -0
  50. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/persistence/interceptors/aiosqa_interceptor.py +0 -0
  51. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/persistence/session.py +0 -0
  52. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/persistence/sort_filter.py +0 -0
  53. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/persistence/utilities.py +0 -0
  54. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/__init__.py +0 -0
  55. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/decorators.py +0 -0
  56. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/hooks.py +0 -0
  57. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/http_microservice.py +0 -0
  58. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/websocket/__init__.py +0 -0
  59. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/websocket/base_types.py +0 -0
  60. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/websocket/context.py +0 -0
  61. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/websocket/decorators.py +0 -0
  62. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/websocket/redis.py +0 -0
  63. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/websocket/types.py +0 -0
  64. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/presentation/websocket/websocket_interceptor.py +0 -0
  65. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/py.typed +0 -0
  66. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/reflect/__init__.py +0 -0
  67. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/reflect/controller_inspect.py +0 -0
  68. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/reflect/metadata.py +0 -0
  69. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/rpc/__init__.py +0 -0
  70. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/rpc/http/__init__.py +0 -0
  71. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/rpc/http/backends/__init__.py +0 -0
  72. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/rpc/http/backends/httpx.py +0 -0
  73. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/rpc/http/backends/otel.py +0 -0
  74. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/rpc/http/decorators.py +0 -0
  75. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/rpc/http/httpx.py +0 -0
  76. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/scheduler/__init__.py +0 -0
  77. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/scheduler/beat_worker.py +0 -0
  78. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/scheduler/decorators.py +0 -0
  79. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/scheduler/types.py +0 -0
  80. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/tools/app_config/__init__.py +0 -0
  81. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/tools/app_config/decorators.py +0 -0
  82. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/tools/app_config/interceptor.py +0 -0
  83. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/tools/typescript/interface_parser.py +0 -0
  84. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/utils/__init__.py +0 -0
  85. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/utils/rabbitmq_utils.py +0 -0
  86. {jararaca-0.3.12a4 → jararaca-0.3.12a5}/src/jararaca/utils/retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.3.12a4
3
+ Version: 0.3.12a5
4
4
  Summary: A simple and fast API framework for Python
5
5
  Home-page: https://github.com/LuscasLeo/jararaca
6
6
  Author: Lucas S
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.12a4"
3
+ version = "0.3.12a5"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"
@@ -102,6 +102,8 @@ if TYPE_CHECKING:
102
102
  from .messagebus.publisher import use_publisher
103
103
  from .microservice import (
104
104
  Microservice,
105
+ is_shutting_down,
106
+ request_shutdown,
105
107
  use_app_context,
106
108
  use_app_transaction_context,
107
109
  use_app_tx_ctx_data,
@@ -207,6 +209,12 @@ if TYPE_CHECKING:
207
209
  "QueryInjector",
208
210
  "HttpMicroservice",
209
211
  "use_current_container",
212
+ "use_app_context",
213
+ "use_app_transaction_context",
214
+ "use_app_tx_ctx_data",
215
+ "is_shutting_down",
216
+ "request_shutdown",
217
+ "Microservice",
210
218
  "T_BASEMODEL",
211
219
  "DatedEntity",
212
220
  "BaseEntity",
@@ -229,7 +237,6 @@ if TYPE_CHECKING:
229
237
  "MessageBusController",
230
238
  "MessageHandler",
231
239
  "ScheduledAction",
232
- "Microservice",
233
240
  "ProviderSpec",
234
241
  "Token",
235
242
  "AIOSqlAlchemySessionInterceptor",
@@ -284,9 +291,6 @@ if TYPE_CHECKING:
284
291
  "RetryConfig",
285
292
  # Exception classes
286
293
  "TimeoutException",
287
- "use_app_context",
288
- "use_app_transaction_context",
289
- "use_app_tx_ctx_data",
290
294
  "AppTransactionContext",
291
295
  "AppContext",
292
296
  "ControllerMemberReflect",
@@ -501,6 +505,8 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
501
505
  "use_app_context": (__SPEC_PARENT__, "microservice", None),
502
506
  "use_app_transaction_context": (__SPEC_PARENT__, "microservice", None),
503
507
  "use_app_tx_ctx_data": (__SPEC_PARENT__, "microservice", None),
508
+ "is_shutting_down": (__SPEC_PARENT__, "microservice", None),
509
+ "request_shutdown": (__SPEC_PARENT__, "microservice", None),
504
510
  "AppContext": (__SPEC_PARENT__, "microservice", None),
505
511
  "AppInterceptor": (__SPEC_PARENT__, "microservice", None),
506
512
  "AppTransactionContext": (__SPEC_PARENT__, "microservice", None),
@@ -49,6 +49,8 @@ from jararaca.microservice import (
49
49
  MessageBusTransactionData,
50
50
  Microservice,
51
51
  SchedulerTransactionData,
52
+ ShutdownState,
53
+ provide_shutdown_state,
52
54
  )
53
55
  from jararaca.scheduler.decorators import ScheduledActionData
54
56
  from jararaca.utils.rabbitmq_utils import RabbitmqUtils
@@ -129,6 +131,17 @@ class MessageBusConsumer(ABC):
129
131
  """Close all resources related to the consumer"""
130
132
 
131
133
 
134
+ class _WorkerShutdownState(ShutdownState):
135
+ def __init__(self, shutdown_event: asyncio.Event):
136
+ self.shutdown_event = shutdown_event
137
+
138
+ def request_shutdown(self) -> None:
139
+ self.shutdown_event.set()
140
+
141
+ def is_shutdown_requested(self) -> bool:
142
+ return self.shutdown_event.is_set()
143
+
144
+
132
145
  class AioPikaMicroserviceConsumer(MessageBusConsumer):
133
146
  def __init__(
134
147
  self,
@@ -146,6 +159,7 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
146
159
  self.incoming_map: dict[str, MessageHandlerData] = {}
147
160
  self.uow_context_provider = uow_context_provider
148
161
  self.shutdown_event = asyncio.Event()
162
+ self.shutdown_state = _WorkerShutdownState(self.shutdown_event)
149
163
  self.lock = asyncio.Lock()
150
164
  self.tasks: set[asyncio.Task[Any]] = set()
151
165
  self.connection: aio_pika.abc.AbstractConnection | None = None
@@ -832,18 +846,19 @@ class ScheduledMessageHandlerCallback:
832
846
  args: tuple[Any, ...],
833
847
  kwargs: dict[str, Any],
834
848
  ) -> None:
835
- async with self.consumer.uow_context_provider(
836
- AppTransactionContext(
837
- controller_member_reflect=scheduled_action.controller_member,
838
- transaction_data=SchedulerTransactionData(
839
- scheduled_to=datetime.now(UTC),
840
- cron_expression=scheduled_action.spec.cron,
841
- triggered_at=datetime.now(UTC),
842
- ),
843
- )
844
- ):
849
+ with provide_shutdown_state(self.consumer.shutdown_state):
850
+ async with self.consumer.uow_context_provider(
851
+ AppTransactionContext(
852
+ controller_member_reflect=scheduled_action.controller_member,
853
+ transaction_data=SchedulerTransactionData(
854
+ scheduled_to=datetime.now(UTC),
855
+ cron_expression=scheduled_action.spec.cron,
856
+ triggered_at=datetime.now(UTC),
857
+ ),
858
+ )
859
+ ):
845
860
 
846
- await scheduled_action.callable(*args, **kwargs)
861
+ await scheduled_action.callable(*args, **kwargs)
847
862
 
848
863
 
849
864
  class MessageHandlerCallback:
@@ -1133,83 +1148,86 @@ class MessageHandlerCallback:
1133
1148
  incoming_message_spec = MessageHandler.get_message_incoming(handler)
1134
1149
  assert incoming_message_spec is not None
1135
1150
 
1136
- async with self.consumer.uow_context_provider(
1137
- AppTransactionContext(
1138
- controller_member_reflect=handler_data.controller_member,
1139
- transaction_data=MessageBusTransactionData(
1140
- message=builded_message,
1141
- topic=routing_key,
1142
- ),
1143
- )
1144
- ):
1145
- ctx: AsyncContextManager[Any]
1146
- if incoming_message_spec.timeout is not None:
1147
- ctx = asyncio.timeout(incoming_message_spec.timeout)
1148
- else:
1149
- ctx = none_context()
1150
- async with ctx:
1151
- try:
1152
- with provide_bus_message_controller(
1153
- AioPikaMessageBusController(aio_pika_message)
1154
- ):
1155
- await handler(builded_message)
1156
- if not incoming_message_spec.auto_ack:
1157
- with suppress(aio_pika.MessageProcessError):
1158
- # Use channel context for acknowledgement
1159
- async with self.consumer.get_channel_ctx(self.queue_name):
1160
- await aio_pika_message.ack()
1161
- except BaseException as base_exc:
1162
- # Get message id for logging
1163
- message_id = aio_pika_message.message_id or str(uuid.uuid4())
1164
-
1165
- # Extract retry count from headers if available
1166
- headers = aio_pika_message.headers or {}
1167
- retry_count = int(str(headers.get("x-retry-count", 0)))
1168
-
1169
- # Process exception handler if configured
1170
- if incoming_message_spec.exception_handler is not None:
1171
- try:
1172
- incoming_message_spec.exception_handler(base_exc)
1173
- except Exception as nested_exc:
1151
+ with provide_shutdown_state(self.consumer.shutdown_state):
1152
+ async with self.consumer.uow_context_provider(
1153
+ AppTransactionContext(
1154
+ controller_member_reflect=handler_data.controller_member,
1155
+ transaction_data=MessageBusTransactionData(
1156
+ message=builded_message,
1157
+ topic=routing_key,
1158
+ ),
1159
+ )
1160
+ ):
1161
+ ctx: AsyncContextManager[Any]
1162
+ if incoming_message_spec.timeout is not None:
1163
+ ctx = asyncio.timeout(incoming_message_spec.timeout)
1164
+ else:
1165
+ ctx = none_context()
1166
+ async with ctx:
1167
+ try:
1168
+ with provide_bus_message_controller(
1169
+ AioPikaMessageBusController(aio_pika_message)
1170
+ ):
1171
+ await handler(builded_message)
1172
+ if not incoming_message_spec.auto_ack:
1173
+ with suppress(aio_pika.MessageProcessError):
1174
+ # Use channel context for acknowledgement
1175
+ async with self.consumer.get_channel_ctx(
1176
+ self.queue_name
1177
+ ):
1178
+ await aio_pika_message.ack()
1179
+ except BaseException as base_exc:
1180
+ # Get message id for logging
1181
+ message_id = aio_pika_message.message_id or str(uuid.uuid4())
1182
+
1183
+ # Extract retry count from headers if available
1184
+ headers = aio_pika_message.headers or {}
1185
+ retry_count = int(str(headers.get("x-retry-count", 0)))
1186
+
1187
+ # Process exception handler if configured
1188
+ if incoming_message_spec.exception_handler is not None:
1189
+ try:
1190
+ incoming_message_spec.exception_handler(base_exc)
1191
+ except Exception as nested_exc:
1192
+ logger.exception(
1193
+ f"Error processing exception handler for message {message_id}: {base_exc} | {nested_exc}"
1194
+ )
1195
+ else:
1174
1196
  logger.exception(
1175
- f"Error processing exception handler for message {message_id}: {base_exc} | {nested_exc}"
1197
+ f"Error processing message {message_id} on topic {routing_key}: {str(base_exc)}"
1176
1198
  )
1177
- else:
1178
- logger.exception(
1179
- f"Error processing message {message_id} on topic {routing_key}: {str(base_exc)}"
1180
- )
1181
1199
 
1182
- # Handle rejection with retry logic
1183
- if incoming_message_spec.requeue_on_exception:
1184
- # Use our retry with backoff mechanism
1185
- await self.handle_reject_message(
1186
- aio_pika_message,
1187
- requeue=False, # Don't requeue directly, use our backoff mechanism
1188
- retry_count=retry_count,
1189
- exception=base_exc,
1190
- )
1191
- else:
1192
- # Message shouldn't be retried, reject it
1193
- await self.handle_reject_message(
1194
- aio_pika_message, requeue=False, exception=base_exc
1195
- )
1196
- else:
1197
- # Message processed successfully, log and clean up any retry state
1198
- message_id = aio_pika_message.message_id or str(uuid.uuid4())
1199
- if message_id in self.retry_state:
1200
- del self.retry_state[message_id]
1201
-
1202
- # Log success with retry information if applicable
1203
- headers = aio_pika_message.headers or {}
1204
- if "x-retry-count" in headers:
1205
- retry_count = int(str(headers.get("x-retry-count", 0)))
1206
- logger.info(
1207
- f"Message {message_id}#{self.queue_name} processed successfully after {retry_count} retries"
1208
- )
1200
+ # Handle rejection with retry logic
1201
+ if incoming_message_spec.requeue_on_exception:
1202
+ # Use our retry with backoff mechanism
1203
+ await self.handle_reject_message(
1204
+ aio_pika_message,
1205
+ requeue=False, # Don't requeue directly, use our backoff mechanism
1206
+ retry_count=retry_count,
1207
+ exception=base_exc,
1208
+ )
1209
+ else:
1210
+ # Message shouldn't be retried, reject it
1211
+ await self.handle_reject_message(
1212
+ aio_pika_message, requeue=False, exception=base_exc
1213
+ )
1209
1214
  else:
1210
- logger.info(
1211
- f"Message {message_id}#{self.queue_name} processed successfully"
1212
- )
1215
+ # Message processed successfully, log and clean up any retry state
1216
+ message_id = aio_pika_message.message_id or str(uuid.uuid4())
1217
+ if message_id in self.retry_state:
1218
+ del self.retry_state[message_id]
1219
+
1220
+ # Log success with retry information if applicable
1221
+ headers = aio_pika_message.headers or {}
1222
+ if "x-retry-count" in headers:
1223
+ retry_count = int(str(headers.get("x-retry-count", 0)))
1224
+ logger.info(
1225
+ f"Message {message_id}#{self.queue_name} processed successfully after {retry_count} retries"
1226
+ )
1227
+ else:
1228
+ logger.info(
1229
+ f"Message {message_id}#{self.queue_name} processed successfully"
1230
+ )
1213
1231
 
1214
1232
 
1215
1233
  @asynccontextmanager
@@ -325,6 +325,48 @@ def provide_container(container: Container) -> Generator[None, None, None]:
325
325
  current_container_ctx.reset(token)
326
326
 
327
327
 
328
+ class ShutdownState(Protocol):
329
+
330
+ def request_shutdown(self) -> None: ...
331
+
332
+ def is_shutdown_requested(self) -> bool: ...
333
+
334
+
335
+ shutdown_state_ctx = ContextVar[ShutdownState]("shutdown_state")
336
+
337
+
338
+ def is_shutting_down() -> bool:
339
+ """
340
+ Check if the application is in the process of shutting down.
341
+ """
342
+ return shutdown_state_ctx.get().is_shutdown_requested()
343
+
344
+
345
+ def request_shutdown() -> None:
346
+ """
347
+ Request the application to shut down.
348
+ This will set the shutdown event, allowing the application to gracefully shut down.
349
+ """
350
+ shutdown_state_ctx.get().request_shutdown()
351
+
352
+
353
+ @contextmanager
354
+ def provide_shutdown_state(
355
+ state: ShutdownState,
356
+ ) -> Generator[None, None, None]:
357
+ """
358
+ Context manager to provide the shutdown state.
359
+ This is used to manage the shutdown event for the application.
360
+ """
361
+
362
+ token = shutdown_state_ctx.set(state)
363
+ try:
364
+ yield
365
+ finally:
366
+ with suppress(ValueError):
367
+ shutdown_state_ctx.reset(token)
368
+
369
+
328
370
  __all__ = [
329
371
  "AppTransactionContext",
330
372
  "AppInterceptor",
@@ -1,4 +1,8 @@
1
+ import os
2
+ import signal
3
+ import threading
1
4
  from contextlib import asynccontextmanager
5
+ from signal import SIGINT, SIGTERM
2
6
  from typing import Any, AsyncGenerator
3
7
 
4
8
  from fastapi import Depends, FastAPI, Request, WebSocket
@@ -10,7 +14,9 @@ from jararaca.lifecycle import AppLifecycle
10
14
  from jararaca.microservice import (
11
15
  AppTransactionContext,
12
16
  HttpTransactionData,
17
+ ShutdownState,
13
18
  WebSocketTransactionData,
19
+ provide_shutdown_state,
14
20
  )
15
21
  from jararaca.presentation.decorators import RestController
16
22
  from jararaca.presentation.http_microservice import HttpMicroservice
@@ -76,10 +82,49 @@ class HttpAppLifecycle:
76
82
  yield
77
83
 
78
84
 
85
+ class HttpShutdownState(ShutdownState):
86
+ def __init__(self) -> None:
87
+ self._requested = False
88
+ self.old_signal_handlers = {
89
+ SIGINT: signal.getsignal(SIGINT),
90
+ SIGTERM: signal.getsignal(SIGTERM),
91
+ }
92
+ self.thread_lock = threading.Lock()
93
+
94
+ def request_shutdown(self) -> None:
95
+ if not self._requested:
96
+ self._requested = True
97
+ os.kill(os.getpid(), SIGINT)
98
+
99
+ def is_shutdown_requested(self) -> bool:
100
+ return self._requested
101
+
102
+ def handle_signal(self, signum: int, frame: Any) -> None:
103
+ print(f"Received signal {signum}, initiating shutdown...")
104
+ if self._requested:
105
+ print("Shutdown already requested, ignoring signal.")
106
+ return
107
+ print("Requesting shutdown...")
108
+ self._requested = True
109
+
110
+ # remove the signal handler to prevent recursion
111
+ for sig in (SIGINT, SIGTERM):
112
+ if self.old_signal_handlers[sig] is not None:
113
+ signal.signal(sig, self.old_signal_handlers[sig])
114
+
115
+ signal.raise_signal(signum)
116
+
117
+ def setup_signal_handlers(self) -> None:
118
+ signal.signal(SIGINT, self.handle_signal)
119
+ signal.signal(SIGTERM, self.handle_signal)
120
+
121
+
79
122
  class HttpUowContextProviderDependency:
80
123
 
81
124
  def __init__(self, uow_provider: UnitOfWorkContextProvider) -> None:
82
125
  self.uow_provider = uow_provider
126
+ self.shutdown_state = HttpShutdownState()
127
+ self.shutdown_state.setup_signal_handlers()
83
128
 
84
129
  async def __call__(
85
130
  self, websocket: WebSocket = None, request: Request = None # type: ignore
@@ -101,17 +146,18 @@ class HttpUowContextProviderDependency:
101
146
  "ControllerMemberReflect, but got: {}".format(type(member))
102
147
  )
103
148
 
104
- async with self.uow_provider(
105
- AppTransactionContext(
106
- controller_member_reflect=member,
107
- transaction_data=(
108
- HttpTransactionData(request=request)
109
- if request
110
- else WebSocketTransactionData(websocket=websocket)
111
- ),
112
- )
113
- ):
114
- yield
149
+ with provide_shutdown_state(self.shutdown_state):
150
+ async with self.uow_provider(
151
+ AppTransactionContext(
152
+ controller_member_reflect=member,
153
+ transaction_data=(
154
+ HttpTransactionData(request=request)
155
+ if request
156
+ else WebSocketTransactionData(websocket=websocket)
157
+ ),
158
+ )
159
+ ):
160
+ yield
115
161
 
116
162
 
117
163
  def create_http_server(
File without changes
File without changes
File without changes
File without changes
File without changes