mellifera 0.2.0__py3-none-any.whl
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.
- mellifera/__init__.py +8 -0
- mellifera/executor.py +61 -0
- mellifera/executors/__init__.py +6 -0
- mellifera/executors/nsmainthread.py +70 -0
- mellifera/executors/trio.py +168 -0
- mellifera/orchestrator.py +92 -0
- mellifera/service.py +195 -0
- mellifera/services/__init__.py +3 -0
- mellifera/services/nsmainthread.py +61 -0
- mellifera/services/trio.py +146 -0
- mellifera-0.2.0.dist-info/METADATA +12 -0
- mellifera-0.2.0.dist-info/RECORD +13 -0
- mellifera-0.2.0.dist-info/WHEEL +4 -0
mellifera/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from mellifera.executors import TrioExecutor, HAS_NSMAINTHREAD
|
|
2
|
+
|
|
3
|
+
if HAS_NSMAINTHREAD:
|
|
4
|
+
from mellifera.executors import NSMainThreadExecutor
|
|
5
|
+
|
|
6
|
+
from mellifera.orchestrator import Orchestrator
|
|
7
|
+
from mellifera.service import Service, expose
|
|
8
|
+
from mellifera.services import TrioService, NSMainThreadService
|
mellifera/executor.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from mellifera.service import Service
|
|
4
|
+
|
|
5
|
+
class Executor(ABC):
|
|
6
|
+
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def run_threadsafe(self, f, *args, **kwargs):
|
|
9
|
+
"""Run function f with args and kwargs inside executor in a threadsafe matter
|
|
10
|
+
|
|
11
|
+
This function is threadsafe.
|
|
12
|
+
"""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def register_service(self, service: Service, name: str) -> None:
|
|
17
|
+
"""Register `service` under `name`
|
|
18
|
+
|
|
19
|
+
Only makes the service known, does not automatically start it
|
|
20
|
+
"""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def start_service(self, service: Service) -> None:
|
|
25
|
+
"""Start `service` eventually
|
|
26
|
+
|
|
27
|
+
Idempotent
|
|
28
|
+
"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def stop_service(self, service: Service) -> None:
|
|
33
|
+
"""Stop `service` eventually
|
|
34
|
+
|
|
35
|
+
Idempotent
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def stop_all(self) -> None:
|
|
41
|
+
"""Stops all services
|
|
42
|
+
|
|
43
|
+
Idempotent
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def start_sync(self) -> None:
|
|
49
|
+
"""Start the executor.
|
|
50
|
+
|
|
51
|
+
Returns after the executor is started (in a different thread or process)
|
|
52
|
+
"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def run_sync(self) -> None:
|
|
57
|
+
"""Run the executor
|
|
58
|
+
|
|
59
|
+
Returns after the executor is done
|
|
60
|
+
"""
|
|
61
|
+
...
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from Foundation import NSThread
|
|
3
|
+
from libdispatch import dispatch_async, dispatch_sync, dispatch_get_main_queue
|
|
4
|
+
except ModuleNotFoundError:
|
|
5
|
+
raise ModuleNotFoundError("To use mellifera.orchestrators.nsmainthread you need to have pyobjc installed and run on macos")
|
|
6
|
+
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
from mellifera.orchestrator import Orchestrator, threadsafe
|
|
10
|
+
from mellifera.services.nsmainthread import NSMainThreadService
|
|
11
|
+
from mellifera.service import Service
|
|
12
|
+
from mellifera.executor import Executor
|
|
13
|
+
|
|
14
|
+
from mellifera.service import ServiceState
|
|
15
|
+
|
|
16
|
+
class NSMainThreadExecutor(Executor):
|
|
17
|
+
requires_run = True
|
|
18
|
+
|
|
19
|
+
def __init__(self, orchestrator) -> None:
|
|
20
|
+
self.orchestrator = orchestrator
|
|
21
|
+
self.service = None
|
|
22
|
+
|
|
23
|
+
def run_threadsafe(self, f, *args, **kwargs):
|
|
24
|
+
if NSThread.isMainThread():
|
|
25
|
+
return f(*args, **kwargs)
|
|
26
|
+
else:
|
|
27
|
+
|
|
28
|
+
def closure():
|
|
29
|
+
return f(*args, **kwargs)
|
|
30
|
+
|
|
31
|
+
return dispatch_sync(dispatch_get_main_queue(), closure)
|
|
32
|
+
|
|
33
|
+
@threadsafe
|
|
34
|
+
def register_service(self, service: Service, name: str) -> None:
|
|
35
|
+
assert isinstance(service, NSMainThreadService)
|
|
36
|
+
if self.service is not None:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
"MainThreadOrchestrator has multiple services to manage, can only orchestrate a single MainThreadService"
|
|
39
|
+
)
|
|
40
|
+
self.service = service
|
|
41
|
+
service.register(self.orchestrator, self, name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@threadsafe
|
|
45
|
+
def start_service(self, service: Service) -> None:
|
|
46
|
+
if self.service is not None and self.service != service:
|
|
47
|
+
raise ValueError(f"Can only start a single service, already starting {self.service}")
|
|
48
|
+
if self.service is None:
|
|
49
|
+
self.service = service
|
|
50
|
+
service.state = ServiceState.WILL_BE_STARTED
|
|
51
|
+
|
|
52
|
+
@threadsafe
|
|
53
|
+
def stop_service(self, service: Service) -> None:
|
|
54
|
+
assert isinstance(service, NSMainThreadService)
|
|
55
|
+
service.stop()
|
|
56
|
+
|
|
57
|
+
@threadsafe
|
|
58
|
+
def stop_all(self) -> None:
|
|
59
|
+
self.service.stop()
|
|
60
|
+
|
|
61
|
+
def start_sync(self) -> None:
|
|
62
|
+
raise NotImplementedError()
|
|
63
|
+
|
|
64
|
+
def run_sync(self) -> None:
|
|
65
|
+
if self.service:
|
|
66
|
+
try:
|
|
67
|
+
self.service.init_sync()
|
|
68
|
+
self.service.run_sync()
|
|
69
|
+
finally:
|
|
70
|
+
self.service.finalize_sync()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
import trio
|
|
4
|
+
import trio.lowlevel
|
|
5
|
+
import threading
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
from queue import Queue
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
|
|
11
|
+
from mellifera.services.trio import TrioService, ServiceState
|
|
12
|
+
from mellifera.orchestrator import Orchestrator, threadsafe
|
|
13
|
+
from mellifera.service import Service
|
|
14
|
+
from mellifera.executor import Executor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TrioExecutor(Executor):
|
|
18
|
+
requires_run = False
|
|
19
|
+
|
|
20
|
+
def __init__(self, orchestrator) -> None:
|
|
21
|
+
self.orchestrator = orchestrator
|
|
22
|
+
self.trio_token = None
|
|
23
|
+
self.services = []
|
|
24
|
+
self.logger = logging.getLogger("mellifera.executors.TrioExecutor")
|
|
25
|
+
|
|
26
|
+
self.nursery = None
|
|
27
|
+
self.to_start = []
|
|
28
|
+
self._thread = None
|
|
29
|
+
self._lock = threading.RLock()
|
|
30
|
+
self.stopped = trio.Event()
|
|
31
|
+
|
|
32
|
+
self.write_channel, self.read_channel = trio.open_memory_channel(100)
|
|
33
|
+
|
|
34
|
+
def run_threadsafe(self, f, *args, **kwargs):
|
|
35
|
+
self.logger.info(f"run_threadsafe {f.__name__}")
|
|
36
|
+
|
|
37
|
+
if inspect.iscoroutinefunction(f):
|
|
38
|
+
async def closure():
|
|
39
|
+
return await f(*args, **kwargs)
|
|
40
|
+
is_async = True
|
|
41
|
+
else:
|
|
42
|
+
def closure():
|
|
43
|
+
return f(*args, **kwargs)
|
|
44
|
+
is_async = False
|
|
45
|
+
|
|
46
|
+
if trio.lowlevel.in_trio_run():
|
|
47
|
+
if is_async:
|
|
48
|
+
try:
|
|
49
|
+
return self.write_channel.send_nowait(closure)
|
|
50
|
+
except trio.BrokenResourceError as e:
|
|
51
|
+
self.logger.info("broken ressource error")
|
|
52
|
+
raise e
|
|
53
|
+
|
|
54
|
+
else:
|
|
55
|
+
return closure()
|
|
56
|
+
else:
|
|
57
|
+
# Orchestrator didn't start yet, use locking for synchronisation
|
|
58
|
+
if not self._thread:
|
|
59
|
+
with self._lock:
|
|
60
|
+
if is_async:
|
|
61
|
+
self.logger.info(f"run_threadsafe {f.__name__} with lock, trio.run")
|
|
62
|
+
return trio.run(closure)
|
|
63
|
+
else:
|
|
64
|
+
self.logger.info(f"run_threadsafe {f.__name__} with lock, direct")
|
|
65
|
+
return closure()
|
|
66
|
+
else:
|
|
67
|
+
if self.trio_token:
|
|
68
|
+
if is_async:
|
|
69
|
+
self.logger.info(f"run_threadsafe {f.__name__} with trio_token, from_thread.run")
|
|
70
|
+
return trio.from_thread.run(closure, trio_token=self.trio_token)
|
|
71
|
+
else:
|
|
72
|
+
self.logger.info(f"run_threadsafe {f.__name__} with trio_token, from_thread.run_sync")
|
|
73
|
+
return trio.from_thread.run_sync(
|
|
74
|
+
closure, trio_token=self.trio_token
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
"Calling TrioOrchestrator.run_in_thread but trio_token is None"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@threadsafe
|
|
82
|
+
def register_service(self, service: Service, name: str) -> None:
|
|
83
|
+
assert isinstance(service, TrioService)
|
|
84
|
+
service.register(self.orchestrator, self, name)
|
|
85
|
+
self.services.append(service)
|
|
86
|
+
|
|
87
|
+
@threadsafe
|
|
88
|
+
async def start_service(self, service):
|
|
89
|
+
if self.stopped.is_set():
|
|
90
|
+
raise ValueError("Trying to start a service when executor is already stopped")
|
|
91
|
+
if service.state == ServiceState.CONSTRUCTED or service.state == ServiceState.WILL_BE_STARTED:
|
|
92
|
+
if service.state == ServiceState.CONSTRUCTED:
|
|
93
|
+
service.state = ServiceState.WILL_BE_STARTED
|
|
94
|
+
self.services.append(service)
|
|
95
|
+
if self.nursery:
|
|
96
|
+
self.logger.info(f"starting running service {service.name}")
|
|
97
|
+
self.nursery.start_soon(self.run_service, service)
|
|
98
|
+
else:
|
|
99
|
+
self.to_start.append(service)
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(f"Service is in state {service.state}")
|
|
102
|
+
|
|
103
|
+
@threadsafe
|
|
104
|
+
async def stop_service(self, service):
|
|
105
|
+
try:
|
|
106
|
+
with trio.fail_after(5):
|
|
107
|
+
await service.stop()
|
|
108
|
+
except trio.TooSlowError:
|
|
109
|
+
with trio.move_on_after(10):
|
|
110
|
+
await service.cancel()
|
|
111
|
+
|
|
112
|
+
@threadsafe
|
|
113
|
+
async def stop_all(self):
|
|
114
|
+
for service in self.services:
|
|
115
|
+
self.stop_service(service)
|
|
116
|
+
await self.write_channel.aclose()
|
|
117
|
+
|
|
118
|
+
async def run_service(self, service):
|
|
119
|
+
self.logger.info(f"Running service {service.name}")
|
|
120
|
+
try:
|
|
121
|
+
self.logger.info(f"Initializing service {service.name}")
|
|
122
|
+
await service.init()
|
|
123
|
+
self.logger.info(f"Initialized service {service.name}")
|
|
124
|
+
self.logger.info(f"Running service {service.name}")
|
|
125
|
+
await service.run()
|
|
126
|
+
self.logger.info(f"Running service {service.name} Done")
|
|
127
|
+
finally:
|
|
128
|
+
self.logger.info(f"Finalizing service {service.name}")
|
|
129
|
+
await service.finalize()
|
|
130
|
+
self.logger.info(f"Finalizing service {service.name} Done")
|
|
131
|
+
|
|
132
|
+
async def start(self, queue: Queue) -> None:
|
|
133
|
+
trio_token = trio.lowlevel.current_trio_token()
|
|
134
|
+
queue.put(trio_token)
|
|
135
|
+
await self.run()
|
|
136
|
+
|
|
137
|
+
async def run(self) -> None:
|
|
138
|
+
async with trio.open_nursery() as nursery:
|
|
139
|
+
self.nursery = nursery
|
|
140
|
+
for service in self.to_start:
|
|
141
|
+
nursery.start_soon(self.run_service, service)
|
|
142
|
+
self.to_start = []
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
async with self.read_channel:
|
|
146
|
+
async for async_function in self.read_channel:
|
|
147
|
+
await async_function()
|
|
148
|
+
except trio.ClosedResourceError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
def start_sync(self) -> None:
|
|
152
|
+
try:
|
|
153
|
+
queue = Queue()
|
|
154
|
+
self._thread = threading.Thread(target=trio.run, args=(self.start, queue))
|
|
155
|
+
self._thread.start()
|
|
156
|
+
value = queue.get()
|
|
157
|
+
if isinstance(value, Exception):
|
|
158
|
+
raise value
|
|
159
|
+
else:
|
|
160
|
+
self.trio_token = value
|
|
161
|
+
except BaseException as e:
|
|
162
|
+
# self.logger.error("Exception occured during start_sync", exc_info=True)
|
|
163
|
+
# for name, service in self._services.items():
|
|
164
|
+
# self.logger.error(f"Service {name} is in state {service.state}")
|
|
165
|
+
raise e
|
|
166
|
+
|
|
167
|
+
def run_sync(self) -> None:
|
|
168
|
+
trio.run(self.run)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
import functools
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
from mellifera.executor import Executor
|
|
12
|
+
from mellifera.services import TrioService, NSMainThreadService
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from mellifera.service import Service
|
|
16
|
+
|
|
17
|
+
def threadsafe(f):
|
|
18
|
+
@functools.wraps(f)
|
|
19
|
+
def inner(self, *args, **kwargs):
|
|
20
|
+
return self.run_threadsafe(f, self, *args, **kwargs)
|
|
21
|
+
|
|
22
|
+
return inner
|
|
23
|
+
|
|
24
|
+
class Orchestrator:
|
|
25
|
+
|
|
26
|
+
def __init__(self, stop_on_finish=True) -> None:
|
|
27
|
+
self.logger = logging.getLogger("calsiprovis.orchestrator.ParentOrchestrator")
|
|
28
|
+
self.services = SimpleNamespace()
|
|
29
|
+
|
|
30
|
+
from mellifera.executors import TrioExecutor, HAS_NSMAINTHREAD
|
|
31
|
+
if HAS_NSMAINTHREAD:
|
|
32
|
+
from mellifera.executors import NSMainThreadExecutor
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
self.executors: dict[str, Executor] = {"trio": TrioExecutor(self)}
|
|
36
|
+
|
|
37
|
+
if HAS_NSMAINTHREAD:
|
|
38
|
+
self.executors["ns"] = NSMainThreadExecutor(self)
|
|
39
|
+
|
|
40
|
+
self.lock = threading.RLock()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def start_service(self, service: Service | str) -> None:
|
|
44
|
+
if isinstance(service, str):
|
|
45
|
+
service = self.get_service(service)
|
|
46
|
+
match service:
|
|
47
|
+
case TrioService():
|
|
48
|
+
self.executors["trio"].start_service(service)
|
|
49
|
+
case _:
|
|
50
|
+
raise ValueError(f"Cannot start service of type {type(service)}")
|
|
51
|
+
|
|
52
|
+
def stop_service(self, service: Service) -> None:
|
|
53
|
+
match service:
|
|
54
|
+
case TrioService():
|
|
55
|
+
self.executors["trio"].stop_service(service)
|
|
56
|
+
case NSMainThreadService():
|
|
57
|
+
self.executors["ns"].stop_service(service)
|
|
58
|
+
case _:
|
|
59
|
+
raise ValueError(f"Cannot start service of type {type(service)}")
|
|
60
|
+
|
|
61
|
+
def register_service(self, service: Service, name: str) -> None:
|
|
62
|
+
with self.lock:
|
|
63
|
+
match service:
|
|
64
|
+
case TrioService():
|
|
65
|
+
self.executors["trio"].register_service(service, name)
|
|
66
|
+
case NSMainThreadService():
|
|
67
|
+
if "ns" in self.executors:
|
|
68
|
+
self.executors["ns"].register_service(service, name)
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError("Cannot handle NSMainThreadService, as NSMainThreadExecutor is not available")
|
|
71
|
+
case _:
|
|
72
|
+
raise ValueError(f"Cannot handle service of type {type(service)}")
|
|
73
|
+
setattr(self.services, name, service)
|
|
74
|
+
|
|
75
|
+
def get_service(self, name: str) -> Service:
|
|
76
|
+
with self.lock:
|
|
77
|
+
if hasattr(self.services, name):
|
|
78
|
+
return getattr(self.services, name)
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(f"Service with name `{name}` not found")
|
|
81
|
+
|
|
82
|
+
def run(self):
|
|
83
|
+
if "ns" in self.executors:
|
|
84
|
+
self.executors["trio"].start_sync()
|
|
85
|
+
self.executors["ns"].run_sync()
|
|
86
|
+
else:
|
|
87
|
+
self.executors["trio"].run_sync()
|
|
88
|
+
|
|
89
|
+
def stop_all(self):
|
|
90
|
+
for executor in self.executors.values():
|
|
91
|
+
executor.stop_all()
|
|
92
|
+
|
mellifera/service.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from mellifera.orchestrator import Orchestrator
|
|
7
|
+
from logging import Logger
|
|
8
|
+
from mellifera.executor import Executor
|
|
9
|
+
|
|
10
|
+
import trio
|
|
11
|
+
from typing import final
|
|
12
|
+
import logging
|
|
13
|
+
from functools import wraps
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServiceError(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ServiceState(Enum):
|
|
23
|
+
NONE = 0
|
|
24
|
+
CONSTRUCTED = 1
|
|
25
|
+
WILL_BE_STARTED = 2
|
|
26
|
+
INITIATING = 3
|
|
27
|
+
INITIATED = 4
|
|
28
|
+
RUNNING = 5
|
|
29
|
+
STOPPING = 6
|
|
30
|
+
STOPPED = 7
|
|
31
|
+
SHUTDOWN_STARTED = 8
|
|
32
|
+
SHUTDOWN = 9
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def expose(f):
|
|
36
|
+
@wraps(f)
|
|
37
|
+
def _inner(service, *args, **kwargs):
|
|
38
|
+
if (not service.state == ServiceState.INITIATED) and (
|
|
39
|
+
not service.state == ServiceState.RUNNING
|
|
40
|
+
):
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
f"Service `{service.name}` not running, but is in state {service.state}"
|
|
43
|
+
)
|
|
44
|
+
return service.orchestrator.run_threadsafe(f, service, *args, **kwargs)
|
|
45
|
+
|
|
46
|
+
return _inner
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Service:
|
|
50
|
+
|
|
51
|
+
orchestrator: Orchestrator
|
|
52
|
+
executor: Executor
|
|
53
|
+
logger: Logger
|
|
54
|
+
name: str
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self._state = ServiceState.NONE
|
|
58
|
+
|
|
59
|
+
self.initiated_event = trio.Event()
|
|
60
|
+
self.started_event = trio.Event()
|
|
61
|
+
self.stopped_event = trio.Event()
|
|
62
|
+
self.shutdown_event = trio.Event()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def initiated(self):
|
|
67
|
+
return self.initiated_event.wait()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def started(self):
|
|
71
|
+
return self.started_event.wait()
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def stopped(self):
|
|
75
|
+
return self.stopped_event.wait()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def shutdown(self):
|
|
79
|
+
return self.shutdown_event.wait()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def state(self) -> ServiceState:
|
|
83
|
+
return self._state
|
|
84
|
+
|
|
85
|
+
@state.setter
|
|
86
|
+
def state(self, state: ServiceState) -> None:
|
|
87
|
+
match state:
|
|
88
|
+
case ServiceState.NONE:
|
|
89
|
+
assert False
|
|
90
|
+
case ServiceState.CONSTRUCTED:
|
|
91
|
+
if self._state != ServiceState.NONE:
|
|
92
|
+
raise ServiceError(
|
|
93
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
94
|
+
)
|
|
95
|
+
self._state = state
|
|
96
|
+
case ServiceState.WILL_BE_STARTED:
|
|
97
|
+
if self._state != ServiceState.CONSTRUCTED:
|
|
98
|
+
raise ServiceError(
|
|
99
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
100
|
+
)
|
|
101
|
+
self._state = state
|
|
102
|
+
case ServiceState.INITIATING:
|
|
103
|
+
if self._state != ServiceState.WILL_BE_STARTED:
|
|
104
|
+
raise ServiceError(
|
|
105
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
106
|
+
)
|
|
107
|
+
self._state = state
|
|
108
|
+
self.logger.debug("Initiating")
|
|
109
|
+
case ServiceState.INITIATED:
|
|
110
|
+
if self._state != ServiceState.INITIATING:
|
|
111
|
+
raise ServiceError(
|
|
112
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
113
|
+
)
|
|
114
|
+
self._state = state
|
|
115
|
+
self.logger.debug("Initiating done")
|
|
116
|
+
self.initiated_event.set()
|
|
117
|
+
case ServiceState.RUNNING:
|
|
118
|
+
if self._state != ServiceState.INITIATED:
|
|
119
|
+
raise ServiceError(
|
|
120
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
121
|
+
)
|
|
122
|
+
self._state = state
|
|
123
|
+
self.logger.debug("Running")
|
|
124
|
+
self.started_event.set()
|
|
125
|
+
case ServiceState.STOPPING:
|
|
126
|
+
if self._state.value >= state.value:
|
|
127
|
+
raise ServiceError(
|
|
128
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
129
|
+
)
|
|
130
|
+
self._state = state
|
|
131
|
+
self.logger.debug("Stopping")
|
|
132
|
+
case ServiceState.STOPPED:
|
|
133
|
+
if (self._state != ServiceState.RUNNING) and (
|
|
134
|
+
self._state != ServiceState.STOPPING
|
|
135
|
+
):
|
|
136
|
+
raise ServiceError(
|
|
137
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
138
|
+
)
|
|
139
|
+
self._state = state
|
|
140
|
+
self.logger.debug("Stopped")
|
|
141
|
+
self.stopped_event.set()
|
|
142
|
+
case ServiceState.SHUTDOWN_STARTED:
|
|
143
|
+
self._state = state
|
|
144
|
+
self.logger.debug("Shutting Down")
|
|
145
|
+
case ServiceState.SHUTDOWN:
|
|
146
|
+
if self._state != ServiceState.SHUTDOWN_STARTED:
|
|
147
|
+
raise ServiceError(
|
|
148
|
+
f"Trying to set service {self.name} into state {state.name} which is in state {self.state.name}"
|
|
149
|
+
)
|
|
150
|
+
self._state = state
|
|
151
|
+
self.logger.debug("Shutting Down Done")
|
|
152
|
+
self.shutdown_event.set()
|
|
153
|
+
|
|
154
|
+
def register(self, orchestrator, executor, name):
|
|
155
|
+
self.orchestrator = orchestrator
|
|
156
|
+
self.executor = executor
|
|
157
|
+
self.name = name
|
|
158
|
+
self._register()
|
|
159
|
+
self.state = ServiceState.CONSTRUCTED
|
|
160
|
+
self.logger = logging.getLogger(f"mellifera.service.{name}")
|
|
161
|
+
|
|
162
|
+
def _register(self):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
@final
|
|
166
|
+
def init_sync(self) -> None:
|
|
167
|
+
if self.state != ServiceState.CONSTRUCTED:
|
|
168
|
+
raise ServiceError(
|
|
169
|
+
f"Trying to initate a service that is in state {self.state.name}"
|
|
170
|
+
)
|
|
171
|
+
self.state = ServiceState.INITIATING
|
|
172
|
+
self._init_sync()
|
|
173
|
+
self.state = ServiceState.INITIATED
|
|
174
|
+
|
|
175
|
+
def _init_sync(self) -> None:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
@final
|
|
179
|
+
def shut_down_sync(self) -> None:
|
|
180
|
+
if self.state.value < ServiceState.STOPPED.value:
|
|
181
|
+
self.logger.error(
|
|
182
|
+
f"shut_down called for service {self.name}, but in state {self.state.name}"
|
|
183
|
+
)
|
|
184
|
+
if self.state.value == ServiceState.SHUTDOWN:
|
|
185
|
+
self.logger.error(
|
|
186
|
+
f"shut_down called for service {self.name}, but in state {self.state.name}"
|
|
187
|
+
)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
self.state = ServiceState.SHUTDOWN_STARTED
|
|
191
|
+
self._shut_down_sync()
|
|
192
|
+
self.state = ServiceState.SHUTDOWN
|
|
193
|
+
|
|
194
|
+
def _shut_down_sync(self) -> None:
|
|
195
|
+
pass
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from mellifera.service import Service, ServiceState, ServiceError
|
|
2
|
+
from typing import final
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NSMainThreadService(Service):
|
|
6
|
+
|
|
7
|
+
def stop_sync(self) -> None:
|
|
8
|
+
if self.state.value >= ServiceState.STOPPING.value:
|
|
9
|
+
return
|
|
10
|
+
if self.state == ServiceState.RUNNING:
|
|
11
|
+
self.state = ServiceState.STOPPING
|
|
12
|
+
self._stop_sync()
|
|
13
|
+
else:
|
|
14
|
+
self.state = ServiceState.STOPPING
|
|
15
|
+
self.state = ServiceState.STOPPED
|
|
16
|
+
|
|
17
|
+
def run_sync(self) -> None:
|
|
18
|
+
if self.state != ServiceState.INITIATED:
|
|
19
|
+
raise ServiceError(
|
|
20
|
+
f"Trying to initate a service that is in state {self.state.name}"
|
|
21
|
+
)
|
|
22
|
+
self.state = ServiceState.RUNNING
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
self._run_sync()
|
|
26
|
+
self.stop_sync()
|
|
27
|
+
finally:
|
|
28
|
+
self.state = ServiceState.STOPPED
|
|
29
|
+
self._nursery = None
|
|
30
|
+
|
|
31
|
+
def _run_sync(self) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@final
|
|
35
|
+
def stop(self) -> None:
|
|
36
|
+
if self.state.value >= ServiceState.STOPPING.value:
|
|
37
|
+
return
|
|
38
|
+
self.state = ServiceState.STOPPING
|
|
39
|
+
self._stop_sync()
|
|
40
|
+
|
|
41
|
+
def _stop_sync(self) -> None:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@final
|
|
45
|
+
def shut_down_sync(self) -> None:
|
|
46
|
+
if self.state.value < ServiceState.STOPPED.value:
|
|
47
|
+
self.logger.error(
|
|
48
|
+
f"shut_down called for service {self.name}, but in state {self.state.name}"
|
|
49
|
+
)
|
|
50
|
+
if self.state == ServiceState.SHUTDOWN:
|
|
51
|
+
self.logger.error(
|
|
52
|
+
f"shut_down called for service {self.name}, but in state {self.state.name}"
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
self.state = ServiceState.SHUTDOWN_STARTED
|
|
57
|
+
self._shut_down_sync()
|
|
58
|
+
self.state = ServiceState.SHUTDOWN
|
|
59
|
+
|
|
60
|
+
def _shut_down_sync(self) -> None:
|
|
61
|
+
pass
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import trio
|
|
2
|
+
from typing import final
|
|
3
|
+
|
|
4
|
+
from mellifera.service import Service, ServiceState, ServiceError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TrioService(Service):
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__()
|
|
11
|
+
|
|
12
|
+
self._nursery = None
|
|
13
|
+
self._trio_token = None
|
|
14
|
+
self._stopped = trio.Event()
|
|
15
|
+
self._default_is_running = trio.Event()
|
|
16
|
+
self._dependents = []
|
|
17
|
+
self._dependencies = []
|
|
18
|
+
self._requires = []
|
|
19
|
+
self.cancel_scope = trio.CancelScope()
|
|
20
|
+
|
|
21
|
+
@final
|
|
22
|
+
async def init(self) -> None:
|
|
23
|
+
if self.state != ServiceState.WILL_BE_STARTED:
|
|
24
|
+
raise ServiceError(
|
|
25
|
+
f"Trying to initate a service that is in state {self.state.name}"
|
|
26
|
+
)
|
|
27
|
+
assert self.name
|
|
28
|
+
assert self.orchestrator
|
|
29
|
+
assert self.executor
|
|
30
|
+
self.state = ServiceState.INITIATING
|
|
31
|
+
|
|
32
|
+
for dependency in self._dependencies:
|
|
33
|
+
self.orchestrator.start_service(dependency)
|
|
34
|
+
for dependency in self._dependencies:
|
|
35
|
+
await dependency.initiated
|
|
36
|
+
for dependency in self._requires:
|
|
37
|
+
await dependency.initiated
|
|
38
|
+
await self._init()
|
|
39
|
+
self.state = ServiceState.INITIATED
|
|
40
|
+
|
|
41
|
+
@final
|
|
42
|
+
async def run(self) -> None:
|
|
43
|
+
if self.state != ServiceState.INITIATED:
|
|
44
|
+
raise ServiceError(
|
|
45
|
+
f"Trying to initate a service that is in state {self.state.name}"
|
|
46
|
+
)
|
|
47
|
+
self.state = ServiceState.RUNNING
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with self.cancel_scope:
|
|
51
|
+
await self._run()
|
|
52
|
+
await self.stop()
|
|
53
|
+
finally:
|
|
54
|
+
self.state = ServiceState.STOPPED
|
|
55
|
+
self._nursery = None
|
|
56
|
+
|
|
57
|
+
@final
|
|
58
|
+
async def stop(self) -> None:
|
|
59
|
+
if self.state.value >= ServiceState.STOPPING.value:
|
|
60
|
+
return
|
|
61
|
+
if self.state == ServiceState.RUNNING:
|
|
62
|
+
self.state = ServiceState.STOPPING
|
|
63
|
+
for dependent in self._dependents:
|
|
64
|
+
self.logger.debug(f"Waiting for {dependent.name}")
|
|
65
|
+
await dependent.stopped
|
|
66
|
+
self.logger.debug(f"Waiting for {dependent.name} DONE")
|
|
67
|
+
await self._stop()
|
|
68
|
+
else:
|
|
69
|
+
self.state = ServiceState.STOPPING
|
|
70
|
+
self.state = ServiceState.STOPPED
|
|
71
|
+
|
|
72
|
+
def requires(self, name: str) -> Service:
|
|
73
|
+
if not self.orchestrator:
|
|
74
|
+
raise ValueError("Service is not attached to an orchestrator")
|
|
75
|
+
service = self.orchestrator.get_service(name)
|
|
76
|
+
self._requires.append(service)
|
|
77
|
+
return service
|
|
78
|
+
|
|
79
|
+
def depends_on(self, name: str) -> Service:
|
|
80
|
+
if not self.orchestrator:
|
|
81
|
+
raise ValueError("Service is not attached to an orchestrator")
|
|
82
|
+
service = self.orchestrator.get_service(name)
|
|
83
|
+
assert isinstance(service, TrioService)
|
|
84
|
+
self._dependencies.append(service)
|
|
85
|
+
service._dependents.append(self)
|
|
86
|
+
return service
|
|
87
|
+
|
|
88
|
+
def uses(self, name: str) -> Service:
|
|
89
|
+
if not self.orchestrator:
|
|
90
|
+
raise ValueError("Service is not attached to an orchestrator")
|
|
91
|
+
service = self.orchestrator.get_service(name)
|
|
92
|
+
return service
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def start(self, nursery: trio.Nursery, task_status=trio.TASK_STATUS_IGNORED) -> None:
|
|
97
|
+
task_status.started()
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@final
|
|
102
|
+
async def cancel(self) -> None:
|
|
103
|
+
self.logger.debug("Cancelling")
|
|
104
|
+
if self._nursery:
|
|
105
|
+
self._nursery.cancel_scope.cancel()
|
|
106
|
+
self.logger.debug("Cancelling Done")
|
|
107
|
+
|
|
108
|
+
@final
|
|
109
|
+
async def finalize(self) -> None:
|
|
110
|
+
if self.state.value < ServiceState.STOPPED.value:
|
|
111
|
+
self.logger.error(
|
|
112
|
+
f"shut_down called for service {self.name}, but in state {self.state.name}"
|
|
113
|
+
)
|
|
114
|
+
if self.state == ServiceState.SHUTDOWN:
|
|
115
|
+
self.logger.error(
|
|
116
|
+
f"shut_down called for service {self.name}, but in state {self.state.name}"
|
|
117
|
+
)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
self.state = ServiceState.SHUTDOWN_STARTED
|
|
121
|
+
await self._finalize()
|
|
122
|
+
self.state = ServiceState.SHUTDOWN
|
|
123
|
+
|
|
124
|
+
def _construct(self) -> None:
|
|
125
|
+
"""Override to request other services
|
|
126
|
+
"""
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
async def _init(self) -> None:
|
|
130
|
+
"""Override to customize initialization.
|
|
131
|
+
|
|
132
|
+
Aquire ressources here
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
async def _run(self) -> None:
|
|
137
|
+
"""Override to customize running behavior"""
|
|
138
|
+
await trio.sleep_forever()
|
|
139
|
+
|
|
140
|
+
async def _stop(self) -> None:
|
|
141
|
+
"""Override to stop running"""
|
|
142
|
+
self.cancel_scope.cancel()
|
|
143
|
+
|
|
144
|
+
async def _finalize(self) -> None:
|
|
145
|
+
"""Override to close all ressources aquired on _init"""
|
|
146
|
+
pass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mellifera
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Runs async services under trio and orchestrates them with a pyobjc NSMainThread service
|
|
5
|
+
Requires-Dist: pyobjc-core>=12.0 ; sys_platform == 'darwin'
|
|
6
|
+
Requires-Dist: trio>=0.32.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
Service Orchestrator
|
|
11
|
+
|
|
12
|
+
Mellifera orchestrates async services with a mainthread service
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
mellifera/__init__.py,sha256=P96Bx3MQntFnk4niGwkbA56aMgFtjxryd5buFQpFLNM,301
|
|
2
|
+
mellifera/executor.py,sha256=xCGJEIC9h8h1GgtNNaAALWs8_kzrBrVhsLZVRegX04E,1326
|
|
3
|
+
mellifera/executors/__init__.py,sha256=d7u4J3zwuEFzFxXYWK7gva4PkaooCfuPurAgJ76W0Jc,210
|
|
4
|
+
mellifera/executors/nsmainthread.py,sha256=7BAJ0KaDX6VdtsRitQhMfIKHmpu4kVPdSr0hvzRFMkM,2333
|
|
5
|
+
mellifera/executors/trio.py,sha256=u4OySuybuA84jK4lznBfjQHoo3WKc3sgstmj1eZpcXw,6268
|
|
6
|
+
mellifera/orchestrator.py,sha256=8rxGaNkwtFbusGMJPmjvYkQUxyd4vJos6-vhN4dUC1E,3132
|
|
7
|
+
mellifera/service.py,sha256=8aKbOEF5O6sGtuARONSJhfSzQAgvlE9BGJTvkspUyN4,6499
|
|
8
|
+
mellifera/services/__init__.py,sha256=otvmQMlP7eFpOvU4UV4HSidxsHslv_-iXvLi0VGvyVY,113
|
|
9
|
+
mellifera/services/nsmainthread.py,sha256=FmQxukzrvq_Y1xauqSm11q9jEQXQmY_d5S57nT_eth4,1804
|
|
10
|
+
mellifera/services/trio.py,sha256=6Kox60XKszaTx0J7CK8IKaIwwAKh0ROgKJQ3vgpfYGo,4621
|
|
11
|
+
mellifera-0.2.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
12
|
+
mellifera-0.2.0.dist-info/METADATA,sha256=GupC66Mu0SOBnnyt21v5mgWD4cnZzp1UplupH39kimI,389
|
|
13
|
+
mellifera-0.2.0.dist-info/RECORD,,
|