portal 3.1.11__tar.gz → 3.2.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 (35) hide show
  1. {portal-3.1.11/portal.egg-info → portal-3.2.0}/PKG-INFO +1 -1
  2. {portal-3.1.11 → portal-3.2.0}/portal/__init__.py +2 -3
  3. {portal-3.1.11 → portal-3.2.0}/portal/batching.py +7 -7
  4. {portal-3.1.11 → portal-3.2.0}/portal/client.py +4 -2
  5. {portal-3.1.11 → portal-3.2.0}/portal/client_socket.py +0 -2
  6. {portal-3.1.11 → portal-3.2.0}/portal/contextlib.py +33 -16
  7. {portal-3.1.11 → portal-3.2.0}/portal/process.py +37 -21
  8. {portal-3.1.11 → portal-3.2.0}/portal/server.py +1 -1
  9. {portal-3.1.11 → portal-3.2.0}/portal/thread.py +16 -12
  10. {portal-3.1.11 → portal-3.2.0}/portal/utils.py +26 -12
  11. {portal-3.1.11 → portal-3.2.0/portal.egg-info}/PKG-INFO +1 -1
  12. {portal-3.1.11 → portal-3.2.0}/tests/test_client.py +2 -6
  13. {portal-3.1.11 → portal-3.2.0}/tests/test_errfile.py +46 -19
  14. {portal-3.1.11 → portal-3.2.0}/tests/test_process.py +20 -12
  15. {portal-3.1.11 → portal-3.2.0}/tests/test_server.py +9 -0
  16. {portal-3.1.11 → portal-3.2.0}/tests/test_thread.py +1 -1
  17. {portal-3.1.11 → portal-3.2.0}/LICENSE +0 -0
  18. {portal-3.1.11 → portal-3.2.0}/MANIFEST.in +0 -0
  19. {portal-3.1.11 → portal-3.2.0}/README.md +0 -0
  20. {portal-3.1.11 → portal-3.2.0}/portal/buffers.py +0 -0
  21. {portal-3.1.11 → portal-3.2.0}/portal/packlib.py +0 -0
  22. {portal-3.1.11 → portal-3.2.0}/portal/poollib.py +0 -0
  23. {portal-3.1.11 → portal-3.2.0}/portal/server_socket.py +0 -0
  24. {portal-3.1.11 → portal-3.2.0}/portal/sharray.py +0 -0
  25. {portal-3.1.11 → portal-3.2.0}/portal.egg-info/SOURCES.txt +0 -0
  26. {portal-3.1.11 → portal-3.2.0}/portal.egg-info/dependency_links.txt +0 -0
  27. {portal-3.1.11 → portal-3.2.0}/portal.egg-info/requires.txt +0 -0
  28. {portal-3.1.11 → portal-3.2.0}/portal.egg-info/top_level.txt +0 -0
  29. {portal-3.1.11 → portal-3.2.0}/pyproject.toml +0 -0
  30. {portal-3.1.11 → portal-3.2.0}/requirements.txt +0 -0
  31. {portal-3.1.11 → portal-3.2.0}/setup.cfg +0 -0
  32. {portal-3.1.11 → portal-3.2.0}/setup.py +0 -0
  33. {portal-3.1.11 → portal-3.2.0}/tests/test_batching.py +0 -0
  34. {portal-3.1.11 → portal-3.2.0}/tests/test_pack.py +0 -0
  35. {portal-3.1.11 → portal-3.2.0}/tests/test_socket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.1.11
3
+ Version: 3.2.0
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.1.11'
1
+ __version__ = '3.2.0'
2
2
 
3
3
  import multiprocessing as mp
4
4
  try:
@@ -29,6 +29,5 @@ from .packlib import tree_equals
29
29
  from .sharray import SharedArray
30
30
 
31
31
  from .utils import free_port
32
- from .utils import kill_procs
33
- from .utils import kill_threads
32
+ from .utils import proc_alive
34
33
  from .utils import run
@@ -53,8 +53,8 @@ class BatchServer:
53
53
  def close(self, timeout=None):
54
54
  assert self.started
55
55
  self.running.clear()
56
- self.server.close(timeout and 0.5 * timeout)
57
- self.batcher.join(timeout and 0.5 * timeout)
56
+ self.server.close(timeout)
57
+ self.batcher.join(timeout)
58
58
  self.batcher.kill()
59
59
 
60
60
  def stats(self):
@@ -160,12 +160,12 @@ def batcher(
160
160
  if errors:
161
161
  raise RuntimeError(message)
162
162
 
163
- outer = server_socket.ServerSocket(outer_port, f'{name}Server', **kwargs)
164
- inner = client.Client('localhost', inner_port, f'{name}Client', **kwargs)
165
- batches = {} # {method: ([addr], [reqnum], structure, [array])}
166
- jobs = []
167
- shutdown = False
168
163
  try:
164
+ outer = server_socket.ServerSocket(outer_port, f'{name}Server', **kwargs)
165
+ inner = client.Client('localhost', inner_port, f'{name}Client', **kwargs)
166
+ batches = {} # {method: ([addr], [reqnum], structure, [array])}
167
+ jobs = []
168
+ shutdown = False
169
169
  while running.is_set() or jobs:
170
170
  if running.is_set():
171
171
  maybe_recv(outer, inner, jobs, batches)
@@ -80,7 +80,6 @@ class Client:
80
80
  name = method.encode('utf-8')
81
81
  strlen = len(name).to_bytes(8, 'little', signed=False)
82
82
  sendargs = (reqnum, strlen, name, *packlib.pack(data))
83
- # self.socket.send(reqnum, strlen, name, *packlib.pack(data))
84
83
  rai = [False]
85
84
  future = Future(rai)
86
85
  future.sendargs = sendargs
@@ -117,7 +116,10 @@ class Client:
117
116
  message = bytes(data[16:]).decode('utf-8')
118
117
  self._seterr(future, RuntimeError(message))
119
118
  with self.cond: self.cond.notify_all()
120
- self.socket.recv()
119
+ try:
120
+ self.socket.recv()
121
+ except AssertionError:
122
+ pass # Socket is already closed.
121
123
 
122
124
  def _disc(self):
123
125
  if self.socket.options.autoconn:
@@ -64,7 +64,6 @@ class ClientSocket:
64
64
  return self.isconn.is_set()
65
65
 
66
66
  def connect(self, timeout=None):
67
- assert not self.connected
68
67
  if not self.options.autoconn:
69
68
  self.wantconn.set()
70
69
  return self.isconn.wait(timeout)
@@ -137,7 +136,6 @@ class ClientSocket:
137
136
  continue
138
137
  _, mask = pairs[0]
139
138
 
140
-
141
139
  if mask & select.POLLIN:
142
140
  try:
143
141
  recvbuf.recv(sock)
@@ -1,4 +1,3 @@
1
- import collections
2
1
  import multiprocessing as mp
3
2
  import os
4
3
  import pathlib
@@ -23,7 +22,6 @@ class Context:
23
22
  self.serverkw = {}
24
23
  self.done = threading.Event()
25
24
  self.watcher = None
26
- self.children = collections.defaultdict(list)
27
25
  self.mp = mp.get_context()
28
26
  self.printlock = self.mp.Lock()
29
27
 
@@ -61,6 +59,7 @@ class Context:
61
59
  assert hasattr(errfile, 'exists') and hasattr(errfile, 'write_text')
62
60
  self.errfile = errfile
63
61
  self._check_errfile()
62
+ self._install_excepthook()
64
63
 
65
64
  if interval:
66
65
  assert isinstance(interval, (int, float))
@@ -122,10 +121,14 @@ class Context:
122
121
  print(f'Wrote errorfile: {self.errfile}', file=sys.stderr)
123
122
 
124
123
  def shutdown(self, exitcode):
125
- # This kills the process tree forcefully to prevent hangs but results in
126
- # leaked semaphore warnings. However, the leaked objects are still cleaned
127
- # up by the resource tracker process of Python's multiprocessing module.
128
- utils.kill_procs(psutil.Process().children(recursive=True))
124
+ children = list(psutil.Process(os.getpid()).children(recursive=True))
125
+ utils.kill_proc(children, timeout=1)
126
+ # TODO
127
+ # if exitcode == 0:
128
+ # for child in self.children(threading.main_thread()):
129
+ # child.kill()
130
+ # os._exit(0)
131
+ # else:
129
132
  os._exit(exitcode)
130
133
 
131
134
  def close(self):
@@ -133,21 +136,35 @@ class Context:
133
136
  if self.watcher:
134
137
  self.watcher.join()
135
138
 
136
- def add_child(self, worker):
137
- ident = threading.get_ident()
138
- if hasattr(worker, 'ident'):
139
- assert worker.ident != ident
140
- self.children[ident].append(worker)
141
-
142
- def get_children(self, ident=None):
143
- if ident is None:
144
- ident = threading.get_ident()
145
- return self.children[ident]
139
+ def add_worker(self, worker):
140
+ assert hasattr(worker, 'kill')
141
+ current = threading.current_thread()
142
+ if current == threading.main_thread():
143
+ return
144
+ if hasattr(worker, 'thread'):
145
+ assert current != worker.thread
146
+ current.children.append(worker)
147
+
148
+ def children(self, thread):
149
+ current = thread or threading.current_thread()
150
+ if current == threading.main_thread():
151
+ return []
152
+ return current.children
146
153
 
147
154
  def _watcher(self):
148
155
  while not self.done.wait(self.interval):
149
156
  self._check_errfile()
150
157
 
158
+ def _install_excepthook(self):
159
+ existing = sys.excepthook
160
+ def patched(typ, val, tb):
161
+ if self.errfile:
162
+ message = ''.join(traceback.format_exception(typ, val, tb)).strip('\n')
163
+ self.errfile.write_text(message)
164
+ print(f'Wrote errorfile: {self.errfile}', file=sys.stderr)
165
+ return existing(typ, val, tb)
166
+ sys.excepthook = patched
167
+
151
168
  def _check_errfile(self):
152
169
  if self.errfile and self.errfile.exists():
153
170
  message = f'Shutting down due to error file: {self.errfile}'
@@ -1,4 +1,6 @@
1
1
  import atexit
2
+ import errno
3
+ import os
2
4
  import traceback
3
5
 
4
6
  import cloudpickle
@@ -20,24 +22,28 @@ class Process:
20
22
  2. The process terminates its nested child processes when it encounters an
21
23
  error. This prevents lingering subprocesses on error.
22
24
 
23
- 3. When the parent process encounters an error, the subprocess will be killed
25
+ 3. When the parent process encounters an error, its subprocess will be killed
24
26
  via `atexit`, preventing hangs.
25
27
 
26
28
  4. It inherits the context object of its parent process, which provides
27
29
  cloudpickled initializer functions for each nested child process and error
28
30
  file watching for global shutdown on error.
31
+
32
+ 5. Standard Python subprocesses do not reliably return the running() state of
33
+ the process. This class makes it more reliable.
29
34
  """
30
35
 
31
36
  def __init__(self, fn, *args, name=None, start=False):
32
37
  name = name or getattr(fn, '__name__', 'process')
33
38
  fn = cloudpickle.dumps(fn)
34
- context = contextlib.context
35
- options = context.options()
36
- self.process = context.mp.Process(
39
+ options = contextlib.context.options()
40
+ self.process = contextlib.context.mp.Process(
37
41
  target=self._wrapper, name=name, args=(options, name, fn, args))
38
- context.add_child(self)
39
42
  self.started = False
43
+ self.killed = False
44
+ self.thepid = None
40
45
  atexit.register(self.kill)
46
+ contextlib.context.add_worker(self)
41
47
  start and self.start()
42
48
 
43
49
  @property
@@ -46,44 +52,54 @@ class Process:
46
52
 
47
53
  @property
48
54
  def pid(self):
49
- return self.process.pid
55
+ return self.thepid
50
56
 
51
57
  @property
52
58
  def running(self):
53
59
  if not self.started:
54
60
  return False
55
- return self.process.is_alive()
61
+ if not self.process.is_alive():
62
+ return False
63
+ return utils.proc_alive(self.pid)
56
64
 
57
65
  @property
58
66
  def exitcode(self):
59
- if not self.started or self.running:
60
- return None
61
- else:
62
- return self.process.exitcode
67
+ exitcode = self.process.exitcode
68
+ if self.killed and exitcode is None:
69
+ return -9
70
+ return exitcode
63
71
 
64
72
  def start(self):
65
73
  assert not self.started
66
74
  self.started = True
67
75
  self.process.start()
68
- assert self.pid is not None
76
+ self.thepid = self.process.pid
77
+ assert self.thepid is not None
69
78
  return self
70
79
 
71
80
  def join(self, timeout=None):
72
81
  assert self.started
73
- if self.running:
74
- self.process.join(timeout)
82
+ self.process.join(timeout)
75
83
  return self
76
84
 
77
85
  def kill(self, timeout=1):
86
+ # Cannot early exit if process is not running, because it may just be
87
+ # starting up.
88
+ assert self.started
78
89
  try:
79
- proc = psutil.Process(self.pid)
80
- tree = [proc] + list(proc.children(recursive=True))
90
+ children = list(psutil.Process(self.pid).children(recursive=True))
81
91
  except psutil.NoSuchProcess:
82
- tree = []
83
- self.process.terminate()
84
- self.process.join(timeout / 2)
85
- self.process.kill()
86
- utils.kill_procs(tree, timeout / 2)
92
+ children = []
93
+ try:
94
+ self.process.terminate()
95
+ self.process.join(timeout)
96
+ self.process.kill()
97
+ utils.kill_proc(children, timeout)
98
+ except OSError as e:
99
+ if e.errno != errno.ESRCH:
100
+ contextlib.context.error(e, self.name)
101
+ contextlib.context.shutdown(exitcode=1)
102
+ self.killed = True
87
103
  return self
88
104
 
89
105
  def __repr__(self):
@@ -83,7 +83,7 @@ class Server:
83
83
  self.close()
84
84
 
85
85
  def _loop(self):
86
- while self.running or self.jobs or self.postfn_out:
86
+ while self.running or self.jobs or self.postfn_inp or self.postfn_out:
87
87
  while True: # Loop syntax used to break on error.
88
88
  if not self.running: # Do not accept further requests.
89
89
  break
@@ -1,5 +1,4 @@
1
1
  import threading
2
- import time
3
2
  import traceback
4
3
 
5
4
  from . import contextlib
@@ -22,13 +21,16 @@ class Thread:
22
21
  """
23
22
 
24
23
  def __init__(self, fn, *args, name=None, start=False):
24
+ global TIDS
25
25
  self.fn = fn
26
26
  self.excode = None
27
27
  name = name or getattr(fn, '__name__', 'thread')
28
28
  self.thread = threading.Thread(
29
29
  target=self._wrapper, args=args, name=name, daemon=True)
30
- contextlib.context.add_child(self)
30
+ self.thread.children = []
31
31
  self.started = False
32
+ self.ready = threading.Barrier(2)
33
+ contextlib.context.add_worker(self)
32
34
  start and self.start()
33
35
 
34
36
  @property
@@ -36,8 +38,8 @@ class Thread:
36
38
  return self.thread.name
37
39
 
38
40
  @property
39
- def ident(self):
40
- return self.thread.ident
41
+ def tid(self):
42
+ return self.thetid
41
43
 
42
44
  @property
43
45
  def running(self):
@@ -53,15 +55,18 @@ class Thread:
53
55
  assert not self.started
54
56
  self.started = True
55
57
  self.thread.start()
58
+ self.ready.wait()
56
59
  return self
57
60
 
58
61
  def join(self, timeout=None):
59
- if self.running:
60
- self.thread.join(timeout)
62
+ self.thread.join(timeout)
61
63
  return self
62
64
 
63
- def kill(self, timeout=3):
64
- utils.kill_threads(self.thread, timeout)
65
+ def kill(self, timeout=1.0):
66
+ assert self.thread != threading.current_thread()
67
+ for child in contextlib.context.children(self.thread):
68
+ child.kill(timeout)
69
+ utils.kill_thread(self.thread, timeout)
65
70
  return self
66
71
 
67
72
  def __repr__(self):
@@ -71,6 +76,7 @@ class Thread:
71
76
 
72
77
  def _wrapper(self, *args):
73
78
  try:
79
+ self.ready.wait()
74
80
  exitcode = self.fn(*args)
75
81
  exitcode = exitcode if isinstance(exitcode, int) else 0
76
82
  self.excode = exitcode
@@ -78,10 +84,8 @@ class Thread:
78
84
  compact = traceback.format_tb(e.__traceback__)
79
85
  compact = '\n'.join([line.split('\n', 1)[0] for line in compact])
80
86
  print(f"Killed thread '{self.name}' at:\n{compact}")
81
- [x.kill(0.1) for x in contextlib.context.get_children()]
87
+ [x.kill(0.1) for x in contextlib.context.children(self.thread)]
82
88
  self.excode = 2
83
89
  except Exception as e:
84
- [x.kill(0.1) for x in contextlib.context.get_children()]
85
90
  contextlib.context.error(e, self.name)
86
- contextlib.context.shutdown(1)
87
- self.excode = 1
91
+ contextlib.context.shutdown(exitcode=1)
@@ -1,10 +1,11 @@
1
1
  import ctypes
2
+ import errno
3
+ import os
2
4
  import socket
3
5
  import sys
4
6
  import threading
5
7
  import time
6
8
 
7
- import numpy as np
8
9
  import psutil
9
10
 
10
11
 
@@ -30,9 +31,8 @@ def run(workers, duration=None):
30
31
  raise RuntimeError(f"'{name}' crashed with exit code {code}")
31
32
 
32
33
 
33
- def kill_threads(threads, timeout=3):
34
+ def kill_thread(threads, timeout=1):
34
35
  threads = threads if isinstance(threads, (list, tuple)) else [threads]
35
- threads = [x for x in threads if x is not threading.main_thread()]
36
36
  for thread in threads:
37
37
  if thread.native_id is None:
38
38
  # Wait because thread may currently be starting.
@@ -52,7 +52,7 @@ def kill_threads(threads, timeout=3):
52
52
  print('Killed thread is still alive.')
53
53
 
54
54
 
55
- def kill_procs(procs, timeout=3):
55
+ def kill_proc(procs, timeout=1):
56
56
  def eachproc(fn, procs):
57
57
  result = []
58
58
  for proc in list(procs):
@@ -75,19 +75,33 @@ def kill_procs(procs, timeout=3):
75
75
  # Should never happen but print warning if any survived.
76
76
  eachproc(lambda p: (
77
77
  print('Killed subprocess is still alive.')
78
- if p.status() != psutil.STATUS_ZOMBIE else None), procs)
78
+ if proc_alive(p.pid) else None), procs)
79
79
 
