portal 0.3.1__tar.gz → 3.0.0__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 (44) hide show
  1. portal-3.0.0/LICENSE +19 -0
  2. portal-3.0.0/MANIFEST.in +1 -0
  3. portal-3.0.0/PKG-INFO +79 -0
  4. portal-3.0.0/README.md +66 -0
  5. portal-3.0.0/portal/__init__.py +34 -0
  6. portal-3.0.0/portal/batching.py +177 -0
  7. portal-3.0.0/portal/buffers.py +93 -0
  8. portal-3.0.0/portal/client.py +185 -0
  9. portal-3.0.0/portal/client_socket.py +227 -0
  10. portal-3.0.0/portal/contextlib.py +158 -0
  11. portal-3.0.0/portal/packlib.py +135 -0
  12. portal-3.0.0/portal/poollib.py +17 -0
  13. portal-3.0.0/portal/process.py +111 -0
  14. portal-3.0.0/portal/server.py +151 -0
  15. portal-3.0.0/portal/server_socket.py +167 -0
  16. portal-3.0.0/portal/sharray.py +45 -0
  17. portal-3.0.0/portal/thread.py +87 -0
  18. portal-3.0.0/portal/utils.py +109 -0
  19. portal-3.0.0/portal.egg-info/PKG-INFO +79 -0
  20. portal-3.0.0/portal.egg-info/SOURCES.txt +33 -0
  21. portal-3.0.0/portal.egg-info/requires.txt +4 -0
  22. portal-3.0.0/pyproject.toml +5 -0
  23. portal-3.0.0/requirements.txt +4 -0
  24. {portal-0.3.1 → portal-3.0.0}/setup.cfg +0 -1
  25. portal-3.0.0/setup.py +36 -0
  26. portal-3.0.0/tests/test_batching.py +175 -0
  27. portal-3.0.0/tests/test_client.py +313 -0
  28. portal-3.0.0/tests/test_errfile.py +87 -0
  29. portal-3.0.0/tests/test_pack.py +32 -0
  30. portal-3.0.0/tests/test_process.py +99 -0
  31. portal-3.0.0/tests/test_server.py +304 -0
  32. portal-3.0.0/tests/test_socket.py +196 -0
  33. portal-3.0.0/tests/test_thread.py +94 -0
  34. portal-0.3.1/PKG-INFO +0 -22
  35. portal-0.3.1/portal/__init__.py +0 -6
  36. portal-0.3.1/portal/_version.py +0 -1
  37. portal-0.3.1/portal/api.py +0 -436
  38. portal-0.3.1/portal/cli.py +0 -312
  39. portal-0.3.1/portal.egg-info/PKG-INFO +0 -22
  40. portal-0.3.1/portal.egg-info/SOURCES.txt +0 -10
  41. portal-0.3.1/portal.egg-info/entry_points.txt +0 -3
  42. portal-0.3.1/setup.py +0 -46
  43. {portal-0.3.1 → portal-3.0.0}/portal.egg-info/dependency_links.txt +0 -0
  44. {portal-0.3.1 → portal-3.0.0}/portal.egg-info/top_level.txt +0 -0
