portal 3.4.0__tar.gz → 3.4.2__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.4.0/portal.egg-info → portal-3.4.2}/PKG-INFO +1 -1
- {portal-3.4.0 → portal-3.4.2}/portal/__init__.py +1 -1
- {portal-3.4.0 → portal-3.4.2}/portal/client_socket.py +2 -2
- {portal-3.4.0 → portal-3.4.2}/portal/contextlib.py +5 -2
- {portal-3.4.0 → portal-3.4.2}/portal/server.py +40 -26
- {portal-3.4.0 → portal-3.4.2}/portal/server_socket.py +3 -2
- {portal-3.4.0 → portal-3.4.2/portal.egg-info}/PKG-INFO +1 -1
- {portal-3.4.0 → portal-3.4.2}/tests/test_server.py +22 -3
- {portal-3.4.0 → portal-3.4.2}/tests/test_socket.py +0 -2
- {portal-3.4.0 → portal-3.4.2}/LICENSE +0 -0
- {portal-3.4.0 → portal-3.4.2}/MANIFEST.in +0 -0
- {portal-3.4.0 → portal-3.4.2}/README.md +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/batching.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/buffers.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/client.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/packlib.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/poollib.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/process.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/sharray.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/thread.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal/utils.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal.egg-info/SOURCES.txt +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal.egg-info/dependency_links.txt +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal.egg-info/requires.txt +0 -0
- {portal-3.4.0 → portal-3.4.2}/portal.egg-info/top_level.txt +0 -0
- {portal-3.4.0 → portal-3.4.2}/pyproject.toml +0 -0
- {portal-3.4.0 → portal-3.4.2}/requirements.txt +0 -0
- {portal-3.4.0 → portal-3.4.2}/setup.cfg +0 -0
- {portal-3.4.0 → portal-3.4.2}/setup.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/tests/test_batching.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/tests/test_client.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/tests/test_errfile.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/tests/test_pack.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/tests/test_process.py +0 -0
- {portal-3.4.0 → portal-3.4.2}/tests/test_thread.py +0 -0
@@ -190,7 +190,6 @@ class ClientSocket:
|
|
190
190
|
port = int(port)
|
191
191
|
addr = (host, port, 0, 0) if self.options.ipv6 else (host, port)
|
192
192
|
sock = self._create()
|
193
|
-
start = time.time()
|
194
193
|
error = None
|
195
194
|
try:
|
196
195
|
sock.settimeout(10)
|
@@ -214,9 +213,10 @@ class ClientSocket:
|
|
214
213
|
def _create(self):
|
215
214
|
if self.options.ipv6:
|
216
215
|
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
216
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
217
217
|
else:
|
218
218
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
219
|
-
# sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
219
|
+
# sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # TODO
|
220
220
|
|
221
221
|
after = self.options.keepalive_after
|
222
222
|
every = self.options.keepalive_every
|
@@ -21,10 +21,13 @@ class Context:
|
|
21
21
|
self.interval = 20
|
22
22
|
self.clientkw = {}
|
23
23
|
self.serverkw = {}
|
24
|
+
self.printlock = threading.Lock()
|
24
25
|
self.done = threading.Event()
|
25
26
|
self.watcher = None
|
26
|
-
|
27
|
-
|
27
|
+
|
28
|
+
@property
|
29
|
+
def mp(self):
|
30
|
+
return mp.get_context('spawn')
|
28
31
|
|
29
32
|
def options(self):
|
30
33
|
return {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import collections
|
2
2
|
import concurrent.futures
|
3
|
-
import threading
|
4
3
|
import time
|
4
|
+
import types
|
5
5
|
|
6
6
|
from . import packlib
|
7
7
|
from . import poollib
|
@@ -34,11 +34,11 @@ class Server:
|
|
34
34
|
self.pools.append(pool)
|
35
35
|
else:
|
36
36
|
pool = self.pool
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
37
|
+
requests = collections.deque()
|
38
|
+
available = (workers or self.workers) + 1
|
39
|
+
self.methods[name] = types.SimpleNamespace(
|
40
|
+
workfn=workfn, postfn=postfn, pool=pool,
|
41
|
+
requests=requests, available=available)
|
42
42
|
|
43
43
|
def start(self, block=True):
|
44
44
|
assert not self.running
|
@@ -67,9 +67,10 @@ class Server:
|
|
67
67
|
'numrecv': mets['recv'],
|
68
68
|
'sendrate': mets['send'] / dur,
|
69
69
|
'recvrate': mets['recv'] / dur,
|
70
|
+
'requests': sum(len(m.requests) for m in self.methods.values()),
|
70
71
|
'jobs': len(self.jobs),
|
71
72
|
}
|
72
|
-
if any(postfn for
|
73
|
+
if any(method.postfn for method in self.methods.values()):
|
73
74
|
stats.update({
|
74
75
|
'post_iqueue': len(self.postfn_inp),
|
75
76
|
'post_oqueue': len(self.postfn_out),
|
@@ -84,7 +85,9 @@ class Server:
|
|
84
85
|
self.close()
|
85
86
|
|
86
87
|
def _loop(self):
|
87
|
-
|
88
|
+
methods = list(self.methods.values())
|
89
|
+
pending = 0
|
90
|
+
while self.running or pending:
|
88
91
|
while True: # Loop syntax used to break on error.
|
89
92
|
if not self.running: # Do not accept further requests.
|
90
93
|
break
|
@@ -107,24 +110,30 @@ class Server:
|
|
107
110
|
self._error(addr, reqnum, 3, f'Unknown method {name}')
|
108
111
|
break
|
109
112
|
self.metrics['recv'] += 1
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
job.postfn = postfn
|
114
|
-
job.addr = addr
|
115
|
-
job.reqnum = reqnum
|
116
|
-
self.jobs.add(job)
|
117
|
-
if postfn:
|
118
|
-
self.postfn_inp.append(job)
|
113
|
+
method = self.methods[name]
|
114
|
+
method.requests.append((addr, reqnum, data))
|
115
|
+
pending += 1
|
119
116
|
break # We do not actually want to loop.
|
117
|
+
|
118
|
+
for method in methods:
|
119
|
+
if method.requests and method.available:
|
120
|
+
method.available -= 1
|
121
|
+
addr, reqnum, data = method.requests.popleft()
|
122
|
+
job = method.pool.submit(method.workfn, *data)
|
123
|
+
job.method = method
|
124
|
+
job.addr = addr
|
125
|
+
job.reqnum = reqnum
|
126
|
+
self.jobs.add(job)
|
127
|
+
if method.postfn:
|
128
|
+
self.postfn_inp.append(job)
|
129
|
+
|
120
130
|
completed, self.jobs = concurrent.futures.wait(
|
121
131
|
self.jobs, 0.0001, concurrent.futures.FIRST_COMPLETED)
|
122
132
|
for job in completed:
|
123
133
|
try:
|
124
134
|
data = job.result()
|
125
|
-
if job.postfn:
|
126
|
-
data,
|
127
|
-
del info
|
135
|
+
if job.method.postfn:
|
136
|
+
data, _ = data
|
128
137
|
data = packlib.pack(data)
|
129
138
|
status = int(0).to_bytes(8, 'little', signed=False)
|
130
139
|
self.socket.send(job.addr, job.reqnum, status, *data)
|
@@ -132,19 +141,24 @@ class Server:
|
|
132
141
|
except Exception as e:
|
133
142
|
self._error(job.addr, job.reqnum, 4, f'Error in server method: {e}')
|
134
143
|
finally:
|
135
|
-
if not job.postfn:
|
136
|
-
job.
|
144
|
+
if not job.method.postfn:
|
145
|
+
job.method.available += 1
|
146
|
+
pending -= 1
|
147
|
+
|
137
148
|
if completed:
|
149
|
+
# Call postfns in the order the requests were received.
|
138
150
|
while self.postfn_inp and self.postfn_inp[0].done():
|
139
151
|
job = self.postfn_inp.popleft()
|
140
|
-
|
141
|
-
postjob = self.postfn_pool.submit(job.postfn, info)
|
142
|
-
postjob.
|
152
|
+
_, info = job.result()
|
153
|
+
postjob = self.postfn_pool.submit(job.method.postfn, info)
|
154
|
+
postjob.method = job.method
|
143
155
|
self.postfn_out.append(postjob)
|
156
|
+
|
144
157
|
while self.postfn_out and self.postfn_out[0].done():
|
145
158
|
postjob = self.postfn_out.popleft()
|
146
|
-
postjob.active.release()
|
147
159
|
postjob.result() # Check if there was an error.
|
160
|
+
postjob.method.available += 1
|
161
|
+
pending -= 1
|
148
162
|
|
149
163
|
def _error(self, addr, reqnum, status, message):
|
150
164
|
status = status.to_bytes(8, 'little', signed=False)
|
@@ -43,16 +43,17 @@ class ServerSocket:
|
|
43
43
|
self.options = Options(**{**contextlib.context.serverkw, **kwargs})
|
44
44
|
if self.options.ipv6:
|
45
45
|
self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
46
|
+
self.sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
46
47
|
self.addr = (self.options.host or '::', port, 0, 0)
|
47
48
|
else:
|
48
49
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
49
50
|
self.addr = (self.options.host or '0.0.0.0', port)
|
50
51
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
51
|
-
# self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
52
|
+
# self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # TODO
|
52
53
|
self._log(f'Binding to {self.addr[0]}:{self.addr[1]}')
|
53
54
|
self.sock.bind(self.addr)
|
54
55
|
self.sock.setblocking(False)
|
55
|
-
self.sock.listen()
|
56
|
+
self.sock.listen(8192)
|
56
57
|
self.sel = selectors.DefaultSelector()
|
57
58
|
self.sel.register(self.sock, selectors.EVENT_READ, data=None)
|
58
59
|
self._log(f'Listening at {self.addr[0]}:{self.addr[1]}')
|
@@ -165,6 +165,25 @@ class TestServer:
|
|
165
165
|
assert completed != list(range(10))
|
166
166
|
assert logged == list(range(10))
|
167
167
|
|
168
|
+
@pytest.mark.parametrize('repeat', range(10))
|
169
|
+
@pytest.mark.parametrize('Server', SERVERS)
|
170
|
+
def test_postfn_no_hang(self, repeat, Server):
|
171
|
+
def wrapper():
|
172
|
+
port = portal.free_port()
|
173
|
+
def workfn(x):
|
174
|
+
return x, x
|
175
|
+
def postfn(x):
|
176
|
+
time.sleep(0.01)
|
177
|
+
server = Server(port, workers=4)
|
178
|
+
server.bind('fn', workfn, postfn)
|
179
|
+
server.start(block=False)
|
180
|
+
client = portal.Client(port)
|
181
|
+
futures = [client.fn(i) for i in range(20)]
|
182
|
+
[future.result() for future in futures] # Used to hang here.
|
183
|
+
client.close()
|
184
|
+
server.close()
|
185
|
+
assert portal.Thread(wrapper, start=True).join(timeout=10).exitcode == 0
|
186
|
+
|
168
187
|
@pytest.mark.parametrize('repeat', range(5))
|
169
188
|
@pytest.mark.parametrize('Server', SERVERS)
|
170
189
|
@pytest.mark.parametrize('workers', (1, 4))
|
@@ -218,7 +237,7 @@ class TestServer:
|
|
218
237
|
# The slow request is processed first, so this will wait until both
|
219
238
|
# requests are done.
|
220
239
|
fast_future.result()
|
221
|
-
assert slow_future.wait(0.
|
240
|
+
assert slow_future.wait(0.1)
|
222
241
|
server.close()
|
223
242
|
client.close()
|
224
243
|
|
@@ -244,7 +263,7 @@ class TestServer:
|
|
244
263
|
# Both requests are processed in parallel, so the fast request returns
|
245
264
|
# before the slow request is done.
|
246
265
|
fast_future.result()
|
247
|
-
assert not slow_future.wait(0.
|
266
|
+
assert not slow_future.wait(0.1)
|
248
267
|
server.close()
|
249
268
|
client.close()
|
250
269
|
|
@@ -313,7 +332,7 @@ class TestServer:
|
|
313
332
|
def fn(x):
|
314
333
|
if x == 1:
|
315
334
|
barrier.wait()
|
316
|
-
time.sleep(0.
|
335
|
+
time.sleep(0.5)
|
317
336
|
return x
|
318
337
|
|
319
338
|
port = portal.free_port()
|
@@ -11,8 +11,6 @@ class TestSocket:
|
|
11
11
|
port = portal.free_port()
|
12
12
|
server = portal.ServerSocket(port, ipv6=ipv6)
|
13
13
|
client = portal.ClientSocket(port, ipv6=ipv6)
|
14
|
-
client.connect()
|
15
|
-
assert client.connected
|
16
14
|
client.send(b'foo')
|
17
15
|
addr, data = server.recv()
|
18
16
|
assert addr[0] == '::1' if ipv6 else '127.0.0.1'
|
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
|
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
|