mrok 0.2.1__tar.gz → 0.2.2__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.
- {mrok-0.2.1 → mrok-0.2.2}/PKG-INFO +1 -1
- mrok-0.2.2/mrok/http/master.py +132 -0
- {mrok-0.2.1 → mrok-0.2.2}/pyproject.toml +1 -1
- {mrok-0.2.1 → mrok-0.2.2}/tests/http/test_master.py +61 -2
- mrok-0.2.1/mrok/http/master.py +0 -90
- {mrok-0.2.1 → mrok-0.2.2}/.github/actions/setup-python-env/action.yml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.github/workflows/assets/turing_team_pr_bot.png +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.github/workflows/notify-pr-closed.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.github/workflows/notify-pr-reviewed.yml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.github/workflows/pr-build-merge.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.github/workflows/release.yml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.gitignore +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.pre-commit-config.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/.python-version +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/LICENSE.txt +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/README.md +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/dev.Dockerfile +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/docker-compose.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/entrypoint.sh +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/agent/sidecar/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/agent/sidecar/app.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/agent/sidecar/main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/agent/ziticorn.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/list/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/list/extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/list/instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/register/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/register/extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/register/instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/unregister/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/unregister/extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/unregister/instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/admin/utils.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/agent/run/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/agent/run/asgi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/agent/run/sidecar.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/controller/openapi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/commands/controller/run.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/cli/rich.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/conf.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/app.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/auth.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/dependencies/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/dependencies/conf.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/dependencies/ziti.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/openapi/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/openapi/examples.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/openapi/utils.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/pagination.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/routes/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/routes/extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/routes/instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/controller/schemas.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/errors.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/http/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/http/config.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/http/forwarder.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/http/lifespan.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/http/protocol.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/http/server.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/logging.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/api.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/constants.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/errors.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/identities.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/pki.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/mrok/ziti/services.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/prod.Dockerfile +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/scripts/ziti.sh +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/settings.yaml +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/sonar-project.properties +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/agent/sidecar/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/agent/sidecar/test_app.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/agent/sidecar/test_main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/agent/test_ziticorn.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/admin/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/admin/test_bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/admin/test_list.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/admin/test_register.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/admin/test_unregister.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/admin/test_utils.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/agent/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/agent/test_run.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/controller/test_openapi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/controller/test_run.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/cli/test_main.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/conftest.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/controller/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/controller/test_auth.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/controller/test_extensions.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/controller/test_instances.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/controller/test_openapi.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/http/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/http/test_config.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/http/test_forwarder.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/http/test_lifespan.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/http/test_protocol.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/http/test_server.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/ziti/__init__.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/ziti/test_api.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/ziti/test_bootstrap.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/ziti/test_identities.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/ziti/test_pki.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/tests/ziti/test_services.py +0 -0
- {mrok-0.2.1 → mrok-0.2.2}/uv.lock +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from watchfiles import watch
|
|
10
|
+
from watchfiles.filters import PythonFilter
|
|
11
|
+
from watchfiles.run import CombinedProcess, start_process
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("mrok.agent")
|
|
14
|
+
|
|
15
|
+
MONITOR_THREAD_JOIN_TIMEOUT = 5
|
|
16
|
+
MONITOR_THREAD_CHECK_DELAY = 1
|
|
17
|
+
MONITOR_THREAD_ERROR_DELAY = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_path(path):
|
|
21
|
+
try:
|
|
22
|
+
return f'"{path.relative_to(Path.cwd())}"'
|
|
23
|
+
except ValueError:
|
|
24
|
+
return f'"{path}"'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Master:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
start_fn: Callable,
|
|
31
|
+
workers: int,
|
|
32
|
+
reload: bool,
|
|
33
|
+
):
|
|
34
|
+
self.start_fn = start_fn
|
|
35
|
+
self.workers = workers
|
|
36
|
+
self.reload = reload
|
|
37
|
+
self.worker_processes: dict[int, CombinedProcess] = {}
|
|
38
|
+
self.stop_event = threading.Event()
|
|
39
|
+
self.watch_filter = PythonFilter(ignore_paths=None)
|
|
40
|
+
self.watcher = watch(
|
|
41
|
+
Path.cwd(),
|
|
42
|
+
watch_filter=self.watch_filter,
|
|
43
|
+
stop_event=self.stop_event,
|
|
44
|
+
yield_on_timeout=True,
|
|
45
|
+
)
|
|
46
|
+
self.setup_signals_handler()
|
|
47
|
+
self.monitor_thread = None
|
|
48
|
+
|
|
49
|
+
def setup_signals_handler(self):
|
|
50
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
51
|
+
signal.signal(sig, self.handle_signal)
|
|
52
|
+
|
|
53
|
+
def handle_signal(self, *args, **kwargs):
|
|
54
|
+
self.stop_event.set()
|
|
55
|
+
|
|
56
|
+
def start_worker(self, worker_id: int):
|
|
57
|
+
"""Start a single worker process"""
|
|
58
|
+
p = start_process(
|
|
59
|
+
self.start_fn,
|
|
60
|
+
"function",
|
|
61
|
+
(),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
logger.info(f"Worker {worker_id} [{p.pid}] started")
|
|
65
|
+
return p
|
|
66
|
+
|
|
67
|
+
def start(self):
|
|
68
|
+
for i in range(self.workers):
|
|
69
|
+
p = self.start_worker(i)
|
|
70
|
+
self.worker_processes[i] = p
|
|
71
|
+
|
|
72
|
+
def stop(self):
|
|
73
|
+
for process in self.worker_processes.values():
|
|
74
|
+
process.stop(sigint_timeout=5, sigkill_timeout=1)
|
|
75
|
+
self.worker_processes.clear()
|
|
76
|
+
|
|
77
|
+
def restart(self):
|
|
78
|
+
self.stop()
|
|
79
|
+
self.start()
|
|
80
|
+
|
|
81
|
+
def monitor_workers(self):
|
|
82
|
+
while not self.stop_event.is_set():
|
|
83
|
+
try:
|
|
84
|
+
for worker_id, process in self.worker_processes.items():
|
|
85
|
+
if not process.is_alive():
|
|
86
|
+
logger.warning(f"Worker {worker_id} [{process.pid}] died unexpectedly")
|
|
87
|
+
process.stop(sigint_timeout=1, sigkill_timeout=1)
|
|
88
|
+
new_process = self.start_worker(worker_id)
|
|
89
|
+
self.worker_processes[worker_id] = new_process
|
|
90
|
+
logger.info(
|
|
91
|
+
f"Restarted worker {worker_id} [{process.pid}] -> [{new_process.pid}]"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
time.sleep(MONITOR_THREAD_CHECK_DELAY)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error in worker monitoring: {e}")
|
|
98
|
+
time.sleep(MONITOR_THREAD_ERROR_DELAY)
|
|
99
|
+
|
|
100
|
+
def __iter__(self):
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __next__(self):
|
|
104
|
+
changes = next(self.watcher)
|
|
105
|
+
if changes:
|
|
106
|
+
return list({Path(change[1]) for change in changes})
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def run(self):
|
|
110
|
+
self.start()
|
|
111
|
+
logger.info(f"Master process started: {os.getpid()}")
|
|
112
|
+
|
|
113
|
+
# Start worker monitoring thread
|
|
114
|
+
self.monitor_thread = threading.Thread(target=self.monitor_workers, daemon=True)
|
|
115
|
+
self.monitor_thread.start()
|
|
116
|
+
logger.debug("Worker monitoring thread started")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
if self.reload:
|
|
120
|
+
for files_changed in self:
|
|
121
|
+
if files_changed:
|
|
122
|
+
logger.warning(
|
|
123
|
+
f"{', '.join(map(print_path, files_changed))} changed, reloading...",
|
|
124
|
+
)
|
|
125
|
+
self.restart()
|
|
126
|
+
else:
|
|
127
|
+
self.stop_event.wait()
|
|
128
|
+
finally:
|
|
129
|
+
if self.monitor_thread and self.monitor_thread.is_alive(): # pragma: no cover
|
|
130
|
+
logger.debug("Wait for monitor worker to exit")
|
|
131
|
+
self.monitor_thread.join(timeout=MONITOR_THREAD_JOIN_TIMEOUT)
|
|
132
|
+
self.stop()
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import signal
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
2
4
|
from collections.abc import Generator
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
@@ -42,7 +44,7 @@ def test_start(mocker: MockerFixture):
|
|
|
42
44
|
start_fn = mocker.MagicMock()
|
|
43
45
|
master = Master(start_fn, 3, False)
|
|
44
46
|
master.start()
|
|
45
|
-
assert master.worker_processes == processes
|
|
47
|
+
assert master.worker_processes == {0: processes[0], 1: processes[1], 2: processes[2]}
|
|
46
48
|
for i in range(3):
|
|
47
49
|
assert mocked_start_process.mock_calls[i].args == (start_fn, "function", (), None)
|
|
48
50
|
|
|
@@ -51,7 +53,7 @@ def test_stop(mocker: MockerFixture):
|
|
|
51
53
|
master = Master(mocker.MagicMock(), 3, False)
|
|
52
54
|
p1 = mocker.MagicMock()
|
|
53
55
|
p2 = mocker.MagicMock()
|
|
54
|
-
master.worker_processes =
|
|
56
|
+
master.worker_processes = {0: p1, 2: p2}
|
|
55
57
|
master.stop()
|
|
56
58
|
p1.stop.assert_called_once_with(sigint_timeout=5, sigkill_timeout=1)
|
|
57
59
|
p2.stop.assert_called_once_with(sigint_timeout=5, sigkill_timeout=1)
|
|
@@ -84,17 +86,20 @@ def test_next(mocker: MockerFixture):
|
|
|
84
86
|
|
|
85
87
|
def test_run(mocker: MockerFixture):
|
|
86
88
|
mocked_start = mocker.patch.object(Master, "start")
|
|
89
|
+
mocked_monitor_fn = mocker.patch.object(Master, "monitor_workers")
|
|
87
90
|
master = Master(mocker.MagicMock(), 3, False)
|
|
88
91
|
mocked_stop_event = mocker.MagicMock()
|
|
89
92
|
master.stop_event = mocked_stop_event
|
|
90
93
|
master.run()
|
|
91
94
|
mocked_start.assert_called_once()
|
|
92
95
|
mocked_stop_event.wait.assert_called_once()
|
|
96
|
+
mocked_monitor_fn.assert_called_once()
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
def test_run_with_reload(mocker: MockerFixture):
|
|
96
100
|
mocker.patch.object(Master, "start")
|
|
97
101
|
mocked_restart = mocker.patch.object(Master, "restart")
|
|
102
|
+
mocked_monitor_fn = mocker.patch.object(Master, "monitor_workers")
|
|
98
103
|
|
|
99
104
|
def watcher() -> Generator:
|
|
100
105
|
yield {(Change.modified, "/file1.py")}
|
|
@@ -105,3 +110,57 @@ def test_run_with_reload(mocker: MockerFixture):
|
|
|
105
110
|
master.run()
|
|
106
111
|
|
|
107
112
|
mocked_restart.assert_called_once()
|
|
113
|
+
mocked_monitor_fn.assert_called_once()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_monitor_workers_restarts_dead_process(mocker: MockerFixture):
|
|
117
|
+
mocker.patch("mrok.http.master.MONITOR_THREAD_CHECK_DELAY", 0.1)
|
|
118
|
+
|
|
119
|
+
mock_start_process = mocker.patch("mrok.http.master.start_process")
|
|
120
|
+
master = Master(mocker.MagicMock(), 3, False)
|
|
121
|
+
|
|
122
|
+
dead_process = mocker.Mock()
|
|
123
|
+
dead_process.is_alive.return_value = False
|
|
124
|
+
dead_process.pid = 12345
|
|
125
|
+
|
|
126
|
+
alive_process = mocker.Mock()
|
|
127
|
+
alive_process.is_alive.return_value = True
|
|
128
|
+
alive_process.pid = 12346
|
|
129
|
+
|
|
130
|
+
new_process = mocker.Mock()
|
|
131
|
+
new_process.pid = 12347
|
|
132
|
+
|
|
133
|
+
master.worker_processes = {0: dead_process, 1: alive_process}
|
|
134
|
+
|
|
135
|
+
mock_start_process.return_value = new_process
|
|
136
|
+
|
|
137
|
+
monitor_thread = threading.Thread(target=master.monitor_workers)
|
|
138
|
+
monitor_thread.start()
|
|
139
|
+
time.sleep(0.1)
|
|
140
|
+
master.stop_event.set()
|
|
141
|
+
monitor_thread.join()
|
|
142
|
+
|
|
143
|
+
dead_process.stop.assert_called_once_with(sigint_timeout=1, sigkill_timeout=1)
|
|
144
|
+
mock_start_process.assert_called_once_with(master.start_fn, "function", (), None)
|
|
145
|
+
assert master.worker_processes[0] == new_process
|
|
146
|
+
assert master.worker_processes[1] == alive_process
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_monitor_workers_handles_is_alive_exception(mocker: MockerFixture):
|
|
150
|
+
mocker.patch("mrok.http.master.MONITOR_THREAD_ERROR_DELAY", 0.1)
|
|
151
|
+
mock_logger = mocker.patch("mrok.http.master.logger.error")
|
|
152
|
+
master = Master(mocker.MagicMock(), 3, False)
|
|
153
|
+
|
|
154
|
+
problematic_process = mocker.Mock()
|
|
155
|
+
problematic_process.is_alive.side_effect = Exception("Test exception")
|
|
156
|
+
|
|
157
|
+
master.worker_processes = {0: problematic_process}
|
|
158
|
+
|
|
159
|
+
monitor_thread = threading.Thread(target=master.monitor_workers)
|
|
160
|
+
monitor_thread.start()
|
|
161
|
+
|
|
162
|
+
time.sleep(0.1)
|
|
163
|
+
master.stop_event.set()
|
|
164
|
+
monitor_thread.join()
|
|
165
|
+
|
|
166
|
+
assert mock_logger.mock_calls[0].args[0] == "Error in worker monitoring: Test exception"
|
mrok-0.2.1/mrok/http/master.py
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
import signal
|
|
4
|
-
import threading
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from watchfiles import watch
|
|
9
|
-
from watchfiles.filters import PythonFilter
|
|
10
|
-
from watchfiles.run import CombinedProcess, start_process
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger("mrok.agent")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def print_path(path):
|
|
16
|
-
try:
|
|
17
|
-
return f'"{path.relative_to(Path.cwd())}"'
|
|
18
|
-
except ValueError:
|
|
19
|
-
return f'"{path}"'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Master:
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
start_fn: Callable,
|
|
26
|
-
workers: int,
|
|
27
|
-
reload: bool,
|
|
28
|
-
):
|
|
29
|
-
self.start_fn = start_fn
|
|
30
|
-
self.workers = workers
|
|
31
|
-
self.reload = reload
|
|
32
|
-
self.worker_processes: list[CombinedProcess] = []
|
|
33
|
-
self.stop_event = threading.Event()
|
|
34
|
-
self.watch_filter = PythonFilter(ignore_paths=None)
|
|
35
|
-
self.watcher = watch(
|
|
36
|
-
Path.cwd(),
|
|
37
|
-
watch_filter=self.watch_filter,
|
|
38
|
-
stop_event=self.stop_event,
|
|
39
|
-
yield_on_timeout=True,
|
|
40
|
-
)
|
|
41
|
-
self.setup_signals_handler()
|
|
42
|
-
|
|
43
|
-
def setup_signals_handler(self):
|
|
44
|
-
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
45
|
-
signal.signal(sig, self.handle_signal)
|
|
46
|
-
|
|
47
|
-
def handle_signal(self, *args, **kwargs):
|
|
48
|
-
self.stop_event.set()
|
|
49
|
-
|
|
50
|
-
def start(self):
|
|
51
|
-
for _ in range(self.workers):
|
|
52
|
-
p = start_process(
|
|
53
|
-
self.start_fn,
|
|
54
|
-
"function",
|
|
55
|
-
(),
|
|
56
|
-
None,
|
|
57
|
-
)
|
|
58
|
-
logger.info(f"Worker [{p.pid}] started")
|
|
59
|
-
self.worker_processes.append(p)
|
|
60
|
-
|
|
61
|
-
def stop(self):
|
|
62
|
-
for process in self.worker_processes:
|
|
63
|
-
process.stop(sigint_timeout=5, sigkill_timeout=1)
|
|
64
|
-
self.worker_processes = []
|
|
65
|
-
|
|
66
|
-
def restart(self):
|
|
67
|
-
self.stop()
|
|
68
|
-
self.start()
|
|
69
|
-
|
|
70
|
-
def __iter__(self):
|
|
71
|
-
return self
|
|
72
|
-
|
|
73
|
-
def __next__(self):
|
|
74
|
-
changes = next(self.watcher)
|
|
75
|
-
if changes:
|
|
76
|
-
return list({Path(change[1]) for change in changes})
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
def run(self):
|
|
80
|
-
self.start()
|
|
81
|
-
logger.info(f"Master process started: {os.getpid()}")
|
|
82
|
-
if self.reload:
|
|
83
|
-
for files_changed in self:
|
|
84
|
-
if files_changed:
|
|
85
|
-
logger.warning(
|
|
86
|
-
f"{', '.join(map(print_path, files_changed))} changed, reloading...",
|
|
87
|
-
)
|
|
88
|
-
self.restart()
|
|
89
|
-
else:
|
|
90
|
-
self.stop_event.wait()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|