80
80
 
81
- def free_port(low=10000, high=50000):
81
+ def proc_alive(pid):
82
+ try:
83
+ if psutil.Process(pid).status() == psutil.STATUS_ZOMBIE:
84
+ return False
85
+ except psutil.NoSuchProcess:
86
+ return False
87
+ try:
88
+ os.kill(pid, 0)
89
+ except OSError as e:
90
+ if e.errno == errno.ESRCH:
91
+ return False
92
+ return True
93
+
94
+
95
+ def free_port():
82
96
  # Return a port that is currently free. This function is not thread or
83
97
  # process safe, because there is no way to guarantee that the port will still
84
98
  # be free at the time it will be used.
85
- rng = np.random.default_rng()
86
- while True:
87
- port = int(rng.integers(low, high))
88
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
89
- if s.connect_ex(('', port)):
90
- return port
99
+ sock = socket.socket()
100
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
101
+ sock.bind(('localhost', 0))
102
+ port = sock.getsockname()[1]
103
+ sock.close()
104
+ return port
91
105
 
92
106
 
93
107
  def style(color=None, background=None, bold=None, underline=None, reset=None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.1.11
3
+ Version: 3.2.0
4
4
  Summary: Fast and reliable distributed systems in Python
5
5
  Home-page: http://github.com/danijar/portal
6
6
  Author: Danijar Hafner
@@ -121,7 +121,7 @@ class TestClient:
121
121
  future.result(timeout=0.01)
122
122
  with pytest.raises(TimeoutError):
123
123
  future.result(timeout=0)
124
- assert future.result(timeout=0.2) == 42
124
+ assert future.result(timeout=1) == 42
125
125
  client.close()
126
126
  server.close()
127
127
 
@@ -155,14 +155,10 @@ class TestClient:
155
155
  server = portal.Server(port)
156
156
  server.bind('fn', lambda x: x)
157
157
  server.start(block=False)
158
- client = portal.Client('localhost', port)
158
+ client = portal.Client('localhost', port, maxinflight=1)
159
159
  client.fn(1)
160
160
  client.fn(2)
161
- # Wait for the server to respond to the first two requests, so that all
162
- # futures are inside the client by the time we block on the third future.
163
- time.sleep(0.1)
164
161
  future3 = client.fn(3)
165
- assert len(client.futures) == 1
166
162
  assert future3.result() == 3
167
163
  del future3
168
164
  assert len(client.futures) == 0
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import pathlib
2
3
  import time
3
4
 
@@ -50,37 +51,63 @@ class TestErrfile:
50
51
  assert "Error in 'fn1' (ValueError: reason):" == first_line
51
52
  assert not worker1.running
52
53
  assert not worker2.running
53
- assert worker1.exitcode == 1
54
+ # The first worker may shut itself down or be shut down based on its own
55
+ # error file watcher, based on how the threads context-switch.
56
+ assert worker1.exitcode in (1, 2)
54
57
  assert worker2.exitcode == 2
55
58
 
56
59
  @pytest.mark.parametrize('repeat', range(3))
57
60
  def test_nested_procs(self, tmpdir, repeat):
58
61
  errfile = pathlib.Path(tmpdir) / 'error'
59
- ready = portal.context.mp.Semaphore(0)
62
+ ready = portal.context.mp.Barrier(7)
63
+ queue = portal.context.mp.Queue()
60
64
 
61
- def hang():
65
+ def outer(ready, queue, errfile):
66
+ portal.setup(errfile=errfile, interval=0.1)
67
+ portal.Process(inner, ready, queue, name='inner', start=True)
68
+ portal.Thread(hang_thread, ready, start=True)
69
+ portal.Process(hang_process, ready, queue, start=True)
70
+ queue.put(os.getpid())
71
+ queue.close()
72
+ queue.join_thread()
73
+ ready.wait() # 1
62
74
  while True:
63
75
  time.sleep(0.1)
64
76
 
65
- def outer(ready, errfile):
66
- portal.setup(errfile=errfile, interval=0.1)
67
- portal.Process(inner, ready, errfile, name='inner', start=True)
68
- portal.Thread(hang, start=True)
69
- portal.Process(hang, start=True)
70
- ready.release()
71
- hang()
72
-
73
- def inner(ready, errfile):
74
- portal.setup(errfile=errfile, interval=0.1)
75
- portal.Thread(hang, start=True)
76
- portal.Process(hang, start=True)
77
- ready.release()
77
+ def inner(ready, queue):
78
+ assert portal.context.errfile
79
+ portal.Thread(hang_thread, ready, start=True)
80
+ portal.Process(hang_process, ready, queue, start=True)
81
+ queue.put(os.getpid())
82
+ queue.close()
83
+ queue.join_thread()
84
+ ready.wait() # 2
78
85
  raise ValueError('reason')
79
86
 
80
- worker = portal.Process(outer, ready, errfile, name='outer', start=True)
81
- ready.acquire()
82
- ready.acquire()
87
+ def hang_thread(ready):
88
+ ready.wait() # 3, 4
89
+ while True:
90
+ time.sleep(0.1)
91
+
92
+ def hang_process(ready, queue):
93
+ assert portal.context.errfile
94
+ queue.put(os.getpid())
95
+ queue.close()
96
+ queue.join_thread()
97
+ ready.wait() # 5, 6
98
+ while True:
99
+ time.sleep(0.1)
100
+
101
+ worker = portal.Process(
102
+ outer, ready, queue, errfile, name='outer', start=True)
103
+ ready.wait() # 7
83
104
  worker.join()
84
105
  content = errfile.read_text()
85
106
  assert "Error in 'inner' (ValueError: reason):" == content.split('\n')[0]
86
107
  assert not worker.running
108
+ pids = [queue.get() for _ in range(4)]
109
+ time.sleep(2.0) # On some systems this can take a while.
110
+ assert not portal.proc_alive(pids[0])
111
+ assert not portal.proc_alive(pids[1])
112
+ assert not portal.proc_alive(pids[2])
113
+ assert not portal.proc_alive(pids[3])
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import time
2
3
 
3
4
  import pytest
@@ -43,26 +44,33 @@ class TestProcess:
43
44
  worker = portal.Process(fn, start=True)
44
45
  worker.kill()
45
46
  assert not worker.running
46
- assert worker.exitcode == -15
47
+ assert worker.exitcode < 0
47
48
 
48
49
  @pytest.mark.parametrize('repeat', range(5))
49
50
  def test_kill_with_subproc(self, repeat):
50
- ready = portal.context.mp.Semaphore(0)
51
- def outer(ready):
52
- portal.Process(inner, ready, start=True)
53
- ready.release()
51
+ ready = portal.context.mp.Barrier(3)
52
+ queue = portal.context.mp.Queue()
53
+
54
+ def outer(ready, queue):
55
+ queue.put(os.getpid())
56
+ portal.Process(inner, ready, queue, start=True)
57
+ ready.wait()
54
58
  while True:
55
59
  time.sleep(0.1)
56
- def inner(ready):
57
- ready.release()
60
+
61
+ def inner(ready, queue):
62
+ queue.put(os.getpid())
63
+ ready.wait()
58
64
  while True:
59
65
  time.sleep(0.1)
60
- worker = portal.Process(outer, ready, start=True)
61
- ready.acquire()
62
- ready.acquire()
66
+
67
+ worker = portal.Process(outer, ready, queue, start=True)
68
+ ready.wait()
63
69
  worker.kill()
64
70
  assert not worker.running
65
- assert worker.exitcode == -15
71
+ assert worker.exitcode < 0
72
+ assert not portal.proc_alive(queue.get())
73
+ assert not portal.proc_alive(queue.get())
66
74
 
67
75
  @pytest.mark.parametrize('repeat', range(5))
68
76
  def test_kill_with_subthread(self, repeat):
@@ -79,7 +87,7 @@ class TestProcess:
79
87
  ready.wait()
80
88
  worker.kill()
81
89
  assert not worker.running
82
- assert worker.exitcode == -15
90
+ assert worker.exitcode < 0
83
91
 
84
92
  def test_initfn(self):
85
93
  def init():
@@ -151,16 +151,19 @@ class TestServer:
151
151
  lock = threading.Lock()
152
152
  work_calls = [0]
153
153
  done_calls = [0]
154
+
154
155
  def workfn(x):
155
156
  with lock:
156
157
  work_calls[0] += 1
157
158
  print(work_calls[0], done_calls[0])
158
159
  assert work_calls[0] <= done_calls[0] + workers + 1
159
160
  return x, x
161
+
160
162
  def postfn(x):
161
163
  with lock:
162
164
  done_calls[0] += 1
163
165
  time.sleep(0.01)
166
+
164
167
  server = Server(port, workers=workers)
165
168
  server.bind('fn', workfn, postfn)
166
169
  server.start(block=False)
@@ -173,11 +176,14 @@ class TestServer:
173
176
  @pytest.mark.parametrize('repeat', range(3))
174
177
  @pytest.mark.parametrize('Server', SERVERS)
175
178
  def test_shared_pool(self, repeat, Server):
179
+
176
180
  def slow(x):
177
181
  time.sleep(0.2)
178
182
  return x
183
+
179
184
  def fast(x):
180
185
  return x
186
+
181
187
  port = portal.free_port()
182
188
  server = Server(port, workers=1)
183
189
  server.bind('slow', slow)
@@ -197,11 +203,14 @@ class TestServer:
197
203
  @pytest.mark.parametrize('repeat', range(3))
198
204
  @pytest.mark.parametrize('Server', SERVERS)
199
205
  def test_separate_pools(self, repeat, Server):
206
+
200
207
  def slow(x):
201
208
  time.sleep(0.1)
202
209
  return x
210
+
203
211
  def fast(x):
204
212
  return x
213
+
205
214
  port = portal.free_port()
206
215
  server = Server(port)
207
216
  server.bind('slow', slow, workers=1)
@@ -91,4 +91,4 @@ class TestThread:
91
91
  assert not worker.running
92
92
  assert not proc[0].running
93
93
  assert worker.exitcode == 2
94
- assert proc[0].exitcode == -15
94
+ assert proc[0].exitcode < 0
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