portal-3.0.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright 2023 Danijar Hafner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ include requirements.txt
portal-3.0.0/PKG-INFO ADDED
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.1
2
+ Name: portal
3
+ Version: 3.0.0
4
+ Summary: Fast and reliable distributed systems in Python
5
+ Home-page: http://github.com/danijar/portal
6
+ Author: Danijar Hafner
7
+ Author-email: mail@danijar.com
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+
14
+ [![PyPI](https://img.shields.io/pypi/v/portal.svg)](https://pypi.python.org/pypi/portal/#history)
15
+
16
+ # 🌀 Portal
17
+
18
+ Fast and reliable distributed systems in Python.
19
+
20
+ ## Features
21
+
22
+ - 📡 **Communication:** Portal lets you bind functions to a `Server` and call
23
+ them from a `Client`. Wait on results via `Future` objects. Clients can
24
+ automatically restore broken connections.
25
+ - 🚀 **Performance:** Optimized for throughput and latency. Array data is
26
+ zero-copy serialized and deserialized for throughput near the hardware limit.
27
+ - 🤸 **Flexibility:** Function inputs and outputs can be nested dicts and lists
28
+ of numbers, strings, bytes, None values, and Numpy arrays. Bytes allow
29
+ applications to chose their own serialization, such as `pickle`.
30
+ - 🚨 **Error handlings:** Provides `Process` and `Thread` objects that can
31
+ reliably be killed by the parent. Unhandled exceptions in threads stop
32
+ the program. Error files can be used to stop distributed systems.
33
+ - 📦 **Request batching:** Use `BatchServer` to collect multiple incoming
34
+ requests and process them at once, for example for AI inference servers.
35
+ Batching and dispatching happens in a separate process to free the GIL.
36
+ - ✅ **Correctness:** Covered by over 100 unit tests for common usage and edge
37
+ cases and used for large scale distributed AI systems.
38
+
39
+ ## Installation
40
+
41
+ ```sh
42
+ pip install portal
43
+ ```
44
+
45
+ ## Example
46
+
47
+ This example runs the server and client in the same Python program using
48
+ subprocesses, but they could also be separate Python scripts running on
49
+ different machines.
50
+
51
+ ```python
52
+ def server():
53
+ import portal
54
+ server = portal.Server(2222)
55
+ server.bind('add', lambda x, y: x + y)
56
+ server.bind('greet', lambda msg: print('Message from client:', msg))
57
+ server.start()
58
+
59
+ def client():
60
+ import portal
61
+ client = portal.Client('localhost', 2222)
62
+ future = client.add(12, 42)
63
+ result = future.result()
64
+ print(result) # 54
65
+ client.greet('Hello World')
66
+
67
+ if __name__ == '__main__':
68
+ import portal
69
+ server_proc = portal.Process(server, start=True)
70
+ client_proc = portal.Process(client, start=True)
71
+ client_proc.join()
72
+ server_proc.kill()
73
+ print('Done')
74
+ ```
75
+
76
+ ## Questions
77
+
78
+ Please open a separate [GitHub issue](https://github.com/danijar/portal/issues)
79
+ for each question.
portal-3.0.0/README.md ADDED
@@ -0,0 +1,66 @@
1
+ [![PyPI](https://img.shields.io/pypi/v/portal.svg)](https://pypi.python.org/pypi/portal/#history)
2
+
3
+ # 🌀 Portal
4
+
5
+ Fast and reliable distributed systems in Python.
6
+
7
+ ## Features
8
+
9
+ - 📡 **Communication:** Portal lets you bind functions to a `Server` and call
10
+ them from a `Client`. Wait on results via `Future` objects. Clients can
11
+ automatically restore broken connections.
12
+ - 🚀 **Performance:** Optimized for throughput and latency. Array data is
13
+ zero-copy serialized and deserialized for throughput near the hardware limit.
14
+ - 🤸 **Flexibility:** Function inputs and outputs can be nested dicts and lists
15
+ of numbers, strings, bytes, None values, and Numpy arrays. Bytes allow
16
+ applications to chose their own serialization, such as `pickle`.
17
+ - 🚨 **Error handlings:** Provides `Process` and `Thread` objects that can
18
+ reliably be killed by the parent. Unhandled exceptions in threads stop
19
+ the program. Error files can be used to stop distributed systems.
20
+ - 📦 **Request batching:** Use `BatchServer` to collect multiple incoming
21
+ requests and process them at once, for example for AI inference servers.
22
+ Batching and dispatching happens in a separate process to free the GIL.
23
+ - ✅ **Correctness:** Covered by over 100 unit tests for common usage and edge
24
+ cases and used for large scale distributed AI systems.
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ pip install portal
30
+ ```
31
+
32
+ ## Example
33
+
34
+ This example runs the server and client in the same Python program using
35
+ subprocesses, but they could also be separate Python scripts running on
36
+ different machines.
37
+
38
+ ```python
39
+ def server():
40
+ import portal
41
+ server = portal.Server(2222)
42
+ server.bind('add', lambda x, y: x + y)
43
+ server.bind('greet', lambda msg: print('Message from client:', msg))
44
+ server.start()
45
+
46
+ def client():
47
+ import portal
48
+ client = portal.Client('localhost', 2222)
49
+ future = client.add(12, 42)
50
+ result = future.result()
51
+ print(result) # 54
52
+ client.greet('Hello World')
53
+
54
+ if __name__ == '__main__':
55
+ import portal
56
+ server_proc = portal.Process(server, start=True)
57
+ client_proc = portal.Process(client, start=True)
58
+ client_proc.join()
59
+ server_proc.kill()
60
+ print('Done')
61
+ ```
62
+
63
+ ## Questions
64
+
65
+ Please open a separate [GitHub issue](https://github.com/danijar/portal/issues)
66
+ for each question.
@@ -0,0 +1,34 @@
1
+ __version__ = '3.0.0'
2
+
3
+ import multiprocessing as mp
4
+ try:
5
+ mp.set_start_method('spawn')
6
+ except RuntimeError:
7
+ pass
8
+
9
+ from .contextlib import context
10
+ from .contextlib import initfn
11
+ from .contextlib import reset
12
+ from .contextlib import setup
13
+
14
+ from .thread import Thread
15
+ from .process import Process
16
+
17
+ from .server_socket import ServerSocket
18
+ from .client_socket import ClientSocket
19
+ from .client_socket import Disconnected
20
+
21
+ from .client import Client
22
+ from .server import Server
23
+ from .batching import BatchServer
24
+
25
+ from .packlib import pack
26
+ from .packlib import unpack
27
+ from .packlib import tree_equals
28
+
29
+ from .sharray import SharedArray
30
+
31
+ from .utils import free_port
32
+ from .utils import kill_procs
33
+ from .utils import kill_threads
34
+ from .utils import run
@@ -0,0 +1,177 @@
1
+ import threading
2
+
3
+ import numpy as np
4
+ import portal
5
+
6
+ from . import client
7
+ from . import packlib
8
+ from . import process
9
+ from . import server
10
+ from . import server_socket
11
+ from . import sharray
12
+ from . import thread
13
+ from . import utils
14
+
15
+
16
+ class BatchServer:
17
+
18
+ def __init__(
19
+ self, port, name='Server', workers=1, errors=True,
20
+ process=True, shmem=False, **kwargs):
21
+ inner_port = utils.free_port()
22
+ self.name = name
23
+ self.server = server.Server(inner_port, name, workers, errors, **kwargs)
24
+ if process:
25
+ self.running = portal.context.mp.Event()
26
+ else:
27
+ self.running = threading.Event()
28
+ self.process = process
29
+ self.batsizes = {}
30
+ self.batargs = (
31
+ self.running, port, inner_port, f'{name}Batcher',
32
+ self.batsizes, errors, shmem, kwargs)
33
+ self.started = False
34
+
35
+ def bind(self, name, workfn, donefn=None, batch=0, workers=0):
36
+ assert not self.started
37
+ self.batsizes[name] = batch
38
+ self.server.bind(name, workfn, donefn, workers=workers)
39
+
40
+ def start(self, block=True):
41
+ assert not self.started
42
+ self.started = True
43
+ self.running.set()
44
+ if self.process:
45
+ self.batcher = process.Process(
46
+ batcher, *self.batargs, name=f'{self.name}Batcher', start=True)
47
+ else:
48
+ self.batcher = thread.Thread(
49
+ batcher, *self.batargs, name=f'{self.name}Batcher', start=True)
50
+ self.server.start(block=block)
51
+
52
+ def close(self, timeout=None):
53
+ assert self.started
54
+ self.running.clear()
55
+ self.server.close(timeout and 0.5 * timeout)
56
+ self.batcher.join(timeout and 0.5 * timeout)
57
+ self.batcher.kill()
58
+
59
+ def stats(self):
60
+ return self.server.stats()
61
+
62
+ def __enter__(self):
63
+ self.start(block=False)
64
+ return self
65
+
66
+ def __exit__(self, *e):
67
+ self.close()
68
+
69
+
70
+ def batcher(
71
+ running, outer_port, inner_port, name, batsizes, errors, shmem,
72
+ kwargs):
73
+
74
+ def maybe_recv(outer, inner, jobs, batches):
75
+ if not running.is_set(): # Do not accept further requests.
76
+ return
77
+ try:
78
+ addr, data = outer.recv(timeout=0.0001)
79
+ except TimeoutError:
80
+ return
81
+ reqnum = bytes(data[:8])
82
+ data = data[8:]
83
+ strlen = int.from_bytes(data[:8], 'little', signed=False)
84
+ data = data[8:]
85
+ name, data = bytes(data[:strlen]).decode('utf-8'), data[strlen:]
86
+ if name not in batsizes:
87
+ send_error(addr, reqnum, 3, f'Unknown method {name}')
88
+ return
89
+ data = packlib.unpack(data)
90
+ batch_size = batsizes[name]
91
+ if not batch_size:
92
+ job = inner.call(name, *data)
93
+ job.args = (False, addr, reqnum)
94
+ jobs.append(job)
95
+ return
96
+ leaves, structure = packlib.tree_flatten(data)
97
+ leaves = [np.asarray(x) for x in leaves]
98
+ if any(x.dtype == object for x in leaves):
99
+ send_error(addr, reqnum, 5, 'Only array arguments can be batched.')
100
+ return
101
+ if name not in batches:
102
+ if shmem:
103
+ buffers = [
104
+ sharray.SharedArray((batch_size, *leaf.shape), leaf.dtype)
105
+ for leaf in leaves]
106
+ else:
107
+ buffers = [
108
+ np.empty((batch_size, *leaf.shape), leaf.dtype)
109
+ for leaf in leaves]
110
+ batches[name] = ([], [], structure, buffers)
111
+ addrs, reqnums, reference, buffers = batches[name]
112
+ if structure != reference:
113
+ send_error(addr, reqnum, 6, (
114
+ f'Argument structure {structure} does not match previous ' +
115
+ f'requests with structure {reference} for batched server ' +
116
+ f'method {name}.'))
117
+ return
118
+ index = len(addrs)
119
+ addrs.append(addr)
120
+ reqnums.append(reqnum)
121
+ for buffer, leaf in zip(buffers, leaves):
122
+ buffer[index] = leaf
123
+ if len(addrs) == batch_size:
124
+ del batches[name]
125
+ data = packlib.tree_unflatten(buffers, reference)
126
+ job = inner.call(name, *data)
127
+ job.args = (True, addrs, reqnums)
128
+ jobs.append(job)
129
+
130
+ def maybe_send(outer, inner, jobs):
131
+ done, waiting = [], []
132
+ [done.append(x) if x.done() else waiting.append(x) for x in jobs]
133
+ for job in done:
134
+ batched, addr, reqnum = job.args
135
+ try:
136
+ result = job.result()
137
+ except RuntimeError as e:
138
+ if batched:
139
+ for i, (addr, reqnum) in enumerate(zip(addr, reqnum)):
140
+ send_error(addr, reqnum, 6, e.args[0])
141
+ else:
142
+ send_error(addr, reqnum, 6, e.args[0])
143
+ continue
144
+ status = int(0).to_bytes(8, 'little', signed=True)
145
+ if batched:
146
+ for i, (addr, reqnum) in enumerate(zip(addr, reqnum)):
147
+ data = packlib.pack(packlib.tree_map(lambda x: x[i], result))
148
+ outer.send(addr, reqnum, status, *data)
149
+ else:
150
+ data = packlib.pack(result)
151
+ outer.send(addr, reqnum, status, *data)
152
+ return waiting
153
+
154
+ def send_error(addr, reqnum, status, message):
155
+ assert 1 <= status, status
156
+ status = status.to_bytes(8, 'little', signed=False)
157
+ data = message.encode('utf-8')
158
+ outer.send(addr, reqnum, status, data)
159
+ if errors:
160
+ raise RuntimeError(message)
161
+
162
+ outer = server_socket.ServerSocket(outer_port, f'{name}Server', **kwargs)
163
+ inner = client.Client('localhost', inner_port, f'{name}Client', **kwargs)
164
+ batches = {} # {method: ([addr], [reqnum], structure, [array])}
165
+ jobs = []
166
+ shutdown = False
167
+ try:
168
+ while running.is_set() or jobs:
169
+ if running.is_set():
170
+ maybe_recv(outer, inner, jobs, batches)
171
+ elif not shutdown:
172
+ shutdown = True
173
+ outer.shutdown()
174
+ jobs = maybe_send(outer, inner, jobs)
175
+ finally:
176
+ outer.close()
177
+ inner.close()
@@ -0,0 +1,93 @@
1
+ import collections
2
+ import os
3
+ import weakref
4
+
5
+ import numpy as np
6
+
7
+
8
+ class SendBuffer:
9
+
10
+ def __init__(self, *buffers, maxsize=None):
11
+ for buffer in buffers:
12
+ assert isinstance(buffer, (bytes, bytearray, memoryview)), type(buffer)
13
+ assert not isinstance(buffer, memoryview) or buffer.c_contiguous
14
+ buffers = tuple(
15
+ x.cast('c') if isinstance(x, memoryview) else x for x in buffers)
16
+ length = sum(len(x) for x in buffers)
17
+ assert all(len(x) for x in buffers)
18
+ assert 1 <= length, length
19
+ assert not maxsize or length <= length, (length, maxsize)
20
+ lenbuf = length.to_bytes(4, 'little', signed=False)
21
+ self.buffers = [lenbuf, *buffers]
22
+ self.remaining = collections.deque(self.buffers)
23
+ self.pos = 0
24
+
25
+ def __repr__(self):
26
+ lens = [len(x) for x in self.buffers]
27
+ left = [len(x) for x in self.remaining]
28
+ return f'SendBuffer(pos={self.pos}, lengths={lens} remaining={left})'
29
+
30
+ def reset(self):
31
+ self.remaining = collections.deque(self.buffers)
32
+ self.pos = 0
33
+
34
+ def send(self, sock):
35
+ first, *others = self.remaining
36
+ assert self.pos < len(first)
37
+ # The writev() call blocks but seems to be slightly faster than sendmsg().
38
+ size = os.writev(sock.fileno(), [memoryview(first)[self.pos:], *others])
39
+ # size = sock.sendmsg(
40
+ # [memoryview(first)[self.pos:], *others], (), socket.MSG_DONTWAIT)
41
+ if size == 0:
42
+ raise ConnectionResetError
43
+ assert 0 <= size, size
44
+ self.pos += max(0, size)
45
+ while self.remaining and self.pos >= len(self.remaining[0]):
46
+ self.pos -= len(self.remaining.popleft())
47
+ return size
48
+
49
+ def done(self):
50
+ return not self.remaining
51
+
52
+
53
+ class RecvBuffer:
54
+
55
+ def __init__(self, maxsize):
56
+ self.maxsize = maxsize
57
+ self.lenbuf = bytearray(4)
58
+ self.buffer = None
59
+ self.pos = 0
60
+
61
+ def __repr__(self):
62
+ length = self.buffer and len(self.buffer)
63
+ return f'RecvBuffer(pos={self.pos}, length={length})'
64
+
65
+ def recv(self, sock):
66
+ if self.buffer is None:
67
+ size = sock.recv_into(memoryview(self.lenbuf)[self.pos:])
68
+ self.pos += max(0, size)
69
+ if self.pos == 4:
70
+ length = int.from_bytes(self.lenbuf, 'little', signed=False)
71
+ assert 1 <= length <= self.maxsize, (1, length, self.maxsize)
72
+ # We use Numpy to allocate uninitialized memory because Python's
73
+ # `bytearray(length)` zero initializes which is slow. This also means
74
+ # the buffer cannot be pickled accidentally unless explicitly converted
75
+ # to a `bytes()` object, which is a nice bonus for preventing
76
+ # performance bugs in user code.
77
+ arr = np.empty(length, np.uint8)
78
+ self.buffer = memoryview(arr.data)
79
+ weakref.finalize(self.buffer, lambda arr=arr: arr)
80
+ self.pos = 0
81
+ else:
82
+ size = sock.recv_into(self.buffer[self.pos:])
83
+ self.pos += max(0, size)
84
+ assert 0 <= self.pos <= len(self.buffer), (0, self.pos, len(self.buffer))
85
+ if size == 0:
86
+ raise ConnectionResetError
87
+ return size
88
+
89
+ def done(self):
90
+ return self.buffer and self.pos == len(self.buffer)
91
+
92
+ def result(self):
93
+ return self.buffer
@@ -0,0 +1,185 @@
1
+ import collections
2
+ import functools
3
+ import itertools
4
+ import threading
5
+ import time
6
+ import weakref
7
+
8
+ from . import client_socket
9
+ from . import packlib
10
+
11
+
12
+ class Client:
13
+
14
+ def __init__(
15
+ self, host, port, name='Client', maxinflight=16, **kwargs):
16
+ assert 1 <= maxinflight, maxinflight
17
+ self.maxinflight = maxinflight
18
+ self.reqnum = iter(itertools.count(0))
19
+ self.futures = {}
20
+ self.errors = collections.deque()
21
+ self.sendrate = [0, time.time()]
22
+ self.recvrate = [0, time.time()]
23
+ self.waitmean = [0, 0]
24
+ self.cond = threading.Condition()
25
+ self.lock = threading.Lock()
26
+ # Socket is created after the above attributes because the callbacks access
27
+ # some of the attributes.
28
+ self.socket = client_socket.ClientSocket(
29
+ host, port, name, start=False, **kwargs)
30
+ self.socket.callbacks_recv.append(self._recv)
31
+ self.socket.callbacks_disc.append(self._disc)
32
+ self.socket.callbacks_conn.append(self._conn)
33
+ self.socket.start()
34
+
35
+ @property
36
+ def connected(self):
37
+ return self.socket.connected
38
+
39
+ def __getattr__(self, name):
40
+ if name.startswith('_'):
41
+ raise AttributeError(name)
42
+ try:
43
+ return functools.partial(self.call, name)
44
+ except AttributeError:
45
+ raise ValueError(name)
46
+
47
+ def stats(self):
48
+ now = time.time()
49
+ stats = {
50
+ 'inflight': self._numinflight(),
51
+ 'numsend': self.sendrate[0],
52
+ 'numrecv': self.recvrate[0],
53
+ 'sendrate': self.sendrate[0] / (now - self.sendrate[1]),
54
+ 'recvrate': self.recvrate[0] / (now - self.recvrate[1]),
55
+ 'waitmean': self.waitmean[0] and (self.waitmean[1] / self.waitmean[0]),
56
+ }
57
+ self.sendrate = [0, now]
58
+ self.recvrate = [0, now]
59
+ self.waitmean = [0, 0]
60
+ return stats
61
+
62
+ def connect(self, timeout=None):
63
+ return self.socket.connect(timeout)
64
+
65
+ def call(self, method, *data):
66
+ reqnum = next(self.reqnum).to_bytes(8, 'little', signed=False)
67
+ start = time.time()
68
+ while self._numinflight() >= self.maxinflight:
69
+ with self.cond: self.cond.wait(timeout=0.2)
70
+ try:
71
+ self.socket.require_connection(timeout=0)
72
+ except TimeoutError:
73
+ pass
74
+ with self.lock:
75
+ self.waitmean[1] += time.time() - start
76
+ self.waitmean[0] += 1
77
+ self.sendrate[0] += 1
78
+ if self.errors: # Raise errors of dropped futures.
79
+ raise self.errors.popleft()
80
+ name = method.encode('utf-8')
81
+ strlen = len(name).to_bytes(8, 'little', signed=False)
82
+ sendargs = (reqnum, strlen, name, *packlib.pack(data))
83
+ # self.socket.send(reqnum, strlen, name, *packlib.pack(data))
84
+ self.socket.send(*sendargs)
85
+ future = Future()
86
+ future.sendargs = sendargs
87
+ self.futures[reqnum] = future
88
+ return future
89
+
90
+ def close(self, timeout=None):
91
+ for future in self.futures.values():
92
+ self._seterr(future, client_socket.Disconnected)
93
+ self.futures.clear()
94
+ self.socket.close(timeout)
95
+
96
+ def _numinflight(self):
97
+ return len([x for x in self.futures.values() if not x.don])
98
+
99
+ def _recv(self, data):
100
+ assert len(data) >= 16, 'Unexpectedly short response'
101
+ reqnum = bytes(data[:8])
102
+ status = int.from_bytes(data[8:16], 'little', signed=False)
103
+ future = self.futures.pop(reqnum, None)
104
+ assert future, (
105
+ f'Unexpected request number: {reqnum}',
106
+ sorted(self.futures.keys()))
107
+ if status == 0:
108
+ data = packlib.unpack(data[16:])
109
+ future.set_result(data)
110
+ else:
111
+ message = bytes(data[16:]).decode('utf-8')
112
+ self._seterr(future, RuntimeError(message))
113
+ with self.cond:
114
+ self.cond.notify_all()
115
+ self.socket.recv()
116
+
117
+ def _disc(self):
118
+ if self.socket.options.autoconn:
119
+ for future in self.futures.values():
120
+ future.resend = True
121
+ else:
122
+ for future in self.futures.values():
123
+ self._seterr(future, client_socket.Disconnected)
124
+ self.futures.clear()
125
+
126
+ def _conn(self):
127
+ if self.socket.options.autoconn:
128
+ for future in list(self.futures.values()):
129
+ if getattr(future, 'resend', False):
130
+ self.socket.send(*future.sendargs)
131
+
132
+ def _seterr(self, future, e):
133
+ raised = [False]
134
+ future.raised = raised
135
+ future.set_error(e)
136
+ weakref.finalize(future, lambda: (
137
+ None if raised[0] else self.errors.append(e)))
138
+
139
+
140
+ class Future:
141
+
142
+ def __init__(self):
143
+ self.raised = [False]
144
+ self.con = threading.Condition()
145
+ self.don = False
146
+ self.res = None
147
+ self.err = None
148
+
149
+ def __repr__(self):
150
+ if not self.done:
151
+ return 'Future(done=False)'
152
+ elif self.err:
153
+ return f"Future(done=True, error='{self.err}', raised={self.raised[0]})"
154
+ else:
155
+ return 'Future(done=True)'
156
+
157
+ def wait(self, timeout=None):
158
+ if self.don:
159
+ return self.don
160
+ with self.con: return self.con.wait(timeout)
161
+
162
+ def done(self):
163
+ return self.don
164
+
165
+ def result(self, timeout=None):
166
+ if not self.wait(timeout):
167
+ raise TimeoutError
168
+ assert self.don
169
+ if self.err is None:
170
+ return self.res
171
+ if not self.raised[0]:
172
+ self.raised[0] = True
173
+ raise self.err
174
+
175
+ def set_result(self, result):
176
+ assert not self.don
177
+ self.don = True
178
+ self.res = result
179
+ with self.con: self.con.notify_all()
180
+
181
+ def set_error(self, e):
182
+ assert not self.don
183
+ self.don = True
184
+ self.err = e
185
+ with self.con: self.con.notify_all()