Fix hanging bug by adding manager

This commit is contained in:
Andras Schmelczer 2022-07-03 16:01:52 +02:00
parent a84d1162ec
commit e61b280d5a
No known key found for this signature in database
GPG key ID: 0EA1BC97D0AB076E
10 changed files with 40 additions and 30 deletions

View file

@ -6,6 +6,6 @@ from .get_sentences import get_sentences
from .language import english_name_of_language, is_english, predict_language from .language import english_name_of_language, is_english, predict_language
from .logger import get_logger from .logger import get_logger
from .match_names import match_names from .match_names import match_names
from .parallel_map import parallel_map, threaded_parallel_map from .parallel_map import WorkerException, parallel_map, threaded_parallel_map
from .unchunk import unchunk from .unchunk import unchunk
from .unique import unique from .unique import unique

View file

@ -1,2 +1,3 @@
from .parallel_map import parallel_map from .parallel_map import parallel_map
from .threaded_parallel_map import threaded_parallel_map from .threaded_parallel_map import threaded_parallel_map
from .worker_exception import WorkerException

View file

@ -5,6 +5,7 @@ from typing import Dict, Iterable, TypeVar, Union
from ..chunk import chunk from ..chunk import chunk
from ..logger import get_logger from ..logger import get_logger
from .worker_exception import WorkerException
logger = get_logger("parallel_map") logger = get_logger("parallel_map")
@ -36,7 +37,7 @@ def manage_communication(
is_iteration_over = True is_iteration_over = True
except Exception as e: except Exception as e:
if not ignore_exceptions: if not ignore_exceptions:
raise e raise
else: else:
logger.error( logger.error(
f"Exception {e} encountered in input, traceback:\n{traceback.format_exc()}" f"Exception {e} encountered in input, traceback:\n{traceback.format_exc()}"
@ -49,7 +50,7 @@ def manage_communication(
if exception is not None: if exception is not None:
e, tb = exception e, tb = exception
if not ignore_exceptions: if not ignore_exceptions:
raise e raise WorkerException from e
else: else:
logger.error( logger.error(
f"Exception {e} encountered in worker, traceback:\n{tb}" f"Exception {e} encountered in worker, traceback:\n{tb}"

View file

@ -2,6 +2,7 @@ import traceback
from typing import Callable, Iterable, TypeVar from typing import Callable, Iterable, TypeVar
from ..logger import get_logger from ..logger import get_logger
from .worker_exception import WorkerException
logger = get_logger("parallel_map") logger = get_logger("parallel_map")
@ -21,15 +22,15 @@ def manage_serial(
yield function(v) yield function(v)
except Exception as e: except Exception as e:
if not ignore_exceptions: if not ignore_exceptions:
raise e raise WorkerException from e
else: else:
logger.error( logger.error(
f"Exception {e} encountered in input, traceback:\n{traceback.format_exc()}" f"Exception {e} encountered in worker, traceback:\n{traceback.format_exc()}"
) )
except Exception as e: except Exception as e:
if not ignore_exceptions: if not ignore_exceptions:
raise e raise
else: else:
logger.error( logger.error(
f"Exception {e} encountered in worker, traceback:\n{traceback.format_exc()}" f"Exception {e} encountered in input, traceback:\n{traceback.format_exc()}"
) )

View file

@ -2,7 +2,6 @@ import multiprocessing as mp
import queue import queue
import threading import threading
import traceback import traceback
from time import sleep
from typing import Callable, Union from typing import Callable, Union
import dill import dill
@ -19,7 +18,7 @@ def mapper_function(
map_function = dill.loads(map_function) map_function = dill.loads(map_function)
last_chunk = None last_chunk = None
while not should_stop.is_set(): while not should_stop.wait(0.1):
if last_chunk is None: if last_chunk is None:
try: try:
input_chunk = input_queue.get_nowait() input_chunk = input_queue.get_nowait()
@ -32,16 +31,13 @@ def mapper_function(
exception = e, traceback.format_exc() exception = e, traceback.format_exc()
last_chunk.append((i, result, exception)) last_chunk.append((i, result, exception))
except queue.Empty: except queue.Empty:
sleep( pass
0.1
) # input_queue.get(0.1) would hang in some cases with multiprocessing if last_chunk is not None:
else:
try: try:
output_queue.put_nowait(last_chunk) output_queue.put_nowait(last_chunk)
last_chunk = None last_chunk = None
except queue.Full: except queue.Full:
sleep( pass
0.1
) # output_queue.put(0.1) would hang in some cases with multiprocessing
except (KeyboardInterrupt, BrokenPipeError): except (KeyboardInterrupt, BrokenPipeError):
pass pass

View file

@ -7,6 +7,7 @@ from .get_config import get_config
from .manage_communication import manage_communication from .manage_communication import manage_communication
from .manage_serial import manage_serial from .manage_serial import manage_serial
from .mapper_function import mapper_function from .mapper_function import mapper_function
from .worker_exception import WorkerException
T = TypeVar("T") T = TypeVar("T")
V = TypeVar("V") V = TypeVar("V")
@ -87,13 +88,17 @@ def parallel_map(
ignore_exceptions=ignore_exceptions, ignore_exceptions=ignore_exceptions,
) )
ctx = mp.get_context("spawn") ctx = (
mp.get_context("fork")
if "fork" in mp.get_all_start_methods()
else mp.get_context("spawn")
)
ctx.freeze_support() ctx.freeze_support()
manager = ctx.Manager()
input_queue = manager.Queue(config.concurrency * 2)
output_queue = manager.Queue(config.concurrency * 2)
input_queue = ctx.Queue(config.concurrency * 2)
output_queue = ctx.Queue(config.concurrency * 2)
should_stop = ctx.Event() should_stop = ctx.Event()
serialized_map_function = dill.dumps(function, byref=True, recurse=True) serialized_map_function = dill.dumps(function, byref=True, recurse=True)
processes = [ processes = [
@ -124,13 +129,17 @@ def parallel_map(
ignore_exceptions=ignore_exceptions, ignore_exceptions=ignore_exceptions,
) )
should_stop.set() should_stop.set()
except: except WorkerException:
should_stop.set()
raise
except Exception:
for p in processes: for p in processes:
p.terminate() p.terminate()
p.kill()
raise raise
finally: finally:
for p in processes: for p in processes:
p.join() # terminated processes have to be joined else they remain zombies p.join() # terminated processes have to be joined else they remain zombies
p.close() p.close()
input_queue.close()
output_queue.close() manager.shutdown()

View file

@ -118,4 +118,4 @@ def threaded_parallel_map(
) )
should_stop.set() should_stop.set()
for t in threads: for t in threads:
t.join() t.join(1)

View file

@ -0,0 +1,2 @@
class WorkerException(Exception):
pass

View file

@ -2,7 +2,7 @@ import unittest
import pytest import pytest
from src.great_ai.utilities import parallel_map from src.great_ai.utilities import WorkerException, parallel_map
COUNT = int(1e5) + 3 COUNT = int(1e5) + 3
@ -91,10 +91,10 @@ class TestParallelMap(unittest.TestCase):
def oh_no(_): def oh_no(_):
raise ValueError("hi") raise ValueError("hi")
with pytest.raises(ValueError): with pytest.raises(WorkerException):
list(parallel_map(oh_no, range(COUNT), concurrency=2)) list(parallel_map(oh_no, range(COUNT), concurrency=2))
with pytest.raises(ValueError): with pytest.raises(WorkerException):
list(parallel_map(oh_no, range(COUNT), concurrency=1)) list(parallel_map(oh_no, range(COUNT), concurrency=1))
def test_ignore_worker_process_exception(self) -> None: def test_ignore_worker_process_exception(self) -> None:

View file

@ -2,7 +2,7 @@ import unittest
import pytest import pytest
from src.great_ai.utilities import threaded_parallel_map from src.great_ai.utilities import WorkerException, threaded_parallel_map
COUNT = int(1e5) + 3 COUNT = int(1e5) + 3
@ -91,10 +91,10 @@ class TestParallelMap(unittest.TestCase):
def oh_no(_): def oh_no(_):
raise ValueError("hi") raise ValueError("hi")
with pytest.raises(ValueError): with pytest.raises(WorkerException):
list(threaded_parallel_map(oh_no, range(COUNT), concurrency=2)) list(threaded_parallel_map(oh_no, range(COUNT), concurrency=2))
with pytest.raises(ValueError): with pytest.raises(WorkerException):
list(threaded_parallel_map(oh_no, range(COUNT), concurrency=1)) list(threaded_parallel_map(oh_no, range(COUNT), concurrency=1))
def test_ignore_worker_worker_exception(self) -> None: def test_ignore_worker_worker_exception(self) -> None: