portal 3.1.12__tar.gz → 3.2.1__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.12/portal.egg-info → portal-3.2.1}/PKG-INFO +1 -1
  2. {portal-3.1.12 → portal-3.2.1}/portal/__init__.py +2 -3
  3. {portal-3.1.12 → portal-3.2.1}/portal/batching.py +7 -7
  4. {portal-3.1.12 → portal-3.2.1}/portal/client.py +4 -2
  5. {portal-3.1.12 → portal-3.2.1}/portal/client_socket.py +0 -2
  6. {portal-3.1.12 → portal-3.2.1}/portal/contextlib.py +22 -16
  7. {portal-3.1.12 → portal-3.2.1}/portal/process.py +40 -23
  8. {portal-3.1.12 → portal-3.2.1}/portal/server.py +4 -1
  9. {portal-3.1.12 → portal-3.2.1}/portal/thread.py +15 -12
  10. {portal-3.1.12 → portal-3.2.1}/portal/utils.py +19 -5
  11. {portal-3.1.12 → portal-3.2.1/portal.egg-info}/PKG-INFO +1 -1
  12. {portal-3.1.12 → portal-3.2.1}/tests/test_client.py +2 -6
  13. {portal-3.1.12 → portal-3.2.1}/tests/test_errfile.py +46 -19
  14. {portal-3.1.12 → portal-3.2.1}/tests/test_process.py +38 -13
  15. {portal-3.1.12 → portal-3.2.1}/tests/test_server.py +32 -2
  16. {portal-3.1.12 → portal-3.2.1}/tests/test_thread.py +1 -1
  17. {portal-3.1.12 → portal-3.2.1}/LICENSE +0 -0
  18. {portal-3.1.12 → portal-3.2.1}/MANIFEST.in +0 -0
  19. {portal-3.1.12 → portal-3.2.1}/README.md +0 -0
  20. {portal-3.1.12 → portal-3.2.1}/portal/buffers.py +0 -0
  21. {portal-3.1.12 → portal-3.2.1}/portal/packlib.py +0 -0
  22. {portal-3.1.12 → portal-3.2.1}/portal/poollib.py +0 -0
  23. {portal-3.1.12 → portal-3.2.1}/portal/server_socket.py +0 -0
  24. {portal-3.1.12 → portal-3.2.1}/portal/sharray.py +0 -0
  25. {portal-3.1.12 → portal-3.2.1}/portal.egg-info/SOURCES.txt +0 -0
  26. {portal-3.1.12 → portal-3.2.1}/portal.egg-info/dependency_links.txt +0 -0
  27. {portal-3.1.12 → portal-3.2.1}/portal.egg-info/requires.txt +0 -0
  28. {portal-3.1.12 → portal-3.2.1}/portal.egg-info/top_level.txt +0 -0
  29. {portal-3.1.12 → portal-3.2.1}/pyproject.toml +0 -0
  30. {portal-3.1.12 → portal-3.2.1}/requirements.txt +0 -0
  31. {portal-3.1.12 → portal-3.2.1}/setup.cfg +0 -0
  32. {portal-3.1.12 → portal-3.2.1}/setup.py +0 -0
  33. {portal-3.1.12 → portal-3.2.1}/tests/test_batching.py +0 -0
  34. {portal-3.1.12 → portal-3.2.1}/tests/test_pack.py +0 -0
  35. {portal-3.1.12 → portal-3.2.1}/tests/test_socket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.1.12
3
+ Version: 3.2.1
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.12'
1
+ __version__ = '3.2.1'
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
 
@@ -123,10 +121,14 @@ class Context:
123
121
  print(f'Wrote errorfile: {self.errfile}', file=sys.stderr)
124
122
 
125
123
  def shutdown(self, exitcode):
126
- # This kills the process tree forcefully to prevent hangs but results in
127
- # leaked semaphore warnings. However, the leaked objects are still cleaned
128
- # up by the resource tracker process of Python's multiprocessing module.
129
- 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:
130
132
  os._exit(exitcode)
131
133
 
132
134
  def close(self):
@@ -134,16 +136,20 @@ class Context:
134
136
  if self.watcher:
135
137
  self.watcher.join()
136
138
 
137
- def add_child(self, worker):
138
- ident = threading.get_ident()
139
- if hasattr(worker, 'ident'):
140
- assert worker.ident != ident
141
- self.children[ident].append(worker)
142
-
143
- def get_children(self, ident=None):
144
- if ident is None:
145
- ident = threading.get_ident()
146
- 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
147
153
 
148
154
  def _watcher(self):
149
155
  while not self.done.wait(self.interval):
@@ -1,4 +1,5 @@
1
1
  import atexit
