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.
- portal-3.0.0/LICENSE +19 -0
- portal-3.0.0/MANIFEST.in +1 -0
- portal-3.0.0/PKG-INFO +79 -0
- portal-3.0.0/README.md +66 -0
- portal-3.0.0/portal/__init__.py +34 -0
- portal-3.0.0/portal/batching.py +177 -0
- portal-3.0.0/portal/buffers.py +93 -0
- portal-3.0.0/portal/client.py +185 -0
- portal-3.0.0/portal/client_socket.py +227 -0
- portal-3.0.0/portal/contextlib.py +158 -0
- portal-3.0.0/portal/packlib.py +135 -0
- portal-3.0.0/portal/poollib.py +17 -0
- portal-3.0.0/portal/process.py +111 -0
- portal-3.0.0/portal/server.py +151 -0
- portal-3.0.0/portal/server_socket.py +167 -0
- portal-3.0.0/portal/sharray.py +45 -0
- portal-3.0.0/portal/thread.py +87 -0
- portal-3.0.0/portal/utils.py +109 -0
- portal-3.0.0/portal.egg-info/PKG-INFO +79 -0
- portal-3.0.0/portal.egg-info/SOURCES.txt +33 -0
- portal-3.0.0/portal.egg-info/requires.txt +4 -0
- portal-3.0.0/pyproject.toml +5 -0
- portal-3.0.0/requirements.txt +4 -0
- {portal-0.3.1 → portal-3.0.0}/setup.cfg +0 -1
- portal-3.0.0/setup.py +36 -0
- portal-3.0.0/tests/test_batching.py +175 -0
- portal-3.0.0/tests/test_client.py +313 -0
- portal-3.0.0/tests/test_errfile.py +87 -0
- portal-3.0.0/tests/test_pack.py +32 -0
- portal-3.0.0/tests/test_process.py +99 -0
- portal-3.0.0/tests/test_server.py +304 -0
- portal-3.0.0/tests/test_socket.py +196 -0
- portal-3.0.0/tests/test_thread.py +94 -0
- portal-0.3.1/PKG-INFO +0 -22
- portal-0.3.1/portal/__init__.py +0 -6
- portal-0.3.1/portal/_version.py +0 -1
- portal-0.3.1/portal/api.py +0 -436
- portal-0.3.1/portal/cli.py +0 -312
- portal-0.3.1/portal.egg-info/PKG-INFO +0 -22
- portal-0.3.1/portal.egg-info/SOURCES.txt +0 -10
- portal-0.3.1/portal.egg-info/entry_points.txt +0 -3
- portal-0.3.1/setup.py +0 -46
- {portal-0.3.1 → portal-3.0.0}/portal.egg-info/dependency_links.txt +0 -0
- {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.
|
portal-3.0.0/MANIFEST.in
ADDED
@@ -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
|
+
[](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
|
+
[](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()
|