ominfra 0.0.0.dev122__py3-none-any.whl → 0.0.0.dev123__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.
- ominfra/deploy/_executor.py +1 -1
- ominfra/journald/messages.py +1 -1
- ominfra/pyremote/_runcommands.py +1 -1
- ominfra/scripts/journald2aws.py +2 -2
- ominfra/scripts/supervisor.py +2216 -2044
- ominfra/supervisor/context.py +16 -41
- ominfra/supervisor/dispatchers.py +88 -58
- ominfra/supervisor/events.py +59 -70
- ominfra/supervisor/groups.py +162 -0
- ominfra/supervisor/main.py +27 -11
- ominfra/supervisor/poller.py +51 -53
- ominfra/supervisor/process.py +267 -352
- ominfra/supervisor/signals.py +52 -0
- ominfra/supervisor/states.py +26 -47
- ominfra/supervisor/supervisor.py +105 -81
- ominfra/supervisor/types.py +57 -6
- ominfra/supervisor/{compat.py → utils.py} +34 -53
- {ominfra-0.0.0.dev122.dist-info → ominfra-0.0.0.dev123.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev122.dist-info → ominfra-0.0.0.dev123.dist-info}/RECORD +23 -21
- {ominfra-0.0.0.dev122.dist-info → ominfra-0.0.0.dev123.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev122.dist-info → ominfra-0.0.0.dev123.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev122.dist-info → ominfra-0.0.0.dev123.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev122.dist-info → ominfra-0.0.0.dev123.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import dataclasses as dc
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from .configs import ProcessConfig
|
6
|
+
from .configs import ProcessGroupConfig
|
7
|
+
from .context import ServerContext
|
8
|
+
from .dispatchers import Dispatcher
|
9
|
+
from .events import EventCallbacks
|
10
|
+
from .events import ProcessGroupAddedEvent
|
11
|
+
from .events import ProcessGroupRemovedEvent
|
12
|
+
from .states import ProcessState
|
13
|
+
from .types import AbstractProcessGroup
|
14
|
+
from .types import AbstractServerContext
|
15
|
+
from .types import AbstractSubprocess
|
16
|
+
|
17
|
+
|
18
|
+
##
|
19
|
+
|
20
|
+
|
21
|
+
@dc.dataclass(frozen=True)
|
22
|
+
class SubprocessFactory:
|
23
|
+
fn: ta.Callable[[ProcessConfig, AbstractProcessGroup], AbstractSubprocess]
|
24
|
+
|
25
|
+
def __call__(self, config: ProcessConfig, group: AbstractProcessGroup) -> AbstractSubprocess:
|
26
|
+
return self.fn(config, group)
|
27
|
+
|
28
|
+
|
29
|
+
class ProcessGroup(AbstractProcessGroup):
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
config: ProcessGroupConfig,
|
33
|
+
context: ServerContext,
|
34
|
+
*,
|
35
|
+
subprocess_factory: SubprocessFactory,
|
36
|
+
):
|
37
|
+
super().__init__()
|
38
|
+
|
39
|
+
self._config = config
|
40
|
+
self._context = context
|
41
|
+
self._subprocess_factory = subprocess_factory
|
42
|
+
|
43
|
+
self._processes = {}
|
44
|
+
for pconfig in self._config.processes or []:
|
45
|
+
process = self._subprocess_factory(pconfig, self)
|
46
|
+
self._processes[pconfig.name] = process
|
47
|
+
|
48
|
+
@property
|
49
|
+
def config(self) -> ProcessGroupConfig:
|
50
|
+
return self._config
|
51
|
+
|
52
|
+
@property
|
53
|
+
def name(self) -> str:
|
54
|
+
return self._config.name
|
55
|
+
|
56
|
+
@property
|
57
|
+
def context(self) -> AbstractServerContext:
|
58
|
+
return self._context
|
59
|
+
|
60
|
+
def __repr__(self):
|
61
|
+
# repr can't return anything other than a native string, but the name might be unicode - a problem on Python 2.
|
62
|
+
name = self._config.name
|
63
|
+
return f'<{self.__class__.__name__} instance at {id(self)} named {name}>'
|
64
|
+
|
65
|
+
def remove_logs(self) -> None:
|
66
|
+
for process in self._processes.values():
|
67
|
+
process.remove_logs()
|
68
|
+
|
69
|
+
def reopen_logs(self) -> None:
|
70
|
+
for process in self._processes.values():
|
71
|
+
process.reopen_logs()
|
72
|
+
|
73
|
+
def stop_all(self) -> None:
|
74
|
+
processes = list(self._processes.values())
|
75
|
+
processes.sort()
|
76
|
+
processes.reverse() # stop in desc priority order
|
77
|
+
|
78
|
+
for proc in processes:
|
79
|
+
state = proc.get_state()
|
80
|
+
if state == ProcessState.RUNNING:
|
81
|
+
# RUNNING -> STOPPING
|
82
|
+
proc.stop()
|
83
|
+
|
84
|
+
elif state == ProcessState.STARTING:
|
85
|
+
# STARTING -> STOPPING
|
86
|
+
proc.stop()
|
87
|
+
|
88
|
+
elif state == ProcessState.BACKOFF:
|
89
|
+
# BACKOFF -> FATAL
|
90
|
+
proc.give_up()
|
91
|
+
|
92
|
+
def get_unstopped_processes(self) -> ta.List[AbstractSubprocess]:
|
93
|
+
return [x for x in self._processes.values() if not x.get_state().stopped]
|
94
|
+
|
95
|
+
def get_dispatchers(self) -> ta.Dict[int, Dispatcher]:
|
96
|
+
dispatchers: dict = {}
|
97
|
+
for process in self._processes.values():
|
98
|
+
dispatchers.update(process.get_dispatchers())
|
99
|
+
return dispatchers
|
100
|
+
|
101
|
+
def before_remove(self) -> None:
|
102
|
+
pass
|
103
|
+
|
104
|
+
def transition(self) -> None:
|
105
|
+
for proc in self._processes.values():
|
106
|
+
proc.transition()
|
107
|
+
|
108
|
+
def after_setuid(self) -> None:
|
109
|
+
for proc in self._processes.values():
|
110
|
+
proc.create_auto_child_logs()
|
111
|
+
|
112
|
+
|
113
|
+
##
|
114
|
+
|
115
|
+
|
116
|
+
class ProcessGroups:
|
117
|
+
def __init__(
|
118
|
+
self,
|
119
|
+
*,
|
120
|
+
event_callbacks: EventCallbacks,
|
121
|
+
) -> None:
|
122
|
+
super().__init__()
|
123
|
+
|
124
|
+
self._event_callbacks = event_callbacks
|
125
|
+
|
126
|
+
self._by_name: ta.Dict[str, ProcessGroup] = {}
|
127
|
+
|
128
|
+
def get(self, name: str) -> ta.Optional[ProcessGroup]:
|
129
|
+
return self._by_name.get(name)
|
130
|
+
|
131
|
+
def __getitem__(self, name: str) -> ProcessGroup:
|
132
|
+
return self._by_name[name]
|
133
|
+
|
134
|
+
def __len__(self) -> int:
|
135
|
+
return len(self._by_name)
|
136
|
+
|
137
|
+
def __iter__(self) -> ta.Iterator[ProcessGroup]:
|
138
|
+
return iter(self._by_name.values())
|
139
|
+
|
140
|
+
def all(self) -> ta.Mapping[str, ProcessGroup]:
|
141
|
+
return self._by_name
|
142
|
+
|
143
|
+
def add(self, group: ProcessGroup) -> None:
|
144
|
+
if (name := group.name) in self._by_name:
|
145
|
+
raise KeyError(f'Process group already exists: {name}')
|
146
|
+
|
147
|
+
self._by_name[name] = group
|
148
|
+
|
149
|
+
self._event_callbacks.notify(ProcessGroupAddedEvent(name))
|
150
|
+
|
151
|
+
def remove(self, name: str) -> None:
|
152
|
+
group = self._by_name[name]
|
153
|
+
|
154
|
+
group.before_remove()
|
155
|
+
|
156
|
+
del self._by_name[name]
|
157
|
+
|
158
|
+
self._event_callbacks.notify(ProcessGroupRemovedEvent(name))
|
159
|
+
|
160
|
+
def clear(self) -> None:
|
161
|
+
# FIXME: events?
|
162
|
+
self._by_name.clear()
|
ominfra/supervisor/main.py
CHANGED
@@ -14,22 +14,30 @@ from omlish.lite.journald import journald_log_handler_factory
|
|
14
14
|
from omlish.lite.logs import configure_standard_logging
|
15
15
|
|
16
16
|
from ..configs import read_config_file
|
17
|
-
from .compat import ExitNow
|
18
|
-
from .compat import get_open_fds
|
19
17
|
from .configs import ProcessConfig
|
20
18
|
from .configs import ProcessGroupConfig
|
21
19
|
from .configs import ServerConfig
|
22
20
|
from .configs import prepare_server_config
|
23
|
-
from .context import InheritedFds
|
24
21
|
from .context import ServerContext
|
25
22
|
from .context import ServerEpoch
|
26
|
-
from .
|
23
|
+
from .events import EventCallbacks
|
24
|
+
from .groups import ProcessGroup
|
25
|
+
from .groups import SubprocessFactory
|
26
|
+
from .poller import Poller
|
27
|
+
from .poller import get_poller_impl
|
28
|
+
from .process import InheritedFds
|
27
29
|
from .process import Subprocess
|
28
|
-
from .
|
29
|
-
from .states import
|
30
|
+
from .signals import SignalReceiver
|
31
|
+
from .states import SupervisorState
|
30
32
|
from .supervisor import ProcessGroupFactory
|
33
|
+
from .supervisor import ProcessGroups
|
34
|
+
from .supervisor import SignalHandler
|
31
35
|
from .supervisor import Supervisor
|
36
|
+
from .types import AbstractProcessGroup
|
32
37
|
from .types import AbstractServerContext
|
38
|
+
from .types import AbstractSubprocess
|
39
|
+
from .utils import ExitNow
|
40
|
+
from .utils import get_open_fds
|
33
41
|
|
34
42
|
|
35
43
|
##
|
@@ -44,9 +52,17 @@ def build_server_bindings(
|
|
44
52
|
lst: ta.List[InjectorBindingOrBindings] = [
|
45
53
|
inj.bind(config),
|
46
54
|
|
55
|
+
inj.bind(get_poller_impl(), key=Poller, singleton=True),
|
56
|
+
|
47
57
|
inj.bind(ServerContext, singleton=True),
|
48
58
|
inj.bind(AbstractServerContext, to_key=ServerContext),
|
49
59
|
|
60
|
+
inj.bind(EventCallbacks, singleton=True),
|
61
|
+
|
62
|
+
inj.bind(SignalReceiver, singleton=True),
|
63
|
+
|
64
|
+
inj.bind(SignalHandler, singleton=True),
|
65
|
+
inj.bind(ProcessGroups, singleton=True),
|
50
66
|
inj.bind(Supervisor, singleton=True),
|
51
67
|
]
|
52
68
|
|
@@ -59,7 +75,7 @@ def build_server_bindings(
|
|
59
75
|
lst.append(inj.bind(make_process_group_factory))
|
60
76
|
|
61
77
|
def make_subprocess_factory(injector: Injector) -> SubprocessFactory:
|
62
|
-
def inner(process_config: ProcessConfig, group:
|
78
|
+
def inner(process_config: ProcessConfig, group: AbstractProcessGroup) -> AbstractSubprocess:
|
63
79
|
return injector.inject(functools.partial(Subprocess, process_config, group))
|
64
80
|
return SubprocessFactory(inner)
|
65
81
|
lst.append(inj.bind(make_subprocess_factory))
|
@@ -105,9 +121,9 @@ def main(
|
|
105
121
|
|
106
122
|
#
|
107
123
|
|
108
|
-
|
124
|
+
inherited_fds: ta.Optional[InheritedFds] = None
|
109
125
|
if args.inherit_initial_fds:
|
110
|
-
|
126
|
+
inherited_fds = InheritedFds(get_open_fds(0x10000))
|
111
127
|
|
112
128
|
# if we hup, restart by making a new Supervisor()
|
113
129
|
for epoch in itertools.count():
|
@@ -120,7 +136,7 @@ def main(
|
|
120
136
|
injector = inj.create_injector(build_server_bindings(
|
121
137
|
config,
|
122
138
|
server_epoch=ServerEpoch(epoch),
|
123
|
-
inherited_fds=
|
139
|
+
inherited_fds=inherited_fds,
|
124
140
|
))
|
125
141
|
|
126
142
|
context = injector.provide(ServerContext)
|
@@ -131,7 +147,7 @@ def main(
|
|
131
147
|
except ExitNow:
|
132
148
|
pass
|
133
149
|
|
134
|
-
if context.state <
|
150
|
+
if context.state < SupervisorState.RESTARTING:
|
135
151
|
break
|
136
152
|
|
137
153
|
|
ominfra/supervisor/poller.py
CHANGED
@@ -8,8 +8,7 @@ import typing as ta
|
|
8
8
|
from omlish.lite.logs import log
|
9
9
|
|
10
10
|
|
11
|
-
class
|
12
|
-
|
11
|
+
class Poller(abc.ABC):
|
13
12
|
def __init__(self) -> None:
|
14
13
|
super().__init__()
|
15
14
|
|
@@ -43,35 +42,34 @@ class BasePoller(abc.ABC):
|
|
43
42
|
pass
|
44
43
|
|
45
44
|
|
46
|
-
class SelectPoller(
|
47
|
-
|
45
|
+
class SelectPoller(Poller):
|
48
46
|
def __init__(self) -> None:
|
49
47
|
super().__init__()
|
50
48
|
|
51
|
-
self.
|
52
|
-
self.
|
49
|
+
self._readable: ta.Set[int] = set()
|
50
|
+
self._writable: ta.Set[int] = set()
|
53
51
|
|
54
52
|
def register_readable(self, fd: int) -> None:
|
55
|
-
self.
|
53
|
+
self._readable.add(fd)
|
56
54
|
|
57
55
|
def register_writable(self, fd: int) -> None:
|
58
|
-
self.
|
56
|
+
self._writable.add(fd)
|
59
57
|
|
60
58
|
def unregister_readable(self, fd: int) -> None:
|
61
|
-
self.
|
59
|
+
self._readable.discard(fd)
|
62
60
|
|
63
61
|
def unregister_writable(self, fd: int) -> None:
|
64
|
-
self.
|
62
|
+
self._writable.discard(fd)
|
65
63
|
|
66
64
|
def unregister_all(self) -> None:
|
67
|
-
self.
|
68
|
-
self.
|
65
|
+
self._readable.clear()
|
66
|
+
self._writable.clear()
|
69
67
|
|
70
68
|
def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
|
71
69
|
try:
|
72
70
|
r, w, x = select.select(
|
73
|
-
self.
|
74
|
-
self.
|
71
|
+
self._readable,
|
72
|
+
self._writable,
|
75
73
|
[], timeout,
|
76
74
|
)
|
77
75
|
except OSError as err:
|
@@ -86,7 +84,7 @@ class SelectPoller(BasePoller):
|
|
86
84
|
return r, w
|
87
85
|
|
88
86
|
|
89
|
-
class PollPoller(
|
87
|
+
class PollPoller(Poller):
|
90
88
|
_READ = select.POLLIN | select.POLLPRI | select.POLLHUP
|
91
89
|
_WRITE = select.POLLOUT
|
92
90
|
|
@@ -94,40 +92,40 @@ class PollPoller(BasePoller):
|
|
94
92
|
super().__init__()
|
95
93
|
|
96
94
|
self._poller = select.poll()
|
97
|
-
self.
|
98
|
-
self.
|
95
|
+
self._readable: set[int] = set()
|
96
|
+
self._writable: set[int] = set()
|
99
97
|
|
100
98
|
def register_readable(self, fd: int) -> None:
|
101
99
|
self._poller.register(fd, self._READ)
|
102
|
-
self.
|
100
|
+
self._readable.add(fd)
|
103
101
|
|
104
102
|
def register_writable(self, fd: int) -> None:
|
105
103
|
self._poller.register(fd, self._WRITE)
|
106
|
-
self.
|
104
|
+
self._writable.add(fd)
|
107
105
|
|
108
106
|
def unregister_readable(self, fd: int) -> None:
|
109
|
-
self.
|
107
|
+
self._readable.discard(fd)
|
110
108
|
self._poller.unregister(fd)
|
111
|
-
if fd in self.
|
109
|
+
if fd in self._writable:
|
112
110
|
self._poller.register(fd, self._WRITE)
|
113
111
|
|
114
112
|
def unregister_writable(self, fd: int) -> None:
|
115
|
-
self.
|
113
|
+
self._writable.discard(fd)
|
116
114
|
self._poller.unregister(fd)
|
117
|
-
if fd in self.
|
115
|
+
if fd in self._readable:
|
118
116
|
self._poller.register(fd, self._READ)
|
119
117
|
|
120
118
|
def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
|
121
119
|
fds = self._poll_fds(timeout) # type: ignore
|
122
|
-
|
120
|
+
readable, writable = [], []
|
123
121
|
for fd, eventmask in fds:
|
124
122
|
if self._ignore_invalid(fd, eventmask):
|
125
123
|
continue
|
126
124
|
if eventmask & self._READ:
|
127
|
-
|
125
|
+
readable.append(fd)
|
128
126
|
if eventmask & self._WRITE:
|
129
|
-
|
130
|
-
return
|
127
|
+
writable.append(fd)
|
128
|
+
return readable, writable
|
131
129
|
|
132
130
|
def _poll_fds(self, timeout: float) -> ta.List[ta.Tuple[int, int]]:
|
133
131
|
try:
|
@@ -143,41 +141,41 @@ class PollPoller(BasePoller):
|
|
143
141
|
# POLLNVAL means `fd` value is invalid, not open. When a process quits it's `fd`s are closed so there is no
|
144
142
|
# more reason to keep this `fd` registered If the process restarts it's `fd`s are registered again.
|
145
143
|
self._poller.unregister(fd)
|
146
|
-
self.
|
147
|
-
self.
|
144
|
+
self._readable.discard(fd)
|
145
|
+
self._writable.discard(fd)
|
148
146
|
return True
|
149
147
|
return False
|
150
148
|
|
151
149
|
|
152
150
|
if sys.platform == 'darwin' or sys.platform.startswith('freebsd'):
|
153
|
-
class KqueuePoller(
|
151
|
+
class KqueuePoller(Poller):
|
154
152
|
max_events = 1000
|
155
153
|
|
156
154
|
def __init__(self) -> None:
|
157
155
|
super().__init__()
|
158
156
|
|
159
157
|
self._kqueue: ta.Optional[ta.Any] = select.kqueue()
|
160
|
-
self.
|
161
|
-
self.
|
158
|
+
self._readable: set[int] = set()
|
159
|
+
self._writable: set[int] = set()
|
162
160
|
|
163
161
|
def register_readable(self, fd: int) -> None:
|
164
|
-
self.
|
162
|
+
self._readable.add(fd)
|
165
163
|
kevent = select.kevent(fd, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD)
|
166
164
|
self._kqueue_control(fd, kevent)
|
167
165
|
|
168
166
|
def register_writable(self, fd: int) -> None:
|
169
|
-
self.
|
167
|
+
self._writable.add(fd)
|
170
168
|
kevent = select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_ADD)
|
171
169
|
self._kqueue_control(fd, kevent)
|
172
170
|
|
173
171
|
def unregister_readable(self, fd: int) -> None:
|
174
172
|
kevent = select.kevent(fd, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_DELETE)
|
175
|
-
self.
|
173
|
+
self._readable.discard(fd)
|
176
174
|
self._kqueue_control(fd, kevent)
|
177
175
|
|
178
176
|
def unregister_writable(self, fd: int) -> None:
|
179
177
|
kevent = select.kevent(fd, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_DELETE)
|
180
|
-
self.
|
178
|
+
self._writable.discard(fd)
|
181
179
|
self._kqueue_control(fd, kevent)
|
182
180
|
|
183
181
|
def _kqueue_control(self, fd: int, kevent: 'select.kevent') -> None:
|
@@ -190,32 +188,32 @@ if sys.platform == 'darwin' or sys.platform.startswith('freebsd'):
|
|
190
188
|
raise
|
191
189
|
|
192
190
|
def poll(self, timeout: ta.Optional[float]) -> ta.Tuple[ta.List[int], ta.List[int]]:
|
193
|
-
|
191
|
+
readable, writable = [], [] # type: ignore
|
194
192
|
|
195
193
|
try:
|
196
194
|
kevents = self._kqueue.control(None, self.max_events, timeout) # type: ignore
|
197
195
|
except OSError as error:
|
198
196
|
if error.errno == errno.EINTR:
|
199
197
|
log.debug('EINTR encountered in poll')
|
200
|
-
return
|
198
|
+
return readable, writable
|
201
199
|
raise
|
202
200
|
|
203
201
|
for kevent in kevents:
|
204
202
|
if kevent.filter == select.KQ_FILTER_READ:
|
205
|
-
|
203
|
+
readable.append(kevent.ident)
|
206
204
|
if kevent.filter == select.KQ_FILTER_WRITE:
|
207
|
-
|
205
|
+
writable.append(kevent.ident)
|
208
206
|
|
209
|
-
return
|
207
|
+
return readable, writable
|
210
208
|
|
211
209
|
def before_daemonize(self) -> None:
|
212
210
|
self.close()
|
213
211
|
|
214
212
|
def after_daemonize(self) -> None:
|
215
213
|
self._kqueue = select.kqueue()
|
216
|
-
for fd in self.
|
214
|
+
for fd in self._readable:
|
217
215
|
self.register_readable(fd)
|
218
|
-
for fd in self.
|
216
|
+
for fd in self._writable:
|
219
217
|
self.register_writable(fd)
|
220
218
|
|
221
219
|
def close(self) -> None:
|
@@ -226,13 +224,13 @@ else:
|
|
226
224
|
KqueuePoller = None
|
227
225
|
|
228
226
|
|
229
|
-
|
230
|
-
if (
|
231
|
-
|
232
|
-
|
233
|
-
):
|
234
|
-
|
235
|
-
elif hasattr(select, 'poll'):
|
236
|
-
|
237
|
-
else:
|
238
|
-
|
227
|
+
def get_poller_impl() -> ta.Type[Poller]:
|
228
|
+
if (
|
229
|
+
sys.platform == 'darwin' or sys.platform.startswith('freebsd') and
|
230
|
+
hasattr(select, 'kqueue') and KqueuePoller is not None
|
231
|
+
):
|
232
|
+
return KqueuePoller
|
233
|
+
elif hasattr(select, 'poll'):
|
234
|
+
return PollPoller
|
235
|
+
else:
|
236
|
+
return SelectPoller
|