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.
- {portal-3.1.11/portal.egg-info → portal-3.2.0}/PKG-INFO +1 -1
- {portal-3.1.11 → portal-3.2.0}/portal/__init__.py +2 -3
- {portal-3.1.11 → portal-3.2.0}/portal/batching.py +7 -7
- {portal-3.1.11 → portal-3.2.0}/portal/client.py +4 -2
- {portal-3.1.11 → portal-3.2.0}/portal/client_socket.py +0 -2
- {portal-3.1.11 → portal-3.2.0}/portal/contextlib.py +33 -16
- {portal-3.1.11 → portal-3.2.0}/portal/process.py +37 -21
- {portal-3.1.11 → portal-3.2.0}/portal/server.py +1 -1
- {portal-3.1.11 → portal-3.2.0}/portal/thread.py +16 -12
- {portal-3.1.11 → portal-3.2.0}/portal/utils.py +26 -12
- {portal-3.1.11 → portal-3.2.0/portal.egg-info}/PKG-INFO +1 -1
- {portal-3.1.11 → portal-3.2.0}/tests/test_client.py +2 -6
- {portal-3.1.11 → portal-3.2.0}/tests/test_errfile.py +46 -19
- {portal-3.1.11 → portal-3.2.0}/tests/test_process.py +20 -12
- {portal-3.1.11 → portal-3.2.0}/tests/test_server.py +9 -0
- {portal-3.1.11 → portal-3.2.0}/tests/test_thread.py +1 -1
- {portal-3.1.11 → portal-3.2.0}/LICENSE +0 -0
- {portal-3.1.11 → portal-3.2.0}/MANIFEST.in +0 -0
- {portal-3.1.11 → portal-3.2.0}/README.md +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal/buffers.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal/packlib.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal/poollib.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal/server_socket.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal/sharray.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal.egg-info/SOURCES.txt +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal.egg-info/dependency_links.txt +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal.egg-info/requires.txt +0 -0
- {portal-3.1.11 → portal-3.2.0}/portal.egg-info/top_level.txt +0 -0
- {portal-3.1.11 → portal-3.2.0}/pyproject.toml +0 -0
- {portal-3.1.11 → portal-3.2.0}/requirements.txt +0 -0
- {portal-3.1.11 → portal-3.2.0}/setup.cfg +0 -0
- {portal-3.1.11 → portal-3.2.0}/setup.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/tests/test_batching.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/tests/test_pack.py +0 -0
- {portal-3.1.11 → portal-3.2.0}/tests/test_socket.py +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
__version__ = '3.
|
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
|
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
|
|
@@ -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
|
-
|
126
|
-
|
127
|
-
#
|
128
|
-
|
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
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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,
|
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
|
-
|
35
|
-
|
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.
|
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
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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):
|
@@ -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
|
-
|
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
|
40
|
-
return self.
|
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
|
-
|
60
|
-
self.thread.join(timeout)
|
62
|
+
self.thread.join(timeout)
|
61
63
|
return self
|
62
64
|
|
63
|
-
def kill(self, timeout=
|
64
|
-
|
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.
|
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
|
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,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.
|
78
|
+
if proc_alive(p.pid) else None), procs)
|
79
79
|
|
80
80
|
|
81
|
-
def
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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):
|
@@ -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
|
@@ -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
|
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.
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
ready.
|
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
|
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
|
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)
|
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
|