vortexpy 4.3.10__tar.gz → 4.3.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. {vortexpy-4.3.10 → vortexpy-4.3.12}/PKG-INFO +1 -1
  2. {vortexpy-4.3.10 → vortexpy-4.3.12}/pyproject.toml +1 -1
  3. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/DeferUtil.py +16 -0
  4. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadResponse.py +60 -17
  5. vortexpy-4.3.12/vortex/PayloadResponse_test.py +161 -0
  6. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexUtil.py +105 -1
  7. vortexpy-4.3.12/vortex/VortexUtil_test.py +292 -0
  8. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/__init__.py +1 -1
  9. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableHandler.py +34 -5
  10. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableHandler_test.py +46 -0
  11. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableProxyHandler.py +3 -2
  12. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/twisted_runtime_metrics.py +17 -0
  13. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/PKG-INFO +1 -1
  14. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/SOURCES.txt +1 -0
  15. vortexpy-4.3.10/vortex/PayloadResponse_test.py +0 -42
  16. {vortexpy-4.3.10 → vortexpy-4.3.12}/README.md +0 -0
  17. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/TwistedDeferredTimeout.py +0 -0
  18. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/__init__.py +0 -0
  19. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  20. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  21. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  22. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  23. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_websocket_rpc_test_client.py +0 -0
  24. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_websocket_rpc_test_server.py +0 -0
  25. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/setup_example_logging.py +0 -0
  26. {vortexpy-4.3.10 → vortexpy-4.3.12}/example/store_and_load_json.py +0 -0
  27. {vortexpy-4.3.10 → vortexpy-4.3.12}/setup.cfg +0 -0
  28. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/DataWrapTuple.py +0 -0
  29. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/DeferUtil_test.py +0 -0
  30. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/Jsonable.py +0 -0
  31. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  32. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  33. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/MtlsServerSslOptions.py +0 -0
  34. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/Payload.py +0 -0
  35. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadEndpoint.py +0 -0
  36. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadEndpointTest.py +0 -0
  37. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadEnvelope.py +0 -0
  38. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadFilterKeys.py +0 -0
  39. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadIO.py +0 -0
  40. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadPriority.py +0 -0
  41. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadResponseTest.py +0 -0
  42. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadTest.py +0 -0
  43. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PerformTestActionTuple.py +0 -0
  44. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/SerialiseUtil.py +0 -0
  45. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/SerialiseUtil_test.py +0 -0
  46. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TestTuple.py +0 -0
  47. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/Tuple.py +0 -0
  48. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleAction.py +0 -0
  49. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleBson.py +0 -0
  50. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleBson_test.py +0 -0
  51. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleDataForTest.py +0 -0
  52. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleDocumentConverter.py +0 -0
  53. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TuplePerfTest.py +0 -0
  54. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  55. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleRestful.py +0 -0
  56. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleRestful_test.py +0 -0
  57. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleSelector.py +0 -0
  58. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleTest.py +0 -0
  59. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexABC.py +0 -0
  60. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexAccessControlContextABC.py +0 -0
  61. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexAuthenticatorABC.py +0 -0
  62. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientHttp.py +0 -0
  63. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientHttpTest.py +0 -0
  64. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientTcp.py +0 -0
  65. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientWebsocketFactory.py +0 -0
  66. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexConnectionABC.py +0 -0
  67. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexEndpointPermission.py +0 -0
  68. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexFactory.py +0 -0
  69. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexFactory_test.py +0 -0
  70. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexPayloadProtocol.py +0 -0
  71. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexPermission.py +0 -0
  72. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexPermissionError.py +0 -0
  73. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServer.py +0 -0
  74. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerConnection.py +0 -0
  75. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerConnection_test.py +0 -0
  76. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerHttpResource.py +0 -0
  77. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerTcp.py +0 -0
  78. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerWebsocket.py +0 -0
  79. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerWebsocket_test.py +0 -0
  80. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServer_test.py +0 -0
  81. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexSession.py +0 -0
  82. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexSession_test.py +0 -0
  83. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  84. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  85. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexTest.py +0 -0
  86. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexWritePushProducer.py +0 -0
  87. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexWritePushProducer_test.py +0 -0
  88. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  89. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoader.py +0 -0
  90. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  91. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  92. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  93. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/__init__.py +0 -0
  94. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  95. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  96. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  97. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/__init__.py +0 -0
  98. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessor.py +0 -0
  99. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  100. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  101. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  102. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionPushOffline.py +0 -0
  103. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  104. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  105. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataActionClient.py +0 -0
  106. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableCache.py +0 -0
  107. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  108. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  109. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  110. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObserverClient.py +0 -0
  111. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataProviderABC.py +0 -0
  112. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  113. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/__init__.py +0 -0
  114. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/mypy_plugin.py +0 -0
  115. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  116. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/py.typed +0 -0
  117. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/GzippedDataHttpClient.py +0 -0
  118. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  119. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  120. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/RestfulHttpClient.py +0 -0
  121. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/RestfulResource.py +0 -0
  122. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/__init__.py +0 -0
  123. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/restful_http_client_test.py +0 -0
  124. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/restful_resource_test.py +0 -0
  125. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/RPC.py +0 -0
  126. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/RPCTest.py +0 -0
  127. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/RPC_test.py +0 -0
  128. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/__init__.py +0 -0
  129. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  130. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  131. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  132. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/__init__.py +0 -0
  133. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  134. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/storage/TupleStorageSqlite.py +0 -0
  135. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/storage/__init__.py +0 -0
  136. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/__init__.py +0 -0
  137. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/constants.py +0 -0
  138. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  139. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  140. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  141. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  142. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_test_fixtures.py +0 -0
  143. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/twisted_runtime_metrics_test.py +0 -0
  144. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/dependency_links.txt +0 -0
  145. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/entry_points.txt +0 -0
  146. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/requires.txt +0 -0
  147. {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 4.3.10
3
+ Version: 4.3.12
4
4
  Summary: Flying Ion's observable, routable, data serialisation and transport code.
5
5
  Author-email: Flying Ion Pty Ltd <contact@flyingion.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortexpy"
7
- version = "4.3.10"
7
+ version = "4.3.12"
8
8
  license = {text = "MIT"}
9
9
  authors = [{name = "Flying Ion Pty Ltd", email = "contact@flyingion.com"}]
10
10
  description = "Flying Ion's observable, routable, data serialisation and transport code."
@@ -16,6 +16,7 @@ import twisted
16
16
  from twisted.internet import reactor
17
17
  from twisted.internet.defer import (
18
18
  maybeDeferred,
19
+ CancelledError,
19
20
  Deferred,
20
21
  inlineCallbacks,
21
22
  ensureDeferred,
@@ -239,6 +240,17 @@ def deferToThreadWrapWithLogger(
239
240
  )
240
241
 
241
242
  def handleFailure(failure):
243
+ if failure.check(CancelledError):
244
+ # Cancellation is designed control flow (for example a
245
+ # limitConcurrency timeout quarantining the thread), not
246
+ # a threaded call failure.
247
+ loggerArg.debug(
248
+ "Threaded call cancelled in %s.%s",
249
+ funcToWrap.__module__,
250
+ funcToWrap.__qualname__,
251
+ )
252
+ return True if consumeError else failure
253
+
242
254
  if logException:
243
255
  return vortexLogFailure(
244
256
  failure, loggerArg, consumeError=consumeError
@@ -253,6 +265,10 @@ def deferToThreadWrapWithLogger(
253
265
  return True if consumeError else failure
254
266
 
255
267
  d: Deferred[_R] = deferToThread(timedFuncToRun, *args, **kwargs)
268
+ # Marks this Deferred as threaded for limitConcurrency: a timed
269
+ # out threaded call cannot be killed, so the limiter accumulates
270
+ # marked Deferreds in its quarantine registry.
271
+ d._isVortexThreadedCall = True # type: ignore[attr-defined]
256
272
  d.addCallback(checkForInlineCallbackYield)
257
273
  d.addErrback(handleFailure)
258
274
  return d
@@ -16,6 +16,8 @@ from uuid import uuid4
16
16
  import pytz
17
17
  from twisted.internet import reactor
18
18
  from twisted.internet.defer import Deferred
19
+ from twisted.internet.defer import TimeoutError
20
+ from twisted.internet.interfaces import IDelayedCall
19
21
  from twisted.python.failure import Failure
20
22
 
21
23
  from vortex.PayloadEndpoint import PayloadEndpoint
@@ -68,6 +70,7 @@ class PayloadResponse(Deferred):
68
70
  timeout: Optional[float] = None,
69
71
  resultCheck=True,
70
72
  logTimeoutException=True,
73
+ retries: Optional[int] = None,
71
74
  ) -> None:
72
75
  """Constructor
73
76
 
@@ -83,9 +86,16 @@ class PayloadResponse(Deferred):
83
86
  it errback will be called.
84
87
  :param logTimeoutException: When True, payload timeouts log at ERROR.
85
88
  When False, timeouts log at DEBUG with neutral wording.
89
+ :param retries: The number of times to resend the payload envelope
90
+ when the response times out; None sends once with no
91
+ retries. Requires destVortexName or destVortexUuid.
86
92
  """
87
93
  Deferred.__init__(self)
88
94
 
95
+ assert retries is None or (destVortexName or destVortexUuid), (
96
+ "retries requires destVortexName or destVortexUuid to resend"
97
+ )
98
+
89
99
  if not timeout:
90
100
  timeout = self.TIMEOUT
91
101
 
@@ -99,6 +109,11 @@ class PayloadResponse(Deferred):
99
109
  payloadEnvelope.filt[self.MESSAGE_ID_KEY] = self._messageId
100
110
  self._filt = copy(payloadEnvelope.filt)
101
111
  self._destVortexName = destVortexName
112
+ self._destVortexUuid = destVortexUuid
113
+ self._payloadEnvelope = payloadEnvelope
114
+ self._timeout = timeout
115
+ self._retries: Optional[int] = retries
116
+ self._timeoutDelayedCall: Optional[IDelayedCall] = None
102
117
 
103
118
  self._status = self.PROCESSING
104
119
  self._date = datetime.now(pytz.utc)
@@ -107,23 +122,49 @@ class PayloadResponse(Deferred):
107
122
  self._filt, self._process, requiredPermissions=frozenset()
108
123
  )
109
124
 
110
- if destVortexName or destVortexUuid:
111
- d: Deferred = payloadEnvelope.toVortexMsgDefer()
112
- d.addCallback(
113
- VortexFactory.sendVortexMsg,
114
- destVortexName=destVortexName,
115
- destVortexUuid=destVortexUuid,
116
- )
117
- d.addErrback(self.errback)
125
+ self._sendVortexMsg()
118
126
 
119
127
  try:
120
128
  raise Exception()
121
129
  except:
122
130
  self._stack = sys.exc_info()[2]
123
131
 
124
- # noinspection PyTypeChecker
125
- self.addTimeout(timeout, reactor)
126
- self.addErrback(self._timedOut)
132
+ if retries is None:
133
+ # noinspection PyTypeChecker
134
+ self.addTimeout(timeout, reactor)
135
+ self.addErrback(self._timedOut)
136
+ else:
137
+ self._armTimeout()
138
+
139
+ def _sendVortexMsg(self) -> None:
140
+ if not (self._destVortexName or self._destVortexUuid):
141
+ return
142
+
143
+ d: Deferred = self._payloadEnvelope.toVortexMsgDefer()
144
+ d.addCallback(
145
+ VortexFactory.sendVortexMsg,
146
+ destVortexName=self._destVortexName,
147
+ destVortexUuid=self._destVortexUuid,
148
+ )
149
+ d.addErrback(self.errback)
150
+
151
+ def _armTimeout(self) -> None:
152
+ self._timeoutDelayedCall = reactor.callLater( # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue]
153
+ self._timeout, self._onTimeout
154
+ )
155
+
156
+ def _onTimeout(self) -> None:
157
+ self._timeoutDelayedCall = None
158
+
159
+ if self._retries:
160
+ self._retries -= 1
161
+ self._sendVortexMsg()
162
+ self._armTimeout()
163
+ return
164
+
165
+ failure = Failure(TimeoutError("timed out"))
166
+ self._timedOut(failure)
167
+ self.errback(failure)
127
168
 
128
169
  @classmethod
129
170
  def isResponsePayloadEnvelope(cls, payloadEnvelope: PayloadEnvelope):
@@ -163,6 +204,12 @@ class PayloadResponse(Deferred):
163
204
  self._endpoint.shutdown()
164
205
  self._endpoint = None
165
206
 
207
+ if self._timeoutDelayedCall is not None:
208
+ # zope Interface methods omit self, which static analysis cannot bind
209
+ if self._timeoutDelayedCall.active(): # pyright: ignore[reportAttributeAccessIssue]
210
+ self._timeoutDelayedCall.cancel() # pyright: ignore[reportAttributeAccessIssue]
211
+ self._timeoutDelayedCall = None
212
+
166
213
  if self._destVortexName and vortexName != self._destVortexName:
167
214
  logger.debug(
168
215
  "Received response from a vortex other than the dest vortex, "
@@ -176,14 +223,10 @@ class PayloadResponse(Deferred):
176
223
  logger.error("Received response after timeout for %s" % self._filt)
177
224
  return
178
225
 
179
- if self._resultCheck and not payloadEnvelope.result in (None, True):
226
+ if self._resultCheck and payloadEnvelope.result not in (None, True):
180
227
  self._status = self.FAILED
181
228
  self.errback(
182
- Failure(
183
- Exception(payloadEnvelope.result).with_traceback(
184
- self._stack
185
- )
186
- )
229
+ Failure(Exception(payloadEnvelope.result).with_traceback(self._stack))
187
230
  )
188
231
 
189
232
  else:
@@ -0,0 +1,161 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ from twisted.internet.defer import TimeoutError
5
+ from twisted.internet.defer import succeed
6
+ from twisted.python.failure import Failure
7
+
8
+ from vortex.PayloadEnvelope import PayloadEnvelope
9
+ from vortex.PayloadResponse import PayloadResponse
10
+
11
+
12
+ class _FakeDelayedCall:
13
+ def __init__(self, callback) -> None:
14
+ self.callback = callback
15
+ self.cancelled = False
16
+
17
+ def active(self) -> bool:
18
+ return not self.cancelled
19
+
20
+ def cancel(self) -> None:
21
+ self.cancelled = True
22
+
23
+ def fire(self) -> None:
24
+ self.callback()
25
+
26
+
27
+ class _FakeReactor:
28
+ def __init__(self) -> None:
29
+ self.delayedCalls: list[_FakeDelayedCall] = []
30
+
31
+ def callLater(self, _seconds: float, callback) -> _FakeDelayedCall:
32
+ call = _FakeDelayedCall(callback)
33
+ self.delayedCalls.append(call)
34
+ return call
35
+
36
+
37
+ class PayloadResponseLoggingTest(unittest.TestCase):
38
+ @patch("vortex.PayloadResponse.VortexFactory.sendVortexMsg")
39
+ @patch("vortex.PayloadResponse.logger")
40
+ def testTimeoutLogsDebugWhenLogTimeoutExceptionFalse(
41
+ self, mockLogger: MagicMock, _mockSend: MagicMock
42
+ ) -> None:
43
+ pr = PayloadResponse(
44
+ PayloadEnvelope(filt={"key": "example"}),
45
+ timeout=30.0,
46
+ logTimeoutException=False,
47
+ )
48
+ pr._timedOut(Failure(TimeoutError("timed out")))
49
+ mockLogger.error.assert_not_called()
50
+ mockLogger.debug.assert_called_once()
51
+
52
+ @patch("vortex.PayloadResponse.VortexFactory.sendVortexMsg")
53
+ @patch("vortex.PayloadResponse.logger")
54
+ def testTimeoutLogsErrorWhenLogTimeoutExceptionTrue(
55
+ self, mockLogger: MagicMock, _mockSend: MagicMock
56
+ ) -> None:
57
+ pr = PayloadResponse(
58
+ PayloadEnvelope(filt={"key": "example"}),
59
+ timeout=30.0,
60
+ logTimeoutException=True,
61
+ )
62
+ pr._timedOut(Failure(TimeoutError("timed out")))
63
+ mockLogger.debug.assert_not_called()
64
+ mockLogger.error.assert_called_once()
65
+
66
+
67
+ class PayloadResponseRetryTest(unittest.TestCase):
68
+ def setUp(self) -> None:
69
+ self.fakeReactor = _FakeReactor()
70
+ patch("vortex.PayloadResponse.reactor", self.fakeReactor).start()
71
+ self.mockSend: MagicMock = patch(
72
+ "vortex.PayloadResponse.VortexFactory.sendVortexMsg"
73
+ ).start()
74
+ patch(
75
+ "vortex.PayloadEnvelope.PayloadEnvelope.toVortexMsgDefer",
76
+ return_value=succeed(b"{}"),
77
+ ).start()
78
+ self.addCleanup(patch.stopall)
79
+
80
+ def testTimeoutResendsPayloadAndSucceedsOnLateResponse(self) -> None:
81
+ pr = PayloadResponse(
82
+ PayloadEnvelope(filt={"key": "example"}),
83
+ destVortexName="destVortex",
84
+ timeout=30.0,
85
+ retries=1,
86
+ )
87
+
88
+ self.assertEqual(self.mockSend.call_count, 1)
89
+ self.assertEqual(len(self.fakeReactor.delayedCalls), 1)
90
+
91
+ # First attempt times out, the payload is resent and re-armed
92
+ self.fakeReactor.delayedCalls[0].fire()
93
+
94
+ self.assertEqual(self.mockSend.call_count, 2)
95
+ self.assertEqual(len(self.fakeReactor.delayedCalls), 2)
96
+ self.assertEqual(pr.status, PayloadResponse.PROCESSING)
97
+
98
+ # The resent payload receives a response
99
+ results = []
100
+ pr.addCallback(results.append)
101
+ response = PayloadEnvelope(filt={"key": "example"})
102
+ pr._process(response, "vortexUuid", "destVortex")
103
+
104
+ self.assertEqual(pr.status, PayloadResponse.SUCCESS)
105
+ self.assertEqual(results, [response])
106
+ self.assertTrue(self.fakeReactor.delayedCalls[1].cancelled)
107
+
108
+ def testExhaustedRetriesErrbackWithTimedOut(self) -> None:
109
+ pr = PayloadResponse(
110
+ PayloadEnvelope(filt={"key": "example"}),
111
+ destVortexName="destVortex",
112
+ timeout=30.0,
113
+ retries=1,
114
+ )
115
+
116
+ failures = []
117
+ pr.addErrback(failures.append)
118
+
119
+ self.fakeReactor.delayedCalls[0].fire()
120
+ self.assertEqual(self.mockSend.call_count, 2)
121
+ self.assertEqual(pr.status, PayloadResponse.PROCESSING)
122
+
123
+ self.fakeReactor.delayedCalls[1].fire()
124
+
125
+ self.assertEqual(self.mockSend.call_count, 2)
126
+ self.assertEqual(pr.status, PayloadResponse.TIMED_OUT)
127
+ self.assertEqual(len(failures), 1)
128
+ self.assertIsInstance(failures[0].value, TimeoutError)
129
+
130
+ def testRetriesRequiresDestinationVortex(self) -> None:
131
+ with self.assertRaises(AssertionError):
132
+ PayloadResponse(
133
+ PayloadEnvelope(filt={"key": "example"}),
134
+ timeout=30.0,
135
+ retries=1,
136
+ )
137
+
138
+ def testDefaultTimeoutPathDoesNotResend(self) -> None:
139
+ pr = PayloadResponse(
140
+ PayloadEnvelope(filt={"key": "example"}),
141
+ destVortexName="destVortex",
142
+ timeout=30.0,
143
+ )
144
+
145
+ failures = []
146
+ pr.addErrback(failures.append)
147
+
148
+ self.assertEqual(self.mockSend.call_count, 1)
149
+ self.assertEqual(len(self.fakeReactor.delayedCalls), 1)
150
+
151
+ # addTimeout cancels the deferred, which errbacks with TimeoutError
152
+ self.fakeReactor.delayedCalls[0].fire()
153
+
154
+ self.assertEqual(self.mockSend.call_count, 1)
155
+ self.assertEqual(pr.status, PayloadResponse.TIMED_OUT)
156
+ self.assertEqual(len(failures), 1)
157
+ self.assertIsInstance(failures[0].value, TimeoutError)
158
+
159
+
160
+ if __name__ == "__main__":
161
+ unittest.main()
@@ -7,6 +7,7 @@
7
7
  # Support : contact@flyingion.com
8
8
 
9
9
  import json
10
+ from collections import deque
10
11
  from collections import namedtuple
11
12
  from datetime import datetime
12
13
  from datetime import timedelta
@@ -20,10 +21,10 @@ from uuid import uuid4
20
21
 
21
22
  import pytz
22
23
  from twisted.internet import reactor
24
+ from twisted.internet.defer import CancelledError
23
25
  from twisted.internet.defer import Deferred
24
26
  from twisted.internet.defer import fail
25
27
  from twisted.internet.defer import maybeDeferred
26
- from twisted.python.failure import Failure
27
28
 
28
29
  from vortex.PayloadEnvelope import PayloadEnvelope
29
30
  from vortex.PayloadEnvelope import VortexMsgList
@@ -37,6 +38,20 @@ _LimitConcurrencyArgsTuple = namedtuple(
37
38
  ("deferred", "funcSelf", "args", "kwargs", "queuedTime"),
38
39
  )
39
40
 
41
+ _LimitConcurrencyQuarantinedCallTuple = namedtuple(
42
+ "_LimitConcurrencyQuarantinedCallTuple",
43
+ ("methodQualname", "callKey", "startDate", "timeoutDate", "deferred"),
44
+ )
45
+
46
+
47
+ class LimitConcurrencyTimeoutError(Exception):
48
+ """Limit Concurrency Timeout Error
49
+
50
+ Raised on the caller's Deferred when a call started by
51
+ L{limitConcurrency} exceeds C{killOrQuarantineCallsLongerThanSeconds}
52
+ and is cancelled.
53
+ """
54
+
40
55
 
41
56
  def _dedupProcessorCallKeys(
42
57
  payloadEnvelope: PayloadEnvelope,
@@ -56,6 +71,7 @@ def limitConcurrency(
56
71
  concurrency: int,
57
72
  logTaskTimeGreaterThanSeconds=5.0,
58
73
  deduplicateBasedOnKwArgsKey: Optional[Callable] = None,
74
+ killOrQuarantineCallsLongerThanSeconds: Optional[float] = None,
59
75
  ):
60
76
  """Limit Concurrency to a call, decorator
61
77
 
@@ -66,6 +82,17 @@ def limitConcurrency(
66
82
  of strings. If this argument is provided,
67
83
  then duplicate calls with the same values for these arguments will
68
84
  be deduplicated. Duplicate calls will use the newest set of arguments.
85
+ :param killOrQuarantineCallsLongerThanSeconds: When set, any started call
86
+ still running after this many seconds is cancelled: @inlineCallbacks
87
+ callees are killed (CancelledError reaches the generator), threaded
88
+ callees are quarantined (the caller's Deferred is released while the
89
+ thread runs on). An error is logged, the limiter's alarm count is
90
+ incremented in TwistedRuntimeMetrics, the caller's Deferred
91
+ errbacks with LimitConcurrencyTimeoutError. Timed out threaded
92
+ calls (deferToThreadWrapWithLogger Deferreds) are accumulated with
93
+ their Deferred reference in the limiter's bounded _quarantinedCalls
94
+ deque for manhole inspection; killed @inlineCallbacks callees die
95
+ and are not registered.
69
96
 
70
97
  # As of python 3.6, dicts are ordered.
71
98
 
@@ -97,6 +124,8 @@ def limitConcurrency(
97
124
  self._callQueue: dict[Any, _LimitConcurrencyArgsTuple] = {}
98
125
  self._runningCount = 0
99
126
  self._processedCount = 0
127
+ self._callTimeoutCount = 0
128
+ self._quarantinedCalls: deque = deque(maxlen=50)
100
129
  self._lastLogTime = None
101
130
  self._lastQueueProcessingDelta = None
102
131
  TwistedRuntimeMetrics._collector()._registerLimitConcurrencyLimiter(
@@ -146,12 +175,87 @@ def limitConcurrency(
146
175
  )
147
176
  d = maybeDeferred(callWithArgs)
148
177
  d.addBoth(self._callComplete, now, queueWaitMs)
178
+
179
+ if killOrQuarantineCallsLongerThanSeconds is not None and not d.called:
180
+ self._utilArmCallTimeout(
181
+ d, killOrQuarantineCallsLongerThanSeconds, nextKey, now
182
+ )
183
+
149
184
  d.chainDeferred(args.deferred)
150
185
 
151
186
  self._logQueueSize()
152
187
  self._f.running = self._runningCount
153
188
  self._f.waiting = len(self._callQueue)
154
189
 
190
+ def _utilArmCallTimeout(self, d, killSeconds, callKey, startDate):
191
+ def onTimeout(d=d, callKey=callKey, startDate=startDate):
192
+ if d.called:
193
+ return
194
+
195
+ # A deferToThreadWrapWithLogger-marked Deferred means the
196
+ # callee is a thread that cannot be killed: it is
197
+ # quarantined, and the Deferred is accumulated in the
198
+ # bounded registry for manhole inspection. maybeDeferred
199
+ # passes a threaded callee's Deferred through as d; an
200
+ # @inlineCallbacks callee pauses d on it as d.result.
201
+ # Killed @inlineCallbacks callees die and are not registered.
202
+ if getattr(d, "_isVortexThreadedCall", False):
203
+ quarantinedDeferred = d
204
+ else:
205
+ awaitedDeferred = getattr(d, "result", None)
206
+ quarantinedDeferred = (
207
+ awaitedDeferred
208
+ if getattr(awaitedDeferred, "_isVortexThreadedCall", False)
209
+ else None
210
+ )
211
+
212
+ self._callTimeoutCount += 1
213
+ if quarantinedDeferred is not None:
214
+ self._quarantinedCalls.append(
215
+ _LimitConcurrencyQuarantinedCallTuple(
216
+ methodQualname=self._methodQualname,
217
+ callKey=callKey,
218
+ startDate=startDate,
219
+ timeoutDate=datetime.now(pytz.utc),
220
+ deferred=quarantinedDeferred,
221
+ )
222
+ )
223
+ TwistedRuntimeMetrics._collector()._recordLimitConcurrencyCallTimeout(
224
+ self._methodQualname
225
+ )
226
+ decoratorLogger.error(
227
+ "%s call exceeded killOrQuarantineCallsLongerThanSeconds"
228
+ " of %ss (key=%s, timeout count=%s),"
229
+ " killing or quarantining the call",
230
+ self._methodQualname,
231
+ killSeconds,
232
+ callKey,
233
+ self._callTimeoutCount,
234
+ )
235
+ d.cancel()
236
+
237
+ timeoutCall = reactor.callLater(killSeconds, onTimeout)
238
+
239
+ def cancelTimeoutCall(result):
240
+ if timeoutCall.active():
241
+ timeoutCall.cancel()
242
+ return result
243
+
244
+ def convertCancelledToTimeout(failure):
245
+ # The watchdog's own `called` flag marks whether this
246
+ # cancellation came from it: DelayedCall.called is only set
247
+ # once onTimeout begins running.
248
+ if not timeoutCall.called:
249
+ return failure
250
+ failure.trap(CancelledError)
251
+ raise LimitConcurrencyTimeoutError(
252
+ "%s call exceeded killOrQuarantineCallsLongerThanSeconds"
253
+ " of %ss" % (self._methodQualname, killSeconds)
254
+ )
255
+
256
+ d.addBoth(cancelTimeoutCall)
257
+ d.addErrback(convertCancelledToTimeout)
258
+
155
259
  def call(self, funcSelf, *args, **kwargs):
156
260
  if deduplicateBasedOnKwArgsKey:
157
261
  key = deduplicateBasedOnKwArgsKey(*args, **kwargs)