vortexpy 4.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vortex/DataWrapTuple.py +27 -0
- vortex/DeferUtil.py +224 -0
- vortex/Jsonable.py +304 -0
- vortex/Payload.py +115 -0
- vortex/PayloadEndpoint.py +216 -0
- vortex/PayloadEndpointTest.py +164 -0
- vortex/PayloadEnvelope.py +166 -0
- vortex/PayloadFilterKeys.py +15 -0
- vortex/PayloadIO.py +152 -0
- vortex/PayloadPriority.py +11 -0
- vortex/PayloadResponse.py +173 -0
- vortex/PayloadResponseTest.py +39 -0
- vortex/PayloadTest.py +136 -0
- vortex/PerformTestActionTuple.py +30 -0
- vortex/SerialiseUtil.py +273 -0
- vortex/TestTuple.py +61 -0
- vortex/Tuple.py +968 -0
- vortex/TupleAction.py +72 -0
- vortex/TupleActionVortex.py +5 -0
- vortex/TupleDataForTest.py +25 -0
- vortex/TuplePerfTest.py +71 -0
- vortex/TuplePolymorphicTupleTypeFieldArgTest.py +74 -0
- vortex/TupleRestfulTest.py +140 -0
- vortex/TupleSelector.py +105 -0
- vortex/TupleTest.py +127 -0
- vortex/VortexABC.py +55 -0
- vortex/VortexClientHttp.py +308 -0
- vortex/VortexClientHttpTest.py +71 -0
- vortex/VortexClientTcp.py +355 -0
- vortex/VortexClientWebsocketFactory.py +383 -0
- vortex/VortexConnectionABC.py +88 -0
- vortex/VortexFactory.py +932 -0
- vortex/VortexPayloadProtocol.py +180 -0
- vortex/VortexServer.py +367 -0
- vortex/VortexServerConnection.py +127 -0
- vortex/VortexServerHttpResource.py +193 -0
- vortex/VortexServerTcp.py +104 -0
- vortex/VortexServerWebsocket.py +287 -0
- vortex/VortexTcpMemoryLeakPayloadTest.py +611 -0
- vortex/VortexTcpMemoryLeakRawTest.py +87 -0
- vortex/VortexTest.py +22 -0
- vortex/VortexUtil.py +314 -0
- vortex/VortexWritePushProducer.py +321 -0
- vortex/__init__.py +10 -0
- vortex/data_loader/TupleDataLiveLockManager.py +383 -0
- vortex/data_loader/TupleDataLoader.py +326 -0
- vortex/data_loader/TupleDataLoaderDelegate.py +90 -0
- vortex/data_loader/TupleDataLoaderTupleABC.py +10 -0
- vortex/data_loader/TupleDataLoaderTuples.py +53 -0
- vortex/data_loader/__init__.py +0 -0
- vortex/event_bus/TupleChangeEventABC.py +15 -0
- vortex/event_bus/TupleChangeEventBus.py +192 -0
- vortex/event_bus/TupleChangeEventBusObserverABC.py +11 -0
- vortex/event_bus/__init__.py +0 -0
- vortex/handler/TupleActionProcessor.py +270 -0
- vortex/handler/TupleActionProcessorProxy.py +218 -0
- vortex/handler/TupleActionProcessorTestHandler.py +27 -0
- vortex/handler/TupleDataActionClient.py +67 -0
- vortex/handler/TupleDataObservableCache.py +201 -0
- vortex/handler/TupleDataObservableHandler.py +371 -0
- vortex/handler/TupleDataObservableProxyHandler.py +340 -0
- vortex/handler/TupleDataObservableTestHandler.py +60 -0
- vortex/handler/TupleDataObserverClient.py +158 -0
- vortex/handler/VortexJSTupleLoaderTestHandler.py +42 -0
- vortex/handler/__init__.py +0 -0
- vortex/restful/GzippedDataHttpClient.py +170 -0
- vortex/restful/GzippedDataHttpClientTest.py +103 -0
- vortex/restful/GzippedPayloadHttpClient.py +181 -0
- vortex/restful/RestfulHttpClient.py +107 -0
- vortex/restful/RestfulHttpClientTest.py +60 -0
- vortex/restful/RestfulResource.py +153 -0
- vortex/restful/RestfulResourceTest.py +100 -0
- vortex/restful/__init__.py +0 -0
- vortex/rpc/RPC.py +446 -0
- vortex/rpc/RPCTest.py +12 -0
- vortex/rpc/__init__.py +0 -0
- vortex/sqla_orm/OrmCrudHandler.py +431 -0
- vortex/sqla_orm/TupleProviderForDb.py +42 -0
- vortex/sqla_orm/VortexTupleJson.py +24 -0
- vortex/sqla_orm/__init__.py +0 -0
- vortex/storage/TupleStorageSqlite.py +359 -0
- vortex/storage/__init__.py +0 -0
- vortexpy-4.0.23.dist-info/LICENSE +21 -0
- vortexpy-4.0.23.dist-info/METADATA +43 -0
- vortexpy-4.0.23.dist-info/RECORD +87 -0
- vortexpy-4.0.23.dist-info/WHEEL +5 -0
- vortexpy-4.0.23.dist-info/top_level.txt +1 -0
vortex/VortexTest.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
* Created by Synerty Pty Ltd
|
|
3
|
+
*
|
|
4
|
+
* This software is open source, the MIT license applies.
|
|
5
|
+
*
|
|
6
|
+
* Website : http://www.synerty.com
|
|
7
|
+
* Support : support@synerty.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from twisted.trial import unittest
|
|
11
|
+
|
|
12
|
+
from run_test_vortex_server import setupVortexServer
|
|
13
|
+
from vortex.VortexClientHttp import VortexClientHttp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VortexTest(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
self.port = setupVortexServer()
|
|
19
|
+
|
|
20
|
+
def testVortexConnect(self):
|
|
21
|
+
client = VortexClientHttp("unittest")
|
|
22
|
+
return client.connect("127.0.0.1", self.port)
|
vortex/VortexUtil.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import Callable
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from typing import Set
|
|
10
|
+
from typing import Union
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
import pytz
|
|
14
|
+
from twisted.internet import reactor
|
|
15
|
+
from twisted.internet.defer import Deferred
|
|
16
|
+
from twisted.internet.defer import fail
|
|
17
|
+
from twisted.internet.defer import maybeDeferred
|
|
18
|
+
from twisted.python.failure import Failure
|
|
19
|
+
|
|
20
|
+
from vortex.PayloadEnvelope import PayloadEnvelope
|
|
21
|
+
from vortex.PayloadEnvelope import VortexMsgList
|
|
22
|
+
from vortex.Tuple import Tuple
|
|
23
|
+
from vortex.Tuple import TupleField
|
|
24
|
+
from vortex.Tuple import addTupleType
|
|
25
|
+
|
|
26
|
+
_LimitConcurrencyArgsTuple = namedtuple(
|
|
27
|
+
"_LimitConcurrencyArgsTuple",
|
|
28
|
+
("deferred", "funcSelf", "args", "kwargs", "queuedTime"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _dedupProcessorCallKeys(
|
|
33
|
+
payloadEnvelope: PayloadEnvelope,
|
|
34
|
+
vortexUuid: str,
|
|
35
|
+
*args,
|
|
36
|
+
**kwargs,
|
|
37
|
+
):
|
|
38
|
+
filt = payloadEnvelope.filt.copy()
|
|
39
|
+
from vortex.PayloadResponse import PayloadResponse
|
|
40
|
+
|
|
41
|
+
filt.pop(PayloadResponse.MESSAGE_ID_KEY, None)
|
|
42
|
+
return vortexUuid, str(filt)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def limitConcurrency(
|
|
46
|
+
decoratorLogger,
|
|
47
|
+
concurrency: int,
|
|
48
|
+
logTaskTimeGreaterThanSeconds=5.0,
|
|
49
|
+
deduplicateBasedOnKwArgsKey: Optional[Callable] = None,
|
|
50
|
+
):
|
|
51
|
+
"""Limit Concurrency to a call, decorator
|
|
52
|
+
|
|
53
|
+
:param decoratorLogger: The logger to log messages for.
|
|
54
|
+
:param concurrency: The number of concurrent calls
|
|
55
|
+
:param logTaskTimeGreaterThanSeconds: If a task takes longer then this
|
|
56
|
+
:param deduplicateBasedOnKwArgsKey: This is a callable that returns a tuple
|
|
57
|
+
of strings. If this argument is provided,
|
|
58
|
+
then duplicate calls with the same values for these arguments will
|
|
59
|
+
be deduplicated. Duplicate calls will use the newest set of arguments.
|
|
60
|
+
|
|
61
|
+
# As of python 3.6, dicts are ordered.
|
|
62
|
+
|
|
63
|
+
>>> x={}
|
|
64
|
+
>>> x[1]=None
|
|
65
|
+
>>> x
|
|
66
|
+
{1: None}
|
|
67
|
+
>>> x[0]=None
|
|
68
|
+
>>> x
|
|
69
|
+
{1: None, 0: None}
|
|
70
|
+
>>> x[123]=None
|
|
71
|
+
>>> x
|
|
72
|
+
{1: None, 0: None, 123: None}
|
|
73
|
+
# Updating a item in the dict retains the same position
|
|
74
|
+
>>> x[0]=None
|
|
75
|
+
>>> x
|
|
76
|
+
{1: None, 0: None, 123: None}
|
|
77
|
+
|
|
78
|
+
# Poping the first off can be done with an iterator.
|
|
79
|
+
>>> next(iter(x.keys()))
|
|
80
|
+
1
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
class Wrap:
|
|
85
|
+
def __init__(self, f):
|
|
86
|
+
self._f = f
|
|
87
|
+
self._callQueue: dict[Any, _LimitConcurrencyArgsTuple] = {}
|
|
88
|
+
self._runningCount = 0
|
|
89
|
+
self._processedCount = 0
|
|
90
|
+
self._lastLogTime = None
|
|
91
|
+
self._lastQueueProcessingDelta = None
|
|
92
|
+
|
|
93
|
+
def _callComplete(self, result, callStartTime):
|
|
94
|
+
self._runningCount -= 1
|
|
95
|
+
self._processedCount += 1
|
|
96
|
+
# decoratorLogger.debug("Queued task completed %s", str(self._f))
|
|
97
|
+
|
|
98
|
+
processingTime = datetime.now(pytz.utc) - callStartTime
|
|
99
|
+
if logTaskTimeGreaterThanSeconds < processingTime.total_seconds():
|
|
100
|
+
decoratorLogger.debug(
|
|
101
|
+
"%s call took %s",
|
|
102
|
+
self._f.__name__,
|
|
103
|
+
processingTime,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
reactor.callLater(0, self._processNext)
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
def _processNext(self):
|
|
110
|
+
now = datetime.now(pytz.utc)
|
|
111
|
+
while self._runningCount < concurrency and self._callQueue:
|
|
112
|
+
# decoratorLogger.debug("Queueing new task %s", str(self._f))
|
|
113
|
+
self._runningCount += 1
|
|
114
|
+
|
|
115
|
+
nextKey = next(iter(self._callQueue.keys()))
|
|
116
|
+
|
|
117
|
+
args = self._callQueue.pop(nextKey)
|
|
118
|
+
|
|
119
|
+
self._lastQueueProcessingDelta = now - args.queuedTime
|
|
120
|
+
|
|
121
|
+
callWithArgs = lambda: self._f(
|
|
122
|
+
args.funcSelf, *args.args, **args.kwargs
|
|
123
|
+
)
|
|
124
|
+
d = maybeDeferred(callWithArgs)
|
|
125
|
+
d.addBoth(self._callComplete, now)
|
|
126
|
+
d.chainDeferred(args.deferred)
|
|
127
|
+
|
|
128
|
+
self._logQueueSize()
|
|
129
|
+
self._f.running = self._runningCount
|
|
130
|
+
self._f.waiting = len(self._callQueue)
|
|
131
|
+
|
|
132
|
+
def call(self, funcSelf, *args, **kwargs):
|
|
133
|
+
if deduplicateBasedOnKwArgsKey:
|
|
134
|
+
key = deduplicateBasedOnKwArgsKey(*args, **kwargs)
|
|
135
|
+
else:
|
|
136
|
+
key = uuid4()
|
|
137
|
+
|
|
138
|
+
if key in self._callQueue:
|
|
139
|
+
decoratorLogger.debug(
|
|
140
|
+
"%s call was deduplicated for key %s",
|
|
141
|
+
self._f.__name__,
|
|
142
|
+
str(key),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
d = Deferred()
|
|
146
|
+
self._callQueue[key] = _LimitConcurrencyArgsTuple(
|
|
147
|
+
d, funcSelf, args, kwargs, datetime.now(pytz.utc)
|
|
148
|
+
)
|
|
149
|
+
self._processNext()
|
|
150
|
+
return d
|
|
151
|
+
|
|
152
|
+
def _logQueueSize(self):
|
|
153
|
+
if (
|
|
154
|
+
self._lastLogTime
|
|
155
|
+
and (datetime.now(pytz.utc) - self._lastLogTime).total_seconds()
|
|
156
|
+
< 5.0
|
|
157
|
+
):
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
deltaTime = self._lastQueueProcessingDelta
|
|
161
|
+
queueSize = len(self._callQueue)
|
|
162
|
+
|
|
163
|
+
def logIt(logFunc):
|
|
164
|
+
self._lastLogTime = datetime.now(pytz.utc)
|
|
165
|
+
logFunc(
|
|
166
|
+
"%s last call was queued for %s, queue size %s,"
|
|
167
|
+
" processed %s since last log message",
|
|
168
|
+
self._f.__name__,
|
|
169
|
+
deltaTime,
|
|
170
|
+
queueSize,
|
|
171
|
+
self._processedCount,
|
|
172
|
+
)
|
|
173
|
+
self._processedCount = 0
|
|
174
|
+
|
|
175
|
+
if 15.0 < deltaTime.total_seconds() or 2000 < queueSize:
|
|
176
|
+
logIt(decoratorLogger.warning)
|
|
177
|
+
|
|
178
|
+
elif 5.0 < deltaTime.total_seconds() or 200 < queueSize:
|
|
179
|
+
logIt(decoratorLogger.debug)
|
|
180
|
+
|
|
181
|
+
def wrap(f):
|
|
182
|
+
wrapC = Wrap(f)
|
|
183
|
+
|
|
184
|
+
def wrapInner(funcSelf, *args, **kwargs):
|
|
185
|
+
return wrapC.call(funcSelf, *args, **kwargs)
|
|
186
|
+
|
|
187
|
+
return wrapInner
|
|
188
|
+
|
|
189
|
+
return wrap
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@addTupleType
|
|
193
|
+
class _DebounceArgsTuple(Tuple):
|
|
194
|
+
__tupleType__ = __name__ + "._DebounceArgsTuple"
|
|
195
|
+
args = TupleField()
|
|
196
|
+
kwargs = TupleField()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def debounceCall(debounceSeconds: float):
|
|
200
|
+
"""Call a debounced function that delays invoking wrapped function until after wait n seconds
|
|
201
|
+
|
|
202
|
+
:param debounceSeconds: cooling down peroid of n seconds
|
|
203
|
+
:return: the wrapped function
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
class Wrap:
|
|
207
|
+
_lastCleanup = datetime.now(pytz.utc)
|
|
208
|
+
_cleanupDelta = timedelta(minutes=1, seconds=debounceSeconds)
|
|
209
|
+
|
|
210
|
+
_debounceTimeDelta = timedelta(seconds=debounceSeconds)
|
|
211
|
+
|
|
212
|
+
_calls: Dict[str, datetime] = {}
|
|
213
|
+
_callsQueued: Set[tuple] = set()
|
|
214
|
+
|
|
215
|
+
def __init__(self, f):
|
|
216
|
+
self._f = f
|
|
217
|
+
|
|
218
|
+
def __cleanup(self):
|
|
219
|
+
now = datetime.now(pytz.utc)
|
|
220
|
+
# Cleanup the old values
|
|
221
|
+
if self._cleanupDelta < now - self._lastCleanup:
|
|
222
|
+
self._lastCleanup = now
|
|
223
|
+
|
|
224
|
+
for key, val in list(self._calls.items()):
|
|
225
|
+
if (
|
|
226
|
+
self._cleanupDelta < now - val
|
|
227
|
+
and key not in self._callsQueued
|
|
228
|
+
):
|
|
229
|
+
self._calls.pop(key)
|
|
230
|
+
|
|
231
|
+
def __wrapArgsAndKwargs(self, *args, **kwargs):
|
|
232
|
+
if not args and not kwargs:
|
|
233
|
+
return ""
|
|
234
|
+
hashedArgs = _DebounceArgsTuple(
|
|
235
|
+
args=args, kwargs=kwargs
|
|
236
|
+
).toJsonDict()
|
|
237
|
+
return json.dumps(hashedArgs)
|
|
238
|
+
|
|
239
|
+
def call(self, funcSelf, *args, **kwargs):
|
|
240
|
+
now = datetime.now(pytz.utc)
|
|
241
|
+
|
|
242
|
+
# Create a hash of the args
|
|
243
|
+
hashedArgs = self.__wrapArgsAndKwargs(args, kwargs)
|
|
244
|
+
self.__cleanup()
|
|
245
|
+
|
|
246
|
+
# If this method has never been called before, call it
|
|
247
|
+
lastCallDate = self._calls.get(hashedArgs)
|
|
248
|
+
if not lastCallDate:
|
|
249
|
+
self._calls[hashedArgs] = now
|
|
250
|
+
return self._f(funcSelf, *args, **kwargs)
|
|
251
|
+
|
|
252
|
+
# Work out how long it's been since this method has been called
|
|
253
|
+
timeSinceLastCall = now - lastCallDate
|
|
254
|
+
|
|
255
|
+
# If this method is overdue for a call, call it
|
|
256
|
+
if timedelta(seconds=debounceSeconds) < timeSinceLastCall:
|
|
257
|
+
# If this call was queued, remove it
|
|
258
|
+
if hashedArgs in self._callsQueued:
|
|
259
|
+
self._callsQueued.remove(hashedArgs)
|
|
260
|
+
self._calls[hashedArgs] = now
|
|
261
|
+
return self._f(funcSelf, *args, **kwargs)
|
|
262
|
+
|
|
263
|
+
timeToNextCall = (
|
|
264
|
+
timedelta(seconds=debounceSeconds) - timeSinceLastCall
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# If not, schedule this method for call after the debounce
|
|
268
|
+
# but only if there isn't already a call queued
|
|
269
|
+
if hashedArgs not in self._callsQueued:
|
|
270
|
+
self._callsQueued.add(hashedArgs)
|
|
271
|
+
reactor.callLater(
|
|
272
|
+
timeToNextCall.total_seconds() + 0.1,
|
|
273
|
+
self.call,
|
|
274
|
+
funcSelf,
|
|
275
|
+
*args,
|
|
276
|
+
**kwargs,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
def wrap(f):
|
|
284
|
+
wrapC = Wrap(f)
|
|
285
|
+
|
|
286
|
+
def wrapInner(funcSelf, *args, **kwargs):
|
|
287
|
+
return wrapC.call(funcSelf, *args, **kwargs)
|
|
288
|
+
|
|
289
|
+
return wrapInner
|
|
290
|
+
|
|
291
|
+
return wrap
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def logLargeMessages(
|
|
295
|
+
logger, vortexMsgs: Union[VortexMsgList, bytes], destVortexUuid: str
|
|
296
|
+
):
|
|
297
|
+
def logIt(vortexMsg):
|
|
298
|
+
# Log anything larger than 128kb
|
|
299
|
+
length = len(vortexMsg)
|
|
300
|
+
if length < 512 * 1024:
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
logger.debug(
|
|
304
|
+
"Sending vortexMsg of size %s to vortex %s",
|
|
305
|
+
"{:,}kB".format(int(length / 1024)),
|
|
306
|
+
destVortexUuid,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if isinstance(vortexMsgs, bytes):
|
|
310
|
+
logIt(vortexMsgs)
|
|
311
|
+
|
|
312
|
+
else:
|
|
313
|
+
for msg in vortexMsgs:
|
|
314
|
+
logIt(msg)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
* Created by Synerty Pty Ltd
|
|
3
|
+
*
|
|
4
|
+
* This software is open source, the MIT license applies.
|
|
5
|
+
*
|
|
6
|
+
* Website : http://www.synerty.com
|
|
7
|
+
* Support : support@synerty.com
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
import typing
|
|
11
|
+
import weakref
|
|
12
|
+
from collections import deque, defaultdict
|
|
13
|
+
from typing import Callable, Deque, Dict
|
|
14
|
+
|
|
15
|
+
from twisted.internet.interfaces import IPushProducer
|
|
16
|
+
from zope.interface import implementer
|
|
17
|
+
|
|
18
|
+
from vortex.DeferUtil import nonConcurrentMethod
|
|
19
|
+
from vortex.PayloadPriority import DEFAULT_PRIORITY
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(name=__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _format_size(size):
|
|
25
|
+
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
|
26
|
+
if abs(size) < 100 and unit != "B":
|
|
27
|
+
# 3 digits (xx.x UNIT)
|
|
28
|
+
return "%.1f %s" % (size, unit)
|
|
29
|
+
if abs(size) < 10 * 1024 or unit == "TiB":
|
|
30
|
+
# 4 or 5 digits (xxxx UNIT)
|
|
31
|
+
return "%.0f %s" % (size, unit)
|
|
32
|
+
size /= 1024
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@implementer(IPushProducer)
|
|
36
|
+
class VortexWritePushProducer:
|
|
37
|
+
WARNING_DATA_LENGTH = 50 * 1024 * 1024
|
|
38
|
+
ERROR_DATA_LENGTH = 500 * 1024 * 1024
|
|
39
|
+
WRITE_CHUNK_SIZE = 128 * 1024
|
|
40
|
+
|
|
41
|
+
__memoryLoggingRefs = None
|
|
42
|
+
__memoryLoggingEnabled = False
|
|
43
|
+
_DEBUG_LOGGING = False
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def setupMemoryLogging(cls) -> None:
|
|
47
|
+
cls.__memoryLoggingRefs = []
|
|
48
|
+
cls.__memoryLoggingEnabled = True
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def memoryLoggingDump(
|
|
52
|
+
cls, top=10, msgs=1
|
|
53
|
+
) -> typing.List[typing.Tuple[str, int, int]]:
|
|
54
|
+
if not cls.__memoryLoggingRefs:
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
# Filter out expired items
|
|
58
|
+
cls.__memoryLoggingRefs = list(
|
|
59
|
+
filter(lambda o: o(), cls.__memoryLoggingRefs)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
results = []
|
|
63
|
+
for producerRef in cls.__memoryLoggingRefs:
|
|
64
|
+
producer = producerRef()
|
|
65
|
+
if not producer:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
queueCount = sum(
|
|
69
|
+
[len(q) for q in producer._queueByPriority.values()]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
results.append(
|
|
73
|
+
(
|
|
74
|
+
producer._remoteVortexName,
|
|
75
|
+
queueCount,
|
|
76
|
+
producer._queuedDataLen,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
data = sorted(results, key=lambda x: x[2], reverse=True)
|
|
81
|
+
|
|
82
|
+
return list(filter(lambda x: x[2] >= msgs, data))[:top]
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
transport,
|
|
87
|
+
stopProducingCallback: Callable,
|
|
88
|
+
remoteVortexName: str = "Pending",
|
|
89
|
+
writeWholeFrames=False,
|
|
90
|
+
terminateFrameWithDot=True,
|
|
91
|
+
splitFrames=False,
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
:param splitFrames: Split frames that come in if they are larger than 128kb.
|
|
96
|
+
This allows better use of ram, as keeping a 1mb to ?mb bytes object around
|
|
97
|
+
is bad for memory management and memory fragmentation.
|
|
98
|
+
"""
|
|
99
|
+
if VortexWritePushProducer.__memoryLoggingEnabled:
|
|
100
|
+
VortexWritePushProducer.__memoryLoggingRefs.append(
|
|
101
|
+
weakref.ref(self)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self._transport = transport
|
|
105
|
+
self._stopProducingCallback = stopProducingCallback
|
|
106
|
+
self._remoteVortexName = remoteVortexName
|
|
107
|
+
self._writeWholeFrames = writeWholeFrames
|
|
108
|
+
self._terminateFrameWithDot = terminateFrameWithDot
|
|
109
|
+
self._splitFrames = splitFrames
|
|
110
|
+
|
|
111
|
+
self._paused = False
|
|
112
|
+
self._pausedDeferred = None
|
|
113
|
+
self._writingInProgress = False
|
|
114
|
+
self._writingFrameInProgress = False
|
|
115
|
+
self._closed = False
|
|
116
|
+
self._queuedDataLen = 0
|
|
117
|
+
self._queueByPriority: Dict[int, Deque] = defaultdict(deque)
|
|
118
|
+
|
|
119
|
+
self._currentlyWritingData: typing.Optional[bytes] = None
|
|
120
|
+
self._currentlyWritingDataOffset: int = 0
|
|
121
|
+
|
|
122
|
+
def setRemoteVortexName(self, remoteVortexName: str):
|
|
123
|
+
self._remoteVortexName = remoteVortexName
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def _canContinue(self):
|
|
127
|
+
return (
|
|
128
|
+
not self._paused
|
|
129
|
+
and not self._startWritingFrame.running
|
|
130
|
+
and [q for q in self._queueByPriority.values() if q]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@nonConcurrentMethod
|
|
134
|
+
def _startWriting(self):
|
|
135
|
+
if self._currentlyWritingData is not None:
|
|
136
|
+
self._startWritingFrame()
|
|
137
|
+
|
|
138
|
+
while self._canContinue:
|
|
139
|
+
# Send the messages in order of priority
|
|
140
|
+
for priority in sorted(self._queueByPriority):
|
|
141
|
+
if not self._canContinue:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
queue = self._queueByPriority[priority]
|
|
145
|
+
if not queue:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
data = queue.popleft()
|
|
149
|
+
preLen = self._queuedDataLen
|
|
150
|
+
self._queuedDataLen -= len(data)
|
|
151
|
+
|
|
152
|
+
if self._queuedDataLen < self.WARNING_DATA_LENGTH < preLen:
|
|
153
|
+
logger.info(
|
|
154
|
+
"%s: Data Queue memory high warning - returned to normal : %s",
|
|
155
|
+
self._remoteVortexName,
|
|
156
|
+
_format_size(self._queuedDataLen),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if self._queuedDataLen < self.ERROR_DATA_LENGTH < preLen:
|
|
160
|
+
logger.info(
|
|
161
|
+
"%s: Data Queue memory high error - returned to warning : %s",
|
|
162
|
+
self._remoteVortexName,
|
|
163
|
+
_format_size(self._queuedDataLen),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if self._writeWholeFrames:
|
|
167
|
+
self._transport.write(data)
|
|
168
|
+
if self._terminateFrameWithDot:
|
|
169
|
+
self._transport.write(b".")
|
|
170
|
+
|
|
171
|
+
else:
|
|
172
|
+
# Use a memoryview for fast slicing.
|
|
173
|
+
self._currentlyWritingData = data
|
|
174
|
+
self._currentlyWritingDataOffset = 0
|
|
175
|
+
self._startWritingFrame()
|
|
176
|
+
|
|
177
|
+
@nonConcurrentMethod
|
|
178
|
+
def _startWritingFrame(self):
|
|
179
|
+
# ---------------
|
|
180
|
+
# Write in progress logic.
|
|
181
|
+
# We should only have one write loop at a time
|
|
182
|
+
if self._writingFrameInProgress:
|
|
183
|
+
return
|
|
184
|
+
self._writingFrameInProgress = True
|
|
185
|
+
|
|
186
|
+
data = self._currentlyWritingData
|
|
187
|
+
offset = self._currentlyWritingDataOffset
|
|
188
|
+
while not self._paused and offset < len(data):
|
|
189
|
+
if self._DEBUG_LOGGING:
|
|
190
|
+
logger.debug(
|
|
191
|
+
"%s: Producer _startWritingFrame called,"
|
|
192
|
+
" wrote %s to %s of %s",
|
|
193
|
+
self._remoteVortexName,
|
|
194
|
+
offset,
|
|
195
|
+
min(offset + self.WRITE_CHUNK_SIZE, len(data)),
|
|
196
|
+
len(data),
|
|
197
|
+
)
|
|
198
|
+
self._transport.write(data[offset : offset + self.WRITE_CHUNK_SIZE])
|
|
199
|
+
offset += self.WRITE_CHUNK_SIZE
|
|
200
|
+
|
|
201
|
+
if len(data) <= offset:
|
|
202
|
+
self._currentlyWritingDataOffset = 0
|
|
203
|
+
self._currentlyWritingData = None
|
|
204
|
+
if self._terminateFrameWithDot:
|
|
205
|
+
self._transport.write(b".")
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
self._currentlyWritingDataOffset = offset
|
|
209
|
+
|
|
210
|
+
self._writingFrameInProgress = False
|
|
211
|
+
|
|
212
|
+
def pauseProducing(self):
|
|
213
|
+
"""
|
|
214
|
+
Pause producing data.
|
|
215
|
+
|
|
216
|
+
Tells a producer that it has produced too much data to process for
|
|
217
|
+
the time being, and to stop until C{resumeProducing()} is called.
|
|
218
|
+
"""
|
|
219
|
+
self._paused = True
|
|
220
|
+
if self._DEBUG_LOGGING:
|
|
221
|
+
logger.debug(
|
|
222
|
+
"%s: Producer paused, data len = %s",
|
|
223
|
+
self._remoteVortexName,
|
|
224
|
+
self._queuedDataLen,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def resumeProducing(self):
|
|
228
|
+
"""
|
|
229
|
+
Resume producing data.
|
|
230
|
+
|
|
231
|
+
This tells a producer to re-add itself to the main loop and produce
|
|
232
|
+
more data for its consumer.
|
|
233
|
+
"""
|
|
234
|
+
self._paused = False
|
|
235
|
+
if self._DEBUG_LOGGING:
|
|
236
|
+
logger.debug(
|
|
237
|
+
"%s: Producer resumed, data len = %s",
|
|
238
|
+
self._remoteVortexName,
|
|
239
|
+
self._queuedDataLen,
|
|
240
|
+
)
|
|
241
|
+
self._startWriting()
|
|
242
|
+
|
|
243
|
+
def stopProducing(self):
|
|
244
|
+
"""
|
|
245
|
+
Stop producing data.
|
|
246
|
+
|
|
247
|
+
This tells a producer that its consumer has died, so it must stop
|
|
248
|
+
producing data for good.
|
|
249
|
+
"""
|
|
250
|
+
if self._stopProducingCallback:
|
|
251
|
+
self._stopProducingCallback()
|
|
252
|
+
self._stopProducingCallback = None
|
|
253
|
+
|
|
254
|
+
def write(self, data: bytes, priority: int = DEFAULT_PRIORITY):
|
|
255
|
+
assert not self._closed
|
|
256
|
+
|
|
257
|
+
# Since write actually queues data and does not write, we will split
|
|
258
|
+
# the data if required so we are storing smaller memory objects.
|
|
259
|
+
if self._splitFrames and self.WRITE_CHUNK_SIZE < len(data):
|
|
260
|
+
for offset in range(0, len(data), self.WRITE_CHUNK_SIZE):
|
|
261
|
+
|
|
262
|
+
if self._DEBUG_LOGGING:
|
|
263
|
+
logger.debug(
|
|
264
|
+
"%s: Producer write, splitting frames,"
|
|
265
|
+
" split %s to %s of %s",
|
|
266
|
+
self._remoteVortexName,
|
|
267
|
+
offset,
|
|
268
|
+
min(offset + self.WRITE_CHUNK_SIZE, len(data)),
|
|
269
|
+
len(data),
|
|
270
|
+
)
|
|
271
|
+
self.write(data[offset : offset + self.WRITE_CHUNK_SIZE])
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
if self._DEBUG_LOGGING:
|
|
275
|
+
logger.debug(
|
|
276
|
+
"%s: Producer write called, data len = %s",
|
|
277
|
+
self._remoteVortexName,
|
|
278
|
+
len(data),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
preLen = self._queuedDataLen
|
|
282
|
+
self._queuedDataLen += len(data)
|
|
283
|
+
|
|
284
|
+
# Queue the data up in chunks
|
|
285
|
+
self._queueByPriority[priority].append(data)
|
|
286
|
+
|
|
287
|
+
if preLen < self.WARNING_DATA_LENGTH < self._queuedDataLen:
|
|
288
|
+
logger.warning(
|
|
289
|
+
"%s: Data Queue memory high warning : %s",
|
|
290
|
+
self._remoteVortexName,
|
|
291
|
+
_format_size(self._queuedDataLen),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if preLen < self.ERROR_DATA_LENGTH < self._queuedDataLen:
|
|
295
|
+
logger.error(
|
|
296
|
+
"%s: Data Queue memory high error : %s",
|
|
297
|
+
self._remoteVortexName,
|
|
298
|
+
_format_size(self._queuedDataLen),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
self._startWriting()
|
|
302
|
+
|
|
303
|
+
def writeSequence(
|
|
304
|
+
self, data: typing.Iterable[bytes], priority: int = DEFAULT_PRIORITY
|
|
305
|
+
) -> None:
|
|
306
|
+
|
|
307
|
+
if self._DEBUG_LOGGING:
|
|
308
|
+
logger.debug(
|
|
309
|
+
"%s: Producer writeSequence called",
|
|
310
|
+
self._remoteVortexName,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
for seq in data:
|
|
314
|
+
self.write(seq, priority=priority)
|
|
315
|
+
|
|
316
|
+
def close(self):
|
|
317
|
+
self._closed = True
|
|
318
|
+
self._queuedDataLen = 0
|
|
319
|
+
self._queueByPriority = defaultdict(deque)
|
|
320
|
+
self._currentlyWritingData = None
|
|
321
|
+
self._currentlyWritingDataOffset = 0
|