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.
- {vortexpy-4.3.10 → vortexpy-4.3.12}/PKG-INFO +1 -1
- {vortexpy-4.3.10 → vortexpy-4.3.12}/pyproject.toml +1 -1
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/DeferUtil.py +16 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadResponse.py +60 -17
- vortexpy-4.3.12/vortex/PayloadResponse_test.py +161 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexUtil.py +105 -1
- vortexpy-4.3.12/vortex/VortexUtil_test.py +292 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/__init__.py +1 -1
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableHandler.py +34 -5
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableHandler_test.py +46 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableProxyHandler.py +3 -2
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/twisted_runtime_metrics.py +17 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/PKG-INFO +1 -1
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/SOURCES.txt +1 -0
- vortexpy-4.3.10/vortex/PayloadResponse_test.py +0 -42
- {vortexpy-4.3.10 → vortexpy-4.3.12}/README.md +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/TwistedDeferredTimeout.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_ping_client.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_ping_server.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_websocket_rpc_test_client.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/run_websocket_rpc_test_server.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/setup_example_logging.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/example/store_and_load_json.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/setup.cfg +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/DataWrapTuple.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/DeferUtil_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/Jsonable.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/MtlsServerSslOptions.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/Payload.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadEndpoint.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadEndpointTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadEnvelope.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadFilterKeys.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadIO.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadPriority.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadResponseTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PayloadTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/PerformTestActionTuple.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/SerialiseUtil.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/SerialiseUtil_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TestTuple.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/Tuple.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleAction.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleBson.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleBson_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleDataForTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleDocumentConverter.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TuplePerfTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleRestful.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleRestful_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleSelector.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/TupleTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexAccessControlContextABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexAuthenticatorABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientHttp.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientHttpTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientTcp.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexClientWebsocketFactory.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexConnectionABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexEndpointPermission.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexFactory.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexFactory_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexPayloadProtocol.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexPermission.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexPermissionError.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServer.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerConnection.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerConnection_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerHttpResource.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerTcp.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerWebsocket.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServerWebsocket_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexServer_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexSession.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexSession_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexWritePushProducer.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/VortexWritePushProducer_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoader.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/data_loader/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventBus.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/event_bus/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessor.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessorProxy.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionPushOffline.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleActionPushOffline_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataActionClient.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableCache.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableCache_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataObserverClient.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/TupleDataProviderABC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/handler/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/mypy_plugin.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/payload_many_tuples_benchmark_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/py.typed +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/GzippedDataHttpClient.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/RestfulHttpClient.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/RestfulResource.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/restful_http_client_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/restful/restful_resource_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/RPC.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/RPCTest.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/RPC_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/rpc/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/VortexTupleJson.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/sqla_orm/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/storage/TupleActionStorageSqlite.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/storage/TupleStorageSqlite.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/storage/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/__init__.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/constants.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/tuple_test_fixtures.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortex/twisted_runtime_metrics_test.py +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/dependency_links.txt +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/entry_points.txt +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/requires.txt +0 -0
- {vortexpy-4.3.10 → vortexpy-4.3.12}/vortexpy.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortexpy"
|
|
7
|
-
version = "4.3.
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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)
|