mrok 0.2.0__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.
Files changed (121) hide show
  1. {mrok-0.2.0 → mrok-0.2.2}/PKG-INFO +2 -1
  2. mrok-0.2.2/mrok/http/master.py +132 -0
  3. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/pki.py +10 -5
  4. {mrok-0.2.0 → mrok-0.2.2}/pyproject.toml +2 -1
  5. {mrok-0.2.0 → mrok-0.2.2}/tests/http/test_master.py +61 -2
  6. {mrok-0.2.0 → mrok-0.2.2}/uv.lock +11 -0
  7. mrok-0.2.0/mrok/http/master.py +0 -90
  8. {mrok-0.2.0 → mrok-0.2.2}/.github/actions/setup-python-env/action.yml +0 -0
  9. {mrok-0.2.0 → mrok-0.2.2}/.github/workflows/assets/turing_team_pr_bot.png +0 -0
  10. {mrok-0.2.0 → mrok-0.2.2}/.github/workflows/notify-pr-closed.yaml +0 -0
  11. {mrok-0.2.0 → mrok-0.2.2}/.github/workflows/notify-pr-reviewed.yml +0 -0
  12. {mrok-0.2.0 → mrok-0.2.2}/.github/workflows/pr-build-merge.yaml +0 -0
  13. {mrok-0.2.0 → mrok-0.2.2}/.github/workflows/release.yml +0 -0
  14. {mrok-0.2.0 → mrok-0.2.2}/.gitignore +0 -0
  15. {mrok-0.2.0 → mrok-0.2.2}/.pre-commit-config.yaml +0 -0
  16. {mrok-0.2.0 → mrok-0.2.2}/.python-version +0 -0
  17. {mrok-0.2.0 → mrok-0.2.2}/LICENSE.txt +0 -0
  18. {mrok-0.2.0 → mrok-0.2.2}/README.md +0 -0
  19. {mrok-0.2.0 → mrok-0.2.2}/dev.Dockerfile +0 -0
  20. {mrok-0.2.0 → mrok-0.2.2}/docker-compose.yaml +0 -0
  21. {mrok-0.2.0 → mrok-0.2.2}/entrypoint.sh +0 -0
  22. {mrok-0.2.0 → mrok-0.2.2}/mrok/__init__.py +0 -0
  23. {mrok-0.2.0 → mrok-0.2.2}/mrok/agent/__init__.py +0 -0
  24. {mrok-0.2.0 → mrok-0.2.2}/mrok/agent/sidecar/__init__.py +0 -0
  25. {mrok-0.2.0 → mrok-0.2.2}/mrok/agent/sidecar/app.py +0 -0
  26. {mrok-0.2.0 → mrok-0.2.2}/mrok/agent/sidecar/main.py +0 -0
  27. {mrok-0.2.0 → mrok-0.2.2}/mrok/agent/ziticorn.py +0 -0
  28. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/__init__.py +0 -0
  29. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/__init__.py +0 -0
  30. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/__init__.py +0 -0
  31. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/bootstrap.py +0 -0
  32. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/list/__init__.py +0 -0
  33. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/list/extensions.py +0 -0
  34. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/list/instances.py +0 -0
  35. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/register/__init__.py +0 -0
  36. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/register/extensions.py +0 -0
  37. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/register/instances.py +0 -0
  38. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/unregister/__init__.py +0 -0
  39. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/unregister/extensions.py +0 -0
  40. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/unregister/instances.py +0 -0
  41. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/admin/utils.py +0 -0
  42. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/agent/__init__.py +0 -0
  43. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/agent/run/__init__.py +0 -0
  44. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/agent/run/asgi.py +0 -0
  45. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/agent/run/sidecar.py +0 -0
  46. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/controller/__init__.py +0 -0
  47. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/controller/openapi.py +0 -0
  48. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/commands/controller/run.py +0 -0
  49. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/main.py +0 -0
  50. {mrok-0.2.0 → mrok-0.2.2}/mrok/cli/rich.py +0 -0
  51. {mrok-0.2.0 → mrok-0.2.2}/mrok/conf.py +0 -0
  52. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/__init__.py +0 -0
  53. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/app.py +0 -0
  54. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/auth.py +0 -0
  55. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/dependencies/__init__.py +0 -0
  56. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/dependencies/conf.py +0 -0
  57. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/dependencies/ziti.py +0 -0
  58. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/openapi/__init__.py +0 -0
  59. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/openapi/examples.py +0 -0
  60. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/openapi/utils.py +0 -0
  61. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/pagination.py +0 -0
  62. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/routes/__init__.py +0 -0
  63. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/routes/extensions.py +0 -0
  64. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/routes/instances.py +0 -0
  65. {mrok-0.2.0 → mrok-0.2.2}/mrok/controller/schemas.py +0 -0
  66. {mrok-0.2.0 → mrok-0.2.2}/mrok/errors.py +0 -0
  67. {mrok-0.2.0 → mrok-0.2.2}/mrok/http/__init__.py +0 -0
  68. {mrok-0.2.0 → mrok-0.2.2}/mrok/http/config.py +0 -0
  69. {mrok-0.2.0 → mrok-0.2.2}/mrok/http/forwarder.py +0 -0
  70. {mrok-0.2.0 → mrok-0.2.2}/mrok/http/lifespan.py +0 -0
  71. {mrok-0.2.0 → mrok-0.2.2}/mrok/http/protocol.py +0 -0
  72. {mrok-0.2.0 → mrok-0.2.2}/mrok/http/server.py +0 -0
  73. {mrok-0.2.0 → mrok-0.2.2}/mrok/logging.py +0 -0
  74. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/__init__.py +0 -0
  75. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/api.py +0 -0
  76. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/bootstrap.py +0 -0
  77. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/constants.py +0 -0
  78. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/errors.py +0 -0
  79. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/identities.py +0 -0
  80. {mrok-0.2.0 → mrok-0.2.2}/mrok/ziti/services.py +0 -0
  81. {mrok-0.2.0 → mrok-0.2.2}/prod.Dockerfile +0 -0
  82. {mrok-0.2.0 → mrok-0.2.2}/scripts/ziti.sh +0 -0
  83. {mrok-0.2.0 → mrok-0.2.2}/settings.yaml +0 -0
  84. {mrok-0.2.0 → mrok-0.2.2}/sonar-project.properties +0 -0
  85. {mrok-0.2.0 → mrok-0.2.2}/tests/__init__.py +0 -0
  86. {mrok-0.2.0 → mrok-0.2.2}/tests/agent/__init__.py +0 -0
  87. {mrok-0.2.0 → mrok-0.2.2}/tests/agent/sidecar/__init__.py +0 -0
  88. {mrok-0.2.0 → mrok-0.2.2}/tests/agent/sidecar/test_app.py +0 -0
  89. {mrok-0.2.0 → mrok-0.2.2}/tests/agent/sidecar/test_main.py +0 -0
  90. {mrok-0.2.0 → mrok-0.2.2}/tests/agent/test_ziticorn.py +0 -0
  91. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/__init__.py +0 -0
  92. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/admin/__init__.py +0 -0
  93. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/admin/test_bootstrap.py +0 -0
  94. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/admin/test_list.py +0 -0
  95. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/admin/test_register.py +0 -0
  96. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/admin/test_unregister.py +0 -0
  97. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/admin/test_utils.py +0 -0
  98. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/agent/__init__.py +0 -0
  99. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/agent/test_run.py +0 -0
  100. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/controller/__init__.py +0 -0
  101. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/controller/test_openapi.py +0 -0
  102. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/controller/test_run.py +0 -0
  103. {mrok-0.2.0 → mrok-0.2.2}/tests/cli/test_main.py +0 -0
  104. {mrok-0.2.0 → mrok-0.2.2}/tests/conftest.py +0 -0
  105. {mrok-0.2.0 → mrok-0.2.2}/tests/controller/__init__.py +0 -0
  106. {mrok-0.2.0 → mrok-0.2.2}/tests/controller/test_auth.py +0 -0
  107. {mrok-0.2.0 → mrok-0.2.2}/tests/controller/test_extensions.py +0 -0
  108. {mrok-0.2.0 → mrok-0.2.2}/tests/controller/test_instances.py +0 -0
  109. {mrok-0.2.0 → mrok-0.2.2}/tests/controller/test_openapi.py +0 -0
  110. {mrok-0.2.0 → mrok-0.2.2}/tests/http/__init__.py +0 -0
  111. {mrok-0.2.0 → mrok-0.2.2}/tests/http/test_config.py +0 -0
  112. {mrok-0.2.0 → mrok-0.2.2}/tests/http/test_forwarder.py +0 -0
  113. {mrok-0.2.0 → mrok-0.2.2}/tests/http/test_lifespan.py +0 -0
  114. {mrok-0.2.0 → mrok-0.2.2}/tests/http/test_protocol.py +0 -0
  115. {mrok-0.2.0 → mrok-0.2.2}/tests/http/test_server.py +0 -0
  116. {mrok-0.2.0 → mrok-0.2.2}/tests/ziti/__init__.py +0 -0
  117. {mrok-0.2.0 → mrok-0.2.2}/tests/ziti/test_api.py +0 -0
  118. {mrok-0.2.0 → mrok-0.2.2}/tests/ziti/test_bootstrap.py +0 -0
  119. {mrok-0.2.0 → mrok-0.2.2}/tests/ziti/test_identities.py +0 -0
  120. {mrok-0.2.0 → mrok-0.2.2}/tests/ziti/test_pki.py +0 -0
  121. {mrok-0.2.0 → mrok-0.2.2}/tests/ziti/test_services.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mrok
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: MPT Extensions OpenZiti Orchestrator
5
5
  Author: SoftwareOne AG