2
+ import errno
2
3
  import traceback
3
4
 
4
5
  import cloudpickle
@@ -20,24 +21,30 @@ class Process:
20
21
  2. The process terminates its nested child processes when it encounters an
21
22
  error. This prevents lingering subprocesses on error.
22
23
 
23
- 3. When the parent process encounters an error, the subprocess will be killed
24
+ 3. When the parent process encounters an error, its subprocess will be killed
24
25
  via `atexit`, preventing hangs.
25
26
 
26
27
  4. It inherits the context object of its parent process, which provides
27
28
  cloudpickled initializer functions for each nested child process and error
28
29
  file watching for global shutdown on error.
30
+
31
+ 5. Standard Python subprocesses do not reliably return the running() state of
32
+ the process. This class makes it more reliable.
29
33
  """
30
34
 
31
35
  def __init__(self, fn, *args, name=None, start=False):
32
36
  name = name or getattr(fn, '__name__', 'process')
33
37
  fn = cloudpickle.dumps(fn)
34
- context = contextlib.context
35
- options = context.options()
36
- self.process = context.mp.Process(
37
- target=self._wrapper, name=name, args=(options, name, fn, args))
38
- context.add_child(self)
38
+ options = contextlib.context.options()
39
+ self.ready = contextlib.context.mp.Barrier(2)
40
+ self.process = contextlib.context.mp.Process(
41
+ target=self._wrapper, name=name,
42
+ args=(options, self.ready, name, fn, args))
39
43
  self.started = False
44
+ self.killed = False
45
+ self.thepid = None
40
46
  atexit.register(self.kill)
47
+ contextlib.context.add_worker(self)
41
48
  start and self.start()
42
49
 
43
50
  @property
@@ -46,44 +53,53 @@ class Process:
46
53
 
47
54
  @property
48
55
  def pid(self):
49
- return self.process.pid
56
+ return self.thepid
50
57
 
51
58
  @property
52
59
  def running(self):
53
60
  if not self.started:
54
61
  return False
55
- return self.process.is_alive()
62
+ if not self.process.is_alive():
63
+ return False
64
+ return utils.proc_alive(self.pid)
56
65
 
57
66
  @property
58
67
  def exitcode(self):
59
- if not self.started or self.running:
60
- return None
61
- else:
62
- return self.process.exitcode
68
+ exitcode = self.process.exitcode
69
+ if self.killed and exitcode is None:
70
+ return -9
71
+ return exitcode
63
72
 
64
73
  def start(self):
65
74
  assert not self.started
66
75
  self.started = True
67
76
  self.process.start()
68
- assert self.pid is not None
77
+ self.ready.wait()
78
+ self.thepid = self.process.pid
79
+ assert self.thepid is not None
69
80
  return self
70
81
 
71
82
  def join(self, timeout=None):
72
83
  assert self.started
73
- if self.running:
74
- self.process.join(timeout)
84
+ self.process.join(timeout)
75
85
  return self
76
86
 
77
87
  def kill(self, timeout=1):
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):
@@ -92,9 +108,10 @@ class Process:
92
108
  return 'Process(' + ', '.join(attrs) + ')'
93
109
 
94
110
  @staticmethod
95
- def _wrapper(options, name, fn, args):
111
+ def _wrapper(options, ready, name, fn, args):
96
112
  exitcode = 0
97
113
  try:
114
+ ready.wait()
98
115
  contextlib.setup(**options)
99
116
  fn = cloudpickle.loads(fn)
100
117
  exitcode = fn(*args)
@@ -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
@@ -148,4 +148,7 @@ class Server:
148
148
  data = message.encode('utf-8')
149
149
  self.socket.send(addr, reqnum, status, data)
150
150
  if self.errors:
151
+ # Wait until the error is delivered to the client and then raise.
152
+ self.socket.shutdown()
153
+ self.socket.close()
151
154
  raise RuntimeError(message)
@@ -1,5 +1,4 @@
1
1
  import threading
2
- import time
3
2
  import traceback
4
3
 
5
4
  from . import contextlib
@@ -27,8 +26,10 @@ class Thread:
27
26
  name = name or getattr(fn, '__name__', 'thread')
28
27
  self.thread = threading.Thread(
29
28
  target=self._wrapper, args=args, name=name, daemon=True)
30
- contextlib.context.add_child(self)
29
+ self.thread.children = []
31
30
  self.started = False
31
+ self.ready = threading.Barrier(2)
32
+ contextlib.context.add_worker(self)
32
33
  start and self.start()
33
34
 
34
35
  @property
@@ -36,8 +37,8 @@ class Thread:
36
37
  return self.thread.name
37
38
 
38
39
  @property
39
- def ident(self):
40
- return self.thread.ident
40
+ def tid(self):
41
+ return self.thetid
41
42
 
42
43
  @property
43
44
  def running(self):
@@ -53,15 +54,18 @@ class Thread:
53
54
  assert not self.started
54
55
  self.started = True
55
56
  self.thread.start()
57
+ self.ready.wait()
56
58
  return self
57
59
 
58
60
  def join(self, timeout=None):
59
- if self.running:
60
- self.thread.join(timeout)
61
+ self.thread.join(timeout)
61
62
  return self
62
63
 
63
- def kill(self, timeout=3):
64
- utils.kill_threads(self.thread, timeout)
64
+ def kill(self, timeout=1.0):
65
+ assert self.thread != threading.current_thread()
66
+ for child in contextlib.context.children(self.thread):
67
+ child.kill(timeout)
68
+ utils.kill_thread(self.thread, timeout)
65
69
  return self
66
70
 
67
71
  def __repr__(self):
@@ -71,6 +75,7 @@ class Thread:
71
75
 
72
76
  def _wrapper(self, *args):
73
77
  try:
78
+ self.ready.wait()
74
79
  exitcode = self.fn(*args)
75
80
  exitcode = exitcode if isinstance(exitcode, int) else 0
76
81
  self.excode = exitcode
@@ -78,10 +83,8 @@ class Thread:
78
83
  compact = traceback.format_tb(e.__traceback__)
79
84
  compact = '\n'.join([line.split('\n', 1)[0] for line in compact])
80
85
  print(f"Killed thread '{self.name}' at:\n{compact}")
81
- [x.kill(0.1) for x in contextlib.context.get_children()]
86
+ [x.kill(0.1) for x in contextlib.context.children(self.thread)]
82
87
  self.excode = 2
83
88
  except Exception as e:
84
- [x.kill(0.1) for x in contextlib.context.get_children()]
85
89
  contextlib.context.error(e, self.name)
86
- contextlib.context.shutdown(1)
87
- self.excode = 1
90
+ 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,7 +75,21 @@ 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
+
80
+
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
79
93
 
80
94
 
81
95
  def free_port():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portal
3
- Version: 3.1.12
3
+ Version: 3.2.1
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
@@ -15,54 +16,73 @@ class TestProcess:
15
16
  assert worker.exitcode == 42
16
17
 
17
18
  def test_error(self):
19
+
18
20
  def fn():
19
21
  raise KeyError('foo')
22
+
20
23
  worker = portal.Process(fn, start=True)
21
24
  worker.join()
22
25
  assert not worker.running
23
26
  assert worker.exitcode == 1
24
27
 
25
28
  def test_error_with_children(self):
29
+
26
30
  def hang():
27
31
  while True:
28
32
  time.sleep(0.1)
33
+
29
34
  def fn():
30
35
  portal.Process(hang, start=True)
31
36
  portal.Thread(hang, start=True)
32
37
  time.sleep(0.1)
33
38
  raise KeyError('foo')
39
+
34
40
  worker = portal.Process(fn, start=True)
35
41
  worker.join()
36
42
  assert not worker.running
37
43
  assert worker.exitcode == 1
38
44
 
39
- def test_kill(self):
45
+ @pytest.mark.parametrize('repeat', range(5))
46
+ def test_kill_basic(self, repeat):
47
+
40
48
  def fn():
41
49
  while True:
42
50
  time.sleep(0.1)
51
+
43
52
  worker = portal.Process(fn, start=True)
44
53
  worker.kill()
45
54
  assert not worker.running
46
- assert worker.exitcode == -15
55
+ assert abs(worker.exitcode) >= 1
47
56
 
48
57
  @pytest.mark.parametrize('repeat', range(5))
49
58
  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()
59
+ ready = portal.context.mp.Barrier(3)
60
+ queue = portal.context.mp.Queue()
61
+
62
+ def outer(ready, queue):
63
+ portal.Process(inner, ready, queue, start=True)
64
+ queue.put(os.getpid())
65
+ queue.close()
66
+ queue.join_thread()
67
+ ready.wait()
54
68
  while True:
55
69
  time.sleep(0.1)
56
- def inner(ready):
57
- ready.release()
70
+
71
+ def inner(ready, queue):
72
+ queue.put(os.getpid())
73
+ queue.close()
74
+ queue.join_thread()
75
+ ready.wait()
58
76
  while True:
59
77
  time.sleep(0.1)
60
- worker = portal.Process(outer, ready, start=True)
61
- ready.acquire()
62
- ready.acquire()
78
+
79
+ worker = portal.Process(outer, ready, queue, start=True)
80
+ ready.wait()
63
81
  worker.kill()
64
82
  assert not worker.running
65
- assert worker.exitcode == -15
83
+ assert abs(worker.exitcode) >= 1
84
+ assert not portal.proc_alive(queue.get())
85
+ assert not portal.proc_alive(queue.get())
66
86
 
67
87
  @pytest.mark.parametrize('repeat', range(5))
68
88
  def test_kill_with_subthread(self, repeat):
@@ -79,20 +99,25 @@ class TestProcess:
79
99
  ready.wait()
80
100
  worker.kill()
81
101
  assert not worker.running
82
- assert worker.exitcode == -15
102
+ assert abs(worker.exitcode) >= 1
83
103
 
84
104
  def test_initfn(self):
105
+
85
106
  def init():
86
107
  portal.foo = 42
108
+
87
109
  portal.initfn(init)
88
110
  ready = portal.context.mp.Event()
89
111
  assert portal.foo == 42
112
+
90
113
  def outer(ready):
91
114
  assert portal.foo == 42
92
115
  portal.Process(inner, ready, start=True).join()
116
+
93
117
  def inner(ready):
94
118
  assert portal.foo == 42
95
119
  ready.set()
120
+
96
121
  portal.Process(outer, ready, start=True).join()
97
122
  ready.wait()
98
123
  assert ready.is_set()
@@ -91,6 +91,27 @@ class TestServer:
91
91
  def test_server_errors(self, Server):
92
92
  port = portal.free_port()
93
93
 
94
+ server = Server(port, errors=False)
95
+ def fn(x):
96
+ if x == 2:
97
+ raise ValueError(x)
98
+ return x
99
+ server.bind('fn', fn)
100
+ server.start(block=False)
101
+
102
+ client = portal.Client('localhost', port)
103
+ assert client.fn(1).result() == 1
104
+ with pytest.raises(RuntimeError):
105
+ client.fn(2).result()
106
+ assert client.fn(3).result() == 3
107
+
108
+ client.close()
109
+ server.close()
110
+
111
+ @pytest.mark.parametrize('Server', SERVERS)
112
+ def test_server_errors_raise(self, Server):
113
+ port = portal.free_port()
114
+
94
115
  def server(port):
95
116
  server = Server(port, errors=True)
96
117
  def fn(x):
@@ -108,8 +129,8 @@ class TestServer:
108
129
  client = portal.Client('localhost', port)
109
130
  assert client.fn(1).result() == 1
110
131
  assert server.running
111
- with pytest.raises(RuntimeError):
112
- client.fn(2).result()
132
+ with pytest.raises((RuntimeError, TimeoutError)):
133
+ client.fn(2).result(timeout=3)
113
134
 
114
135
  client.close()
115
136
  server.join()
@@ -151,16 +172,19 @@ class TestServer:
151
172
  lock = threading.Lock()
152
173
  work_calls = [0]
153
174
  done_calls = [0]
175
+
154
176
  def workfn(x):
155
177
  with lock:
156
178
  work_calls[0] += 1
157
179
  print(work_calls[0], done_calls[0])
158
180
  assert work_calls[0] <= done_calls[0] + workers + 1
159
181
  return x, x
182
+
160
183
  def postfn(x):
161
184
  with lock:
162
185
  done_calls[0] += 1
163
186
  time.sleep(0.01)
187
+
164
188
  server = Server(port, workers=workers)
165
189
  server.bind('fn', workfn, postfn)
166
190
  server.start(block=False)
@@ -173,11 +197,14 @@ class TestServer:
173
197
  @pytest.mark.parametrize('repeat', range(3))
174
198
  @pytest.mark.parametrize('Server', SERVERS)
175
199
  def test_shared_pool(self, repeat, Server):
200
+
176
201
  def slow(x):
177
202
  time.sleep(0.2)
178
203
  return x
204
+
179
205
  def fast(x):
180
206
  return x
207
+
181
208
  port = portal.free_port()
182
209
  server = Server(port, workers=1)
183
210
  server.bind('slow', slow)
@@ -197,11 +224,14 @@ class TestServer:
197
224
  @pytest.mark.parametrize('repeat', range(3))
198
225
  @pytest.mark.parametrize('Server', SERVERS)
199
226
  def test_separate_pools(self, repeat, Server):
227
+
200
228
  def slow(x):
201
229
  time.sleep(0.1)
202
230
  return x
231
+
203
232
  def fast(x):
204
233
  return x
234
+
205
235
  port = portal.free_port()
206
236
  server = Server(port)
207
237
  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 abs(proc[0].exitcode) >= 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