vortexpy 4.3.11__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 (146) hide show
  1. {vortexpy-4.3.11 → vortexpy-4.3.12}/PKG-INFO +1 -1
  2. {vortexpy-4.3.11 → vortexpy-4.3.12}/pyproject.toml +1 -1
  3. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/DeferUtil.py +16 -0
  4. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexUtil.py +105 -1
  5. vortexpy-4.3.12/vortex/VortexUtil_test.py +292 -0
  6. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/__init__.py +1 -1
  7. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableHandler.py +34 -5
  8. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableHandler_test.py +46 -0
  9. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableProxyHandler.py +3 -2
  10. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/twisted_runtime_metrics.py +17 -0
  11. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortexpy.egg-info/PKG-INFO +1 -1
  12. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortexpy.egg-info/SOURCES.txt +1 -0
  13. {vortexpy-4.3.11 → vortexpy-4.3.12}/README.md +0 -0
  14. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/TwistedDeferredTimeout.py +0 -0
  15. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/__init__.py +0 -0
  16. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_ping_client.py +0 -0
  17. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_ping_server.py +0 -0
  18. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_rpc_test_client.py +0 -0
  19. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/run_http_upgraded_websocket_rpc_test_server.py +0 -0
  20. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/run_websocket_rpc_test_client.py +0 -0
  21. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/run_websocket_rpc_test_server.py +0 -0
  22. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/setup_example_logging.py +0 -0
  23. {vortexpy-4.3.11 → vortexpy-4.3.12}/example/store_and_load_json.py +0 -0
  24. {vortexpy-4.3.11 → vortexpy-4.3.12}/setup.cfg +0 -0
  25. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/DataWrapTuple.py +0 -0
  26. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/DeferUtil_test.py +0 -0
  27. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/Jsonable.py +0 -0
  28. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/MtlsAuthorizedClientRegistry.py +0 -0
  29. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/MtlsAuthorizedClientRegistry_test.py +0 -0
  30. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/MtlsServerSslOptions.py +0 -0
  31. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/Payload.py +0 -0
  32. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadEndpoint.py +0 -0
  33. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadEndpointTest.py +0 -0
  34. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadEnvelope.py +0 -0
  35. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadFilterKeys.py +0 -0
  36. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadIO.py +0 -0
  37. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadPriority.py +0 -0
  38. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadResponse.py +0 -0
  39. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadResponseTest.py +0 -0
  40. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadResponse_test.py +0 -0
  41. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PayloadTest.py +0 -0
  42. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/PerformTestActionTuple.py +0 -0
  43. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/SerialiseUtil.py +0 -0
  44. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/SerialiseUtil_test.py +0 -0
  45. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TestTuple.py +0 -0
  46. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/Tuple.py +0 -0
  47. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleAction.py +0 -0
  48. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleBson.py +0 -0
  49. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleBson_test.py +0 -0
  50. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleDataForTest.py +0 -0
  51. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleDocumentConverter.py +0 -0
  52. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TuplePerfTest.py +0 -0
  53. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TuplePolymorphicTupleTypeFieldArgTest.py +0 -0
  54. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleRestful.py +0 -0
  55. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleRestful_test.py +0 -0
  56. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleSelector.py +0 -0
  57. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/TupleTest.py +0 -0
  58. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexABC.py +0 -0
  59. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexAccessControlContextABC.py +0 -0
  60. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexAuthenticatorABC.py +0 -0
  61. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexClientHttp.py +0 -0
  62. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexClientHttpTest.py +0 -0
  63. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexClientTcp.py +0 -0
  64. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexClientWebsocketFactory.py +0 -0
  65. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexConnectionABC.py +0 -0
  66. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexEndpointPermission.py +0 -0
  67. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexFactory.py +0 -0
  68. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexFactory_test.py +0 -0
  69. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexPayloadProtocol.py +0 -0
  70. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexPermission.py +0 -0
  71. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexPermissionError.py +0 -0
  72. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServer.py +0 -0
  73. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServerConnection.py +0 -0
  74. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServerConnection_test.py +0 -0
  75. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServerHttpResource.py +0 -0
  76. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServerTcp.py +0 -0
  77. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServerWebsocket.py +0 -0
  78. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServerWebsocket_test.py +0 -0
  79. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexServer_test.py +0 -0
  80. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexSession.py +0 -0
  81. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexSession_test.py +0 -0
  82. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexTcpMemoryLeakPayloadTest.py +0 -0
  83. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexTcpMemoryLeakRawTest.py +0 -0
  84. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexTest.py +0 -0
  85. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexWritePushProducer.py +0 -0
  86. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/VortexWritePushProducer_test.py +0 -0
  87. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLiveLockManager.py +0 -0
  88. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoader.py +0 -0
  89. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderDelegate.py +0 -0
  90. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderTupleABC.py +0 -0
  91. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/data_loader/TupleDataLoaderTuples.py +0 -0
  92. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/data_loader/__init__.py +0 -0
  93. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventABC.py +0 -0
  94. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventBus.py +0 -0
  95. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/event_bus/TupleChangeEventBusObserverABC.py +0 -0
  96. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/event_bus/__init__.py +0 -0
  97. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessor.py +0 -0
  98. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessorProxy.py +0 -0
  99. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessorTestHandler.py +0 -0
  100. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleActionProcessor_shutdown_test.py +0 -0
  101. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleActionPushOffline.py +0 -0
  102. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleActionPushOffline_test.py +0 -0
  103. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataAccessGranterABC.py +0 -0
  104. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataActionClient.py +0 -0
  105. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableCache.py +0 -0
  106. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableCache_test.py +0 -0
  107. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableProxyHandler_test.py +0 -0
  108. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObservableTestHandler.py +0 -0
  109. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataObserverClient.py +0 -0
  110. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/TupleDataProviderABC.py +0 -0
  111. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/VortexJSTupleLoaderTestHandler.py +0 -0
  112. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/handler/__init__.py +0 -0
  113. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/mypy_plugin.py +0 -0
  114. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/payload_many_tuples_benchmark_test.py +0 -0
  115. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/py.typed +0 -0
  116. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/GzippedDataHttpClient.py +0 -0
  117. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/GzippedDataHttpClientTest.py +0 -0
  118. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/GzippedPayloadHttpClient.py +0 -0
  119. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/RestfulHttpClient.py +0 -0
  120. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/RestfulResource.py +0 -0
  121. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/__init__.py +0 -0
  122. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/restful_http_client_test.py +0 -0
  123. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/restful/restful_resource_test.py +0 -0
  124. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/rpc/RPC.py +0 -0
  125. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/rpc/RPCTest.py +0 -0
  126. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/rpc/RPC_test.py +0 -0
  127. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/rpc/__init__.py +0 -0
  128. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/sqla_orm/OrmCrudHandler.py +0 -0
  129. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/sqla_orm/TupleProviderForDb.py +0 -0
  130. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/sqla_orm/VortexTupleJson.py +0 -0
  131. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/sqla_orm/__init__.py +0 -0
  132. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/storage/TupleActionStorageSqlite.py +0 -0
  133. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/storage/TupleStorageSqlite.py +0 -0
  134. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/storage/__init__.py +0 -0
  135. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/tuple_json_schema/__init__.py +0 -0
  136. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/tuple_json_schema/constants.py +0 -0
  137. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/tuple_json_schema/json_schema_node_tuple.py +0 -0
  138. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_builder.py +0 -0
  139. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_test.py +0 -0
  140. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/tuple_json_schema/tuple_json_schema_test_tuples.py +0 -0
  141. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/tuple_test_fixtures.py +0 -0
  142. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortex/twisted_runtime_metrics_test.py +0 -0
  143. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortexpy.egg-info/dependency_links.txt +0 -0
  144. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortexpy.egg-info/entry_points.txt +0 -0
  145. {vortexpy-4.3.11 → vortexpy-4.3.12}/vortexpy.egg-info/requires.txt +0 -0
  146. {vortexpy-4.3.11 → 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.11
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.11"
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
@@ -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)
@@ -0,0 +1,292 @@
1
+ # Copyright (c) 2016-2026 Flying Ion Pty Ltd
2
+ # This software is open source; the MIT license applies.
3
+ # See LICENSE for the full license text.
4
+ #
5
+ # Website : https://flyingion.com
6
+ # Support : contact@flyingion.com
7
+
8
+ from __future__ import annotations
9
+
10
+ import gc
11
+ import logging
12
+ import threading
13
+ from collections.abc import Generator
14
+
15
+ import pytest
16
+ from pytest_twisted import ensureDeferred # type: ignore[import-untyped]
17
+ from twisted.internet import threads
18
+ from twisted.internet.defer import CancelledError
19
+ from twisted.internet.defer import Deferred
20
+ from twisted.internet.defer import inlineCallbacks
21
+ from twisted.internet.task import Clock
22
+ from twisted.python.failure import Failure
23
+
24
+ from vortex import VortexUtil
25
+ from vortex.DeferUtil import deferToThreadWrapWithLogger
26
+ from vortex.VortexUtil import LimitConcurrencyTimeoutError
27
+ from vortex.VortexUtil import limitConcurrency
28
+ from vortex.twisted_runtime_metrics import TwistedRuntimeMetrics
29
+ from vortex.twisted_runtime_metrics_test import (
30
+ utilResetTwistedRuntimeMetricsSingletonForTests,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ _KILL_SECONDS = 10.0
36
+
37
+
38
+ @pytest.fixture(autouse=True)
39
+ def resetMetricsSingleton() -> Generator[None, None, None]:
40
+ utilResetTwistedRuntimeMetricsSingletonForTests()
41
+ yield
42
+ utilResetTwistedRuntimeMetricsSingletonForTests()
43
+
44
+
45
+ @pytest.fixture
46
+ def clock(monkeypatch: pytest.MonkeyPatch) -> Clock:
47
+ clock = Clock()
48
+ monkeypatch.setattr(VortexUtil, "reactor", clock)
49
+ return clock
50
+
51
+
52
+ class _SwitchableWorker:
53
+ """One decorated method: parks forever or completes, by flag."""
54
+
55
+ def __init__(self) -> None:
56
+ self.behaviour = "stuck"
57
+ self.cancelledCount = 0
58
+
59
+ @limitConcurrency(logger, 1, killOrQuarantineCallsLongerThanSeconds=_KILL_SECONDS)
60
+ @inlineCallbacks
61
+ def work(self) -> Generator[Deferred, object, str]:
62
+ if self.behaviour == "fast":
63
+ return "done"
64
+
65
+ try:
66
+ yield Deferred() # Never fires, the call is stuck
67
+ except CancelledError:
68
+ self.cancelledCount += 1
69
+ raise
70
+ return "unreachable"
71
+
72
+
73
+ class _ConcurrencyTwoWorker:
74
+ def __init__(self) -> None:
75
+ self.cancelledCount = 0
76
+
77
+ @limitConcurrency(logger, 2, killOrQuarantineCallsLongerThanSeconds=_KILL_SECONDS)
78
+ @inlineCallbacks
79
+ def work(self) -> Generator[Deferred, object, str]:
80
+ try:
81
+ yield Deferred() # Never fires, the call is stuck
82
+ except CancelledError:
83
+ self.cancelledCount += 1
84
+ raise
85
+ return "unreachable"
86
+
87
+
88
+ class _NoTimeoutWorker:
89
+ @limitConcurrency(logger, 1)
90
+ @inlineCallbacks
91
+ def work(self) -> Generator[Deferred, object, str]:
92
+ yield Deferred() # Never fires, the call is stuck
93
+ return "unreachable"
94
+
95
+
96
+ class _ThreadedWorker:
97
+ """A limiter-wrapped call that blocks in a real threadpool thread."""
98
+
99
+ def __init__(self) -> None:
100
+ self.behaviour = "stuck"
101
+ self.threadStarted = threading.Event()
102
+ self.threadRelease = threading.Event()
103
+ self.threadFinished = threading.Event()
104
+ self.threadCompleted = False
105
+
106
+ @limitConcurrency(logger, 1, killOrQuarantineCallsLongerThanSeconds=_KILL_SECONDS)
107
+ def work(self) -> Deferred:
108
+ return self._threadedWork()
109
+
110
+ @deferToThreadWrapWithLogger(logger)
111
+ def _threadedWork(self) -> str:
112
+ if self.behaviour == "fast":
113
+ return "done"
114
+
115
+ self.threadStarted.set()
116
+ self.threadRelease.wait(timeout=30)
117
+ self.threadCompleted = True
118
+ self.threadFinished.set()
119
+ return "thread-done"
120
+
121
+
122
+ def _callerFailure(caller: Deferred) -> Failure:
123
+ assert caller.called, "caller Deferred must have fired"
124
+ assert isinstance(caller.result, Failure), (
125
+ f"caller must errback, got {caller.result!r}"
126
+ )
127
+ return caller.result
128
+
129
+
130
+ def test_stuck_call_times_out_kills_generator_and_frees_slot(clock) -> None:
131
+ worker = _SwitchableWorker()
132
+
133
+ caller1 = worker.work()
134
+ assert not caller1.called, "stuck call must not fire before the timeout"
135
+
136
+ clock.advance(_KILL_SECONDS)
137
+
138
+ failure = _callerFailure(caller1)
139
+ failure.trap(LimitConcurrencyTimeoutError)
140
+ assert worker.cancelledCount == 1, (
141
+ "CancelledError must reach the inlineCallbacks generator"
142
+ )
143
+
144
+ worker.behaviour = "fast"
145
+ caller2 = worker.work()
146
+ assert caller2.called and caller2.result == "done", (
147
+ "the timed out call must free its concurrency slot"
148
+ )
149
+
150
+ snapshot = TwistedRuntimeMetrics.snapshotAndResetWindow()
151
+ assert snapshot.limitConcurrencyCallTimeoutCount == 1, (
152
+ "one call timeout must be recorded in the alarm metrics"
153
+ )
154
+ assert snapshot.limitConcurrencyLastCallTimeoutMethodName is not None
155
+ assert snapshot.limitConcurrencyLastCallTimeoutMethodName.endswith("work")
156
+
157
+
158
+ def test_fast_call_is_not_timed_out(clock) -> None:
159
+ worker = _SwitchableWorker()
160
+ worker.behaviour = "fast"
161
+
162
+ caller = worker.work()
163
+ clock.advance(_KILL_SECONDS * 2)
164
+
165
+ assert caller.called and caller.result == "done", (
166
+ "fast call must complete normally and cancel its watchdog"
167
+ )
168
+
169
+ snapshot = TwistedRuntimeMetrics.snapshotAndResetWindow()
170
+ assert snapshot.limitConcurrencyCallTimeoutCount == 0, (
171
+ "no timeout may be recorded for a fast call"
172
+ )
173
+
174
+
175
+ def test_timeouts_are_per_call_at_concurrency_two(clock) -> None:
176
+ worker = _ConcurrencyTwoWorker()
177
+
178
+ caller1 = worker.work()
179
+ caller2 = worker.work()
180
+ clock.advance(_KILL_SECONDS)
181
+
182
+ _callerFailure(caller1).trap(LimitConcurrencyTimeoutError)
183
+ _callerFailure(caller2).trap(LimitConcurrencyTimeoutError)
184
+ assert worker.cancelledCount == 2, "each started call must have its own watchdog"
185
+
186
+ snapshot = TwistedRuntimeMetrics.snapshotAndResetWindow()
187
+ assert snapshot.limitConcurrencyCallTimeoutCount == 2
188
+
189
+
190
+ def test_no_timeout_when_parameter_not_set(clock) -> None:
191
+ worker = _NoTimeoutWorker()
192
+
193
+ caller = worker.work()
194
+ clock.advance(1000)
195
+
196
+ assert not caller.called, (
197
+ "without killOrQuarantineCallsLongerThanSeconds, calls must not be timed out"
198
+ )
199
+
200
+ snapshot = TwistedRuntimeMetrics.snapshotAndResetWindow()
201
+ assert snapshot.limitConcurrencyCallTimeoutCount == 0
202
+
203
+
204
+ def test_timed_out_call_is_registered_for_manhole(clock) -> None:
205
+ # A function-local class gets its own limiter Wrap, isolated from the
206
+ # module-level workers' shared Wraps.
207
+ class _ManholeWorker:
208
+ @limitConcurrency(
209
+ logger, 1, killOrQuarantineCallsLongerThanSeconds=_KILL_SECONDS
210
+ )
211
+ @inlineCallbacks
212
+ def work(self) -> Generator[Deferred, object, str]:
213
+ yield Deferred() # Never fires, the call is stuck
214
+ return "unreachable"
215
+
216
+ worker = _ManholeWorker()
217
+
218
+ caller1 = worker.work()
219
+ clock.advance(_KILL_SECONDS)
220
+ _callerFailure(caller1).trap(LimitConcurrencyTimeoutError)
221
+
222
+ # Reach the limiter the way manhole inspection does in production.
223
+ wraps = [
224
+ o
225
+ for o in gc.get_objects()
226
+ if type(o).__name__ == "Wrap"
227
+ and hasattr(o, "_callQueue")
228
+ and any(
229
+ entry.methodQualname.endswith("_ManholeWorker.work")
230
+ for entry in o._quarantinedCalls
231
+ )
232
+ ]
233
+ assert wraps == [], (
234
+ "a killed inlineCallbacks call must not be quarantine-registered;"
235
+ " only timed out threaded calls accumulate"
236
+ )
237
+
238
+
239
+ @pytest.mark.twisted
240
+ @ensureDeferred
241
+ async def test_threaded_call_is_quarantined_not_killed(clock) -> None:
242
+ worker = _ThreadedWorker()
243
+
244
+ caller1 = worker.work()
245
+ await threads.deferToThread(worker.threadStarted.wait, 5)
246
+
247
+ clock.advance(_KILL_SECONDS)
248
+
249
+ with pytest.raises(LimitConcurrencyTimeoutError):
250
+ await caller1
251
+
252
+ worker.behaviour = "fast"
253
+ result = await worker.work()
254
+ assert result == "done", (
255
+ "a quarantined call must be moved out of the limited count:"
256
+ " the freed slot serves the next call while the thread runs on"
257
+ )
258
+
259
+ assert worker.threadCompleted is False, (
260
+ "the quarantined thread must still be blocked; it cannot be killed"
261
+ )
262
+
263
+ worker.threadRelease.set()
264
+ await threads.deferToThread(worker.threadFinished.wait, 5)
265
+ assert worker.threadCompleted is True, (
266
+ "the late thread result must be discarded without AlreadyCalledError"
267
+ )
268
+
269
+ # The timed out threaded Deferred is accumulated in the limiter's
270
+ # quarantine registry, reachable the way manhole inspection works.
271
+ wraps = [
272
+ o
273
+ for o in gc.get_objects()
274
+ if type(o).__name__ == "Wrap"
275
+ and hasattr(o, "_callQueue")
276
+ and any(
277
+ entry.methodQualname.endswith("_ThreadedWorker.work")
278
+ for entry in o._quarantinedCalls
279
+ )
280
+ ]
281
+ assert len(wraps) == 1, "one limiter must hold the quarantined thread"
282
+
283
+ entry = wraps[0]._quarantinedCalls[0]
284
+ assert entry.startDate <= entry.timeoutDate, (
285
+ "quarantine entry must carry the call start and timeout dates"
286
+ )
287
+ assert getattr(entry.deferred, "_isVortexThreadedCall", False), (
288
+ "the registry must hold the threaded Deferred itself"
289
+ )
290
+
291
+ snapshot = TwistedRuntimeMetrics.snapshotAndResetWindow()
292
+ assert snapshot.limitConcurrencyCallTimeoutCount == 1
@@ -6,4 +6,4 @@
6
6
  # Website : https://flyingion.com
7
7
  # Support : contact@flyingion.com
8
8
 
9
- __version__ = '4.3.11'
9
+ __version__ = '4.3.12'
@@ -244,7 +244,14 @@ class TupleDataObservableHandler:
244
244
  if d:
245
245
  d.addErrback(vortexLogFailure, logger, consumeError=True)
246
246
 
247
- @limitConcurrency(logger, 25, deduplicateBasedOnKwArgsKey=_dedupProcessorCallKeys)
247
+ @limitConcurrency(
248
+ logger,
249
+ 25,
250
+ deduplicateBasedOnKwArgsKey=_dedupProcessorCallKeys,
251
+ # Legitimate slow queries run to ~2 minutes; 300s bounds slot
252
+ # starvation to 5 minutes without killing healthy calls.
253
+ killOrQuarantineCallsLongerThanSeconds=300.0,
254
+ )
248
255
  def _processLimitedDecorated(
249
256
  self,
250
257
  payloadEnvelope: PayloadEnvelope,
@@ -389,10 +396,32 @@ class TupleDataObservableHandler:
389
396
  vortexUuid
390
397
  ] = vs
391
398
 
392
- vortexMsg = yield self._createVortexMsg(
393
- payloadEnvelope.filt,
394
- tupleSelector,
395
- )
399
+ try:
400
+ vortexMsg = yield self._createVortexMsg(
401
+ payloadEnvelope.filt,
402
+ tupleSelector,
403
+ )
404
+ except Exception as e:
405
+ # The caller is waiting on a PayloadResponse; without an error
406
+ # response it waits the full client timeout for every failure.
407
+ logger.error(
408
+ "Observable:%s provider failed for TupleSelector = %s,"
409
+ " sending error response",
410
+ self._observableName,
411
+ tsStr,
412
+ )
413
+ logger.exception(e)
414
+ # DatabaseError strings include the full SQL and parameters;
415
+ # the leading characters carry the actual error code.
416
+ errorText = f"{e.__class__.__name__}: {e}"[:500]
417
+ errorEnvelope = PayloadEnvelope(
418
+ filt=payloadEnvelope.filt,
419
+ result=f"Tuple provider failed on the server: {errorText}",
420
+ )
421
+ errorVortexMsg = yield errorEnvelope.toVortexMsgDefer()
422
+ yield sendResponse(errorVortexMsg)
423
+ return
424
+
396
425
  try:
397
426
  yield sendResponse(vortexMsg)
398
427
  except Exception as e:
@@ -390,3 +390,49 @@ async def test_notify_unsubscribe_during_provider_yield_does_not_raise() -> None
390
390
  await notifyDeferred
391
391
 
392
392
  sendVortexMsgMock.assert_not_called()
393
+
394
+
395
+ class _FailingProvider(TupleDataProviderABC):
396
+ requiredPermissions = frozenset()
397
+
398
+ def makeVortexMsg(
399
+ self,
400
+ filt: dict,
401
+ tupleSelector: TupleSelector,
402
+ ) -> bytes:
403
+ raise ValueError("ORA-00942: table or view does not exist")
404
+
405
+
406
+ @pytest.mark.twisted
407
+ @ensureDeferred
408
+ async def test_provider_failure_sends_error_response() -> None:
409
+ handler = TupleDataObservableHandler("testObservable")
410
+ handler.addTupleProvider(_TUPLE_NAME, _FailingProvider())
411
+ sendResponseCalls: list[bytes] = []
412
+
413
+ def sendResponse(vortexMsg: bytes) -> Deferred[None]:
414
+ sendResponseCalls.append(vortexMsg)
415
+ return succeed(None)
416
+
417
+ with patch(
418
+ "vortex.handler.TupleDataObservableHandler.VortexFactory.isVortexUuidOnline",
419
+ return_value=True,
420
+ ):
421
+ await handler._process(
422
+ _pollEnvelope(), _VORTEX_UUID, "client-vortex", sendResponse
423
+ )
424
+
425
+ assert len(sendResponseCalls) == 1, (
426
+ "provider failure must send an error response, not silence"
427
+ )
428
+
429
+ errorEnvelope = await PayloadEnvelope().fromVortexMsgDefer(sendResponseCalls[0])
430
+ assert errorEnvelope.result not in (None, True), (
431
+ "error response result must fail the client resultCheck"
432
+ )
433
+ assert "ORA-00942" in errorEnvelope.result, (
434
+ "error response must carry the provider error text"
435
+ )
436
+ assert (
437
+ errorEnvelope.filt["tupleSelector"].toJsonStr() == _TUPLE_SELECTOR.toJsonStr()
438
+ ), "error response must preserve the request filt"
@@ -418,8 +418,9 @@ class TupleDataObservableProxyHandler(TupleDataObservableCache):
418
418
 
419
419
  # Send the vortex messages
420
420
  for vortexUuid in observingUuids:
421
- reactor.callLater(
422
- 0,
421
+ # This method runs on a thread-pool thread; callLater is
422
+ # reactor-thread-only, so schedule with thread-safe callFromThread.
423
+ reactor.callFromThread(
423
424
  self._sendVortexMsgInReactor,
424
425
  vortexMsg=vortexMsg,
425
426
  destVortexUuid=vortexUuid,
@@ -59,6 +59,8 @@ class TwistedRuntimeMetricsSnapshot(Tuple):
59
59
  limitConcurrencyLimiterCount: int = TupleField()
60
60
  limitConcurrencySaturatedCount: int = TupleField()
61
61
  limitConcurrencyCallCount: int = TupleField()
62
+ limitConcurrencyCallTimeoutCount: int = TupleField()
63
+ limitConcurrencyLastCallTimeoutMethodName: str | None = TupleField(None)
62
64
  longestLimitConcurrencyMethodName: str | None = TupleField(None)
63
65
  longestLimitConcurrencyMethodMs: float = TupleField()
64
66
  longestLimitConcurrencyQueueWaitMs: float = TupleField()
@@ -119,6 +121,8 @@ class TwistedRuntimeMetrics:
119
121
  _LimitConcurrencyLimiterRegistration
120
122
  ] = []
121
123
  self._limitConcurrencyCallCount = 0
124
+ self._limitConcurrencyCallTimeoutCount = 0
125
+ self._limitConcurrencyLastCallTimeoutMethodName: str | None = None
122
126
  self._longestLimitConcurrencyMethodName: str | None = None
123
127
  self._longestLimitConcurrencyMethodMs = 0.0
124
128
  self._longestLimitConcurrencyQueueWaitMs = 0.0
@@ -229,6 +233,11 @@ class TwistedRuntimeMetrics:
229
233
  self._longestLimitConcurrencyQueueWaitMs = queueWaitMs
230
234
  self._longestLimitConcurrencyQueueWaitMethod = methodQualname
231
235
 
236
+ def _recordLimitConcurrencyCallTimeout(self, methodQualname: str) -> None:
237
+ with self._metricsLock:
238
+ self._limitConcurrencyCallTimeoutCount += 1
239
+ self._limitConcurrencyLastCallTimeoutMethodName = methodQualname
240
+
232
241
  def _snapshotLimitConcurrencyStateLocked(
233
242
  self,
234
243
  ) -> tuple[
@@ -308,6 +317,12 @@ class TwistedRuntimeMetrics:
308
317
  limitConcurrencyLimiterCount=limitConcurrencyLimiterCount,
309
318
  limitConcurrencySaturatedCount=limitConcurrencySaturatedCount,
310
319
  limitConcurrencyCallCount=self._limitConcurrencyCallCount,
320
+ limitConcurrencyCallTimeoutCount=(
321
+ self._limitConcurrencyCallTimeoutCount
322
+ ),
323
+ limitConcurrencyLastCallTimeoutMethodName=(
324
+ self._limitConcurrencyLastCallTimeoutMethodName
325
+ ),
311
326
  longestLimitConcurrencyMethodName=(
312
327
  self._longestLimitConcurrencyMethodName
313
328
  ),
@@ -354,6 +369,8 @@ class TwistedRuntimeMetrics:
354
369
  self._loopingCallLastExceptionMethod = None
355
370
  self._loopingCallLastExceptionFrame = None
356
371
  self._limitConcurrencyCallCount = 0
372
+ self._limitConcurrencyCallTimeoutCount = 0
373
+ self._limitConcurrencyLastCallTimeoutMethodName = None
357
374
  self._longestLimitConcurrencyMethodName = None
358
375
  self._longestLimitConcurrencyMethodMs = 0.0
359
376
  self._longestLimitConcurrencyQueueWaitMs = 0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortexpy
3
- Version: 4.3.11
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
@@ -72,6 +72,7 @@ vortex/VortexTcpMemoryLeakPayloadTest.py
72
72
  vortex/VortexTcpMemoryLeakRawTest.py
73
73
  vortex/VortexTest.py
74
74
  vortex/VortexUtil.py
75
+ vortex/VortexUtil_test.py
75
76
  vortex/VortexWritePushProducer.py
76
77
  vortex/VortexWritePushProducer_test.py
77
78
  vortex/__init__.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes