ominfra 0.0.0.dev121__py3-none-any.whl → 0.0.0.dev123__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/deploy/_executor.py +10 -1
- ominfra/journald/messages.py +1 -1
- ominfra/pyremote/_runcommands.py +10 -1
- ominfra/scripts/journald2aws.py +10 -2
- ominfra/scripts/supervisor.py +2227 -2043
- 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.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/RECORD +23 -21
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev121.dist-info → ominfra-0.0.0.dev123.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev121.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
|