6
6
  License: Apache License
@@ -206,6 +206,7 @@ License: Apache License
206
206
  limitations under the License.
207
207
  License-File: LICENSE.txt
208
208
  Requires-Python: <4,>=3.12
209
+ Requires-Dist: asn1crypto<2.0.0,>=1.5.1
209
210
  Requires-Dist: cryptography<46.0.0,>=45.0.7
210
211
  Requires-Dist: dynaconf<4.0.0,>=3.2.11
211
212
  Requires-Dist: fastapi-pagination<0.15.0,>=0.14.1
@@ -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,9 +1,9 @@
1
1
  import base64
2
2
 
3
+ from asn1crypto import cms
3
4
  from cryptography import x509
4
5
  from cryptography.hazmat.primitives import hashes, serialization
5
6
  from cryptography.hazmat.primitives.asymmetric import rsa
6
- from cryptography.hazmat.primitives.serialization.pkcs7 import load_der_pkcs7_certificates
7
7
  from cryptography.x509.oid import NameOID
8
8
 
9
9
  from mrok.ziti.api import ZitiManagementAPI
@@ -16,11 +16,16 @@ async def get_ca_certificates(mgmt_api: ZitiManagementAPI) -> str:
16
16
  if not _ca_certificates:
17
17
  cas_pkcs7 = await mgmt_api.fetch_ca_certificates()
