portal 3.4.1__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.
Files changed (35) hide show
  1. {portal-3.4.1/portal.egg-info → portal-3.4.2}/PKG-INFO +1 -1
  2. {portal-3.4.1 → portal-3.4.2}/portal/__init__.py +1 -1
  3. {portal-3.4.1 → portal-3.4.2}/portal/server.py +40 -26
  4. {portal-3.4.1 → portal-3.4.2/portal.egg-info}/PKG-INFO +1 -1
  5. {portal-3.4.1 → portal-3.4.2}/tests/test_server.py +22 -3
  6. {portal-3.4.1 → portal-3.4.2}/tests/test_socket.py +0 -2
  7. {portal-3.4.1 → portal-3.4.2}/LICENSE +0 -0
  8. {portal-3.4.1 → portal-3.4.2}/MANIFEST.in +0 -0
  9. {portal-3.4.1 → portal-3.4.2}/README.md +0 -0
  10. {portal-3.4.1 → portal-3.4.2}/portal/batching.py +0 -0
  11. {portal-3.4.1 → portal-3.4.2}/portal/buffers.py +0 -0
  12. {portal-3.4.1 → portal-3.4.2}/portal/client.py +0 -0
  13. {portal-3.4.1 → portal-3.4.2}/portal/client_socket.py +0 -0
  14. {portal-3.4.1 → portal-3.4.2}/portal/contextlib.py +0 -0
  15. {portal-3.4.1 → portal-3.4.2}/portal/packlib.py +0 -0
  16. {portal-3.4.1 → portal-3.4.2}/portal/poollib.py +0 -0
  17. {portal-3.4.1 → portal-3.4.2}/portal/process.py +0 -0
  18. {portal-3.4.1 → portal-3.4.2}/portal/server_socket.py +0 -0
  19. {portal-3.4.1 → portal-3.4.2}/portal/sharray.py +0 -0
  20. {portal-3.4.1 → portal-3.4.2}/portal/thread.py +0 -0
  21. {portal-3.4.1 → portal-3.4.2}/portal/utils.py +0 -0
  22. {portal-3.4.1 → portal-3.4.2}/portal.egg-info/SOURCES.txt +0 -0
  23. {portal-3.4.1 → portal-3.4.2}/portal.egg-info/dependency_links.txt +0 -0
  24. {portal-3.4.1 → portal-3.4.2}/portal.egg-info/requires.txt +0 -0
  25. {portal-3.4.1 → portal-3.4.2}/portal.egg-info/top_level.txt +0 -0
  26. {portal-3.4.1 → portal-3.4.2}/pyproject.toml +0 -0
  27. {portal-3.4.1 → portal-3.4.2}/requirements.txt +0 -0
  28. {portal-3.4.1 → portal-3.4.2}/setup.cfg +0 -0
  29. {portal-3.4.1 → portal-3.4.2}/setup.py +0 -0
  30. {portal-3.4.1 → portal-3.4.2}/tests/test_batching.py +0 -0
  31. {portal-3.4.1 → portal-3.4.2}/tests/test_client.py +0 -0
  32. {portal-3.4.1 → portal-3.4.2}/tests/test_errfile.py +0 -0
  33. {portal-3.4.1 → portal-3.4.2}/tests/test_pack.py +0 -0
  34. {portal-3.4.1 → portal-3.4.2}/tests/test_process.py +0 -0
  35. {portal-3.4.1 → portal-3.4.2}/tests/test_thread.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.4.1
3
+ Version: 3.4.2
4
4
  Summary: Fast and reliable distributed systems in Python
5
5
  Home-page: http://github.com/danijar/portal
6
6
  Author: Danijar Hafner
@@ -1,4 +1,4 @@
1
- __version__ = '3.4.1'
1
+ __version__ = '3.4.2'
2
2
 
3
3
  import multiprocessing as mp
4
4
  try:
@@ -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
- active = threading.Semaphore((workers or self.workers) + 1)
38
- def workfn2(*args):
39
- active.acquire()
40
- return workfn(*args)
41
- self.methods[name] = (workfn2, postfn, pool, active)
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 _, postfn, _, _ in self.methods.values()):
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
- while self.running or self.jobs or self.postfn_inp or self.postfn_out:
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
- workfn, postfn, pool, active = self.methods[name]
111
- job = pool.submit(workfn, *data)
112
- job.active = active
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, info = 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.active.release()
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
- data, info = job.result()
141
- postjob = self.postfn_pool.submit(job.postfn, info)
142
- postjob.active = job.active
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.4.1
3
+ Version: 3.4.2
4
4
  Summary: Fast and reliable distributed systems in Python
5
5
  Home-page: http://github.com/danijar/portal
6
6
  Author: Danijar Hafner
@@ -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.01)
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.01)
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.2)
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