portal 3.4.1__tar.gz → 3.4.3__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.1/portal.egg-info → portal-3.4.3}/PKG-INFO +1 -1
- {portal-3.4.1 → portal-3.4.3}/portal/__init__.py +1 -1
- {portal-3.4.1 → portal-3.4.3}/portal/client_socket.py +4 -0
- {portal-3.4.1 → portal-3.4.3}/portal/server.py +40 -26
- {portal-3.4.1 → portal-3.4.3}/portal/server_socket.py +4 -0
- {portal-3.4.1 → portal-3.4.3/portal.egg-info}/PKG-INFO +1 -1
- {portal-3.4.1 → portal-3.4.3}/tests/test_server.py +22 -3
- {portal-3.4.1 → portal-3.4.3}/tests/test_socket.py +0 -2
- {portal-3.4.1 → portal-3.4.3}/LICENSE +0 -0
- {portal-3.4.1 → portal-3.4.3}/MANIFEST.in +0 -0
- {portal-3.4.1 → portal-3.4.3}/README.md +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/batching.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/buffers.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/client.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/contextlib.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/packlib.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/poollib.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/process.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/sharray.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/thread.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal/utils.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal.egg-info/SOURCES.txt +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal.egg-info/dependency_links.txt +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal.egg-info/requires.txt +0 -0
- {portal-3.4.1 → portal-3.4.3}/portal.egg-info/top_level.txt +0 -0
- {portal-3.4.1 → portal-3.4.3}/pyproject.toml +0 -0
- {portal-3.4.1 → portal-3.4.3}/requirements.txt +0 -0
- {portal-3.4.1 → portal-3.4.3}/setup.cfg +0 -0
- {portal-3.4.1 → portal-3.4.3}/setup.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/tests/test_batching.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/tests/test_client.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/tests/test_errfile.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/tests/test_pack.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/tests/test_process.py +0 -0
- {portal-3.4.1 → portal-3.4.3}/tests/test_thread.py +0 -0
@@ -30,6 +30,7 @@ class Options:
|
|
30
30
|
logging: bool = True
|
31
31
|
logging_color: str = 'yellow'
|
32
32
|
connect_wait: float = 0.1
|
33
|
+
loop_sleep: float = 0.0
|
33
34
|
|
34
35
|
|
35
36
|
class ClientSocket:
|
@@ -175,6 +176,9 @@ class ClientSocket:
|
|
175
176
|
[x() for x in self.callbacks_disc]
|
176
177
|
continue
|
177
178
|
|
179
|
+
if self.options.loop_sleep:
|
180
|
+
time.sleep(self.options.loop_sleep)
|
181
|
+
|
178
182
|
if sock:
|
179
183
|
sock.close()
|
180
184
|
|
@@ -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)
|
@@ -3,6 +3,7 @@ import dataclasses
|
|
3
3
|
import queue
|
4
4
|
import selectors
|
5
5
|
import socket
|
6
|
+
import time
|
6
7
|
|
7
8
|
from . import buffers
|
8
9
|
from . import contextlib
|
@@ -31,6 +32,7 @@ class Options:
|
|
31
32
|
max_send_queue: int = 4096
|
32
33
|
logging: bool = True
|
33
34
|
logging_color: str = 'blue'
|
35
|
+
loop_sleep: float = 0.0
|
34
36
|
|
35
37
|
|
36
38
|
class ServerSocket:
|
@@ -127,6 +129,8 @@ class ServerSocket:
|
|
127
129
|
# The client is gone but we may have buffered messages left to
|
128
130
|
# read, so we keep the socket open until recv() fails.
|
129
131
|
pass
|
132
|
+
if self.options.loop_sleep:
|
133
|
+
time.sleep(self.options.loop_sleep)
|
130
134
|
except Exception as e:
|
131
135
|
self.error = e
|
132
136
|
|
@@ -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
|
File without changes
|