18
18
  pkcs7_bytes = base64.b64decode(cas_pkcs7)
19
- pkcs7_certs = load_der_pkcs7_certificates(pkcs7_bytes)
19
+
20
+ content_info = cms.ContentInfo.load(pkcs7_bytes)
21
+ certs = content_info["content"]["certificates"]
22
+
20
23
  ca_certificates = []
21
- for cert in pkcs7_certs:
22
- cert_pem = cert.public_bytes(serialization.Encoding.PEM)
23
- ca_certificates.append(cert_pem.decode("utf-8"))
24
+ for cert in certs:
25
+ crypt_cert = x509.load_der_x509_certificate(cert.dump())
26
+ pem = crypt_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
27
+ ca_certificates.append(pem)
28
+
24
29
  _ca_certificates = "\n".join(ca_certificates)
25
30
  return _ca_certificates
26
31
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mrok"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "MPT Extensions OpenZiti Orchestrator"
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  authors = [
@@ -9,6 +9,7 @@ authors = [
9
9
  license = { file = "LICENSE.txt" }
10
10
  requires-python = ">=3.12,<4"
11
11
  dependencies = [
12
+ "asn1crypto>=1.5.1,<2.0.0",
12
13
  "cryptography>=45.0.7,<46.0.0",
13
14
  "dynaconf>=3.2.11,<4.0.0",
14
15
  "fastapi-pagination>=0.14.1,<0.15.0",
@@ -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 = [p1, p2]
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"
@@ -37,6 +37,15 @@ wheels = [
37
37
  { url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" },
38
38
  ]
39
39
 
40
+ [[package]]
41
+ name = "asn1crypto"
42
+ version = "1.5.1"
43
+ source = { registry = "https://pypi.org/simple" }
44
+ sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" },
47
+ ]
48
+
40
49
  [[package]]
41
50
  name = "asttokens"
42
51
  version = "3.0.0"
@@ -672,6 +681,7 @@ name = "mrok"
672
681
  version = "0.0.0.dev0"
673
682
  source = { editable = "." }
674
683
  dependencies = [
684
+ { name = "asn1crypto" },
675
685
  { name = "cryptography" },
676
686
  { name = "dynaconf" },
677
687
  { name = "fastapi", extra = ["standard"] },
@@ -708,6 +718,7 @@ dev = [
708
718
 
709
719
  [package.metadata]
710
720
  requires-dist = [
721
+ { name = "asn1crypto", specifier = ">=1.5.1,<2.0.0" },
711
722
  { name = "cryptography", specifier = ">=45.0.7,<46.0.0" },
712
723
  { name = "dynaconf", specifier = ">=3.2.11,<4.0.0" },
713
724
  { name = "fastapi", extras = ["standard"], specifier = ">=0.119.0,<0.120.0" },
@@ -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