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.
- {portal-3.1.12/portal.egg-info → portal-3.2.1}/PKG-INFO +1 -1
- {portal-3.1.12 → portal-3.2.1}/portal/__init__.py +2 -3
- {portal-3.1.12 → portal-3.2.1}/portal/batching.py +7 -7
- {portal-3.1.12 → portal-3.2.1}/portal/client.py +4 -2
- {portal-3.1.12 → portal-3.2.1}/portal/client_socket.py +0 -2
- {portal-3.1.12 → portal-3.2.1}/portal/contextlib.py +22 -16
- {portal-3.1.12 → portal-3.2.1}/portal/process.py +40 -23
- {portal-3.1.12 → portal-3.2.1}/portal/server.py +4 -1
- {portal-3.1.12 → portal-3.2.1}/portal/thread.py +15 -12
- {portal-3.1.12 → portal-3.2.1}/portal/utils.py +19 -5
- {portal-3.1.12 → portal-3.2.1/portal.egg-info}/PKG-INFO +1 -1
- {portal-3.1.12 → portal-3.2.1}/tests/test_client.py +2 -6
- {portal-3.1.12 → portal-3.2.1}/tests/test_errfile.py +46 -19
- {portal-3.1.12 → portal-3.2.1}/tests/test_process.py +38 -13
- {portal-3.1.12 → portal-3.2.1}/tests/test_server.py +32 -2
- {portal-3.1.12 → portal-3.2.1}/tests/test_thread.py +1 -1
- {portal-3.1.12 → portal-3.2.1}/LICENSE +0 -0
- {portal-3.1.12 → portal-3.2.1}/MANIFEST.in +0 -0
- {portal-3.1.12 → portal-3.2.1}/README.md +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal/buffers.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal/packlib.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal/poollib.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal/server_socket.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal/sharray.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal.egg-info/SOURCES.txt +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal.egg-info/dependency_links.txt +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal.egg-info/requires.txt +0 -0
- {portal-3.1.12 → portal-3.2.1}/portal.egg-info/top_level.txt +0 -0
- {portal-3.1.12 → portal-3.2.1}/pyproject.toml +0 -0
- {portal-3.1.12 → portal-3.2.1}/requirements.txt +0 -0
- {portal-3.1.12 → portal-3.2.1}/setup.cfg +0 -0
- {portal-3.1.12 → portal-3.2.1}/setup.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/tests/test_batching.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/tests/test_pack.py +0 -0
- {portal-3.1.12 → portal-3.2.1}/tests/test_socket.py +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
__version__ = '3.1
|
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
|
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
|
57
|
-
self.batcher.join(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
|
-
|
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
|
-
|
127
|
-
|
128
|
-
#
|
129
|
-
|
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
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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,
|
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
|
-
|
35
|
-
|
36
|
-
self.process = context.mp.Process(
|
37
|
-
target=self._wrapper, name=name,
|
38
|
-
|
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.
|
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
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
80
|
-
tree = [proc] + list(proc.children(recursive=True))
|
90
|
+
children = list(psutil.Process(self.pid).children(recursive=True))
|
81
91
|
except psutil.NoSuchProcess:
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
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
|
40
|
-
return self.
|
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
|
-
|
60
|
-
self.thread.join(timeout)
|
61
|
+
self.thread.join(timeout)
|
61
62
|
return self
|
62
63
|
|
63
|
-
def kill(self, timeout=
|
64
|
-
|
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.
|
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
|
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
|
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.
|
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():
|
@@ -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=
|
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
|
-
|
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.
|
62
|
+
ready = portal.context.mp.Barrier(7)
|
63
|
+
queue = portal.context.mp.Queue()
|
60
64
|
|
61
|
-
def
|
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
|
66
|
-
portal.
|
67
|
-
portal.
|
68
|
-
portal.
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
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.
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
ready.
|
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
|
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
|
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)
|
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
|