moat-kv 0.71.0__py3-none-any.whl → 0.71.7__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.
- moat/kv/__init__.py +6 -7
- moat/kv/_cfg.yaml +3 -2
- moat/kv/actor/__init__.py +2 -1
- moat/kv/actor/deletor.py +4 -1
- moat/kv/auth/__init__.py +12 -13
- moat/kv/auth/_test.py +4 -1
- moat/kv/auth/password.py +11 -7
- moat/kv/backend/mqtt.py +4 -5
- moat/kv/client.py +20 -39
- moat/kv/code.py +3 -3
- moat/kv/command/data.py +4 -3
- moat/kv/command/dump/__init__.py +36 -34
- moat/kv/command/internal.py +2 -3
- moat/kv/command/job.py +1 -2
- moat/kv/command/type.py +3 -6
- moat/kv/data.py +9 -8
- moat/kv/errors.py +16 -8
- moat/kv/mock/__init__.py +2 -12
- moat/kv/model.py +29 -33
- moat/kv/obj/__init__.py +3 -3
- moat/kv/obj/command.py +3 -3
- moat/kv/runner.py +4 -5
- moat/kv/server.py +106 -126
- moat/kv/types.py +10 -12
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/METADATA +6 -2
- moat_kv-0.71.7.dist-info/RECORD +47 -0
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/WHEEL +1 -1
- moat_kv-0.71.7.dist-info/licenses/LICENSE +3 -0
- moat_kv-0.71.7.dist-info/licenses/LICENSE.APACHE2 +202 -0
- moat_kv-0.71.7.dist-info/licenses/LICENSE.MIT +20 -0
- moat_kv-0.71.7.dist-info/top_level.txt +1 -0
- build/lib/docs/source/conf.py +0 -201
- build/lib/examples/pathify.py +0 -45
- build/lib/moat/kv/__init__.py +0 -19
- build/lib/moat/kv/_cfg.yaml +0 -93
- build/lib/moat/kv/_main.py +0 -91
- build/lib/moat/kv/actor/__init__.py +0 -98
- build/lib/moat/kv/actor/deletor.py +0 -139
- build/lib/moat/kv/auth/__init__.py +0 -444
- build/lib/moat/kv/auth/_test.py +0 -166
- build/lib/moat/kv/auth/password.py +0 -234
- build/lib/moat/kv/auth/root.py +0 -58
- build/lib/moat/kv/backend/__init__.py +0 -67
- build/lib/moat/kv/backend/mqtt.py +0 -71
- build/lib/moat/kv/client.py +0 -1025
- build/lib/moat/kv/code.py +0 -236
- build/lib/moat/kv/codec.py +0 -11
- build/lib/moat/kv/command/__init__.py +0 -1
- build/lib/moat/kv/command/acl.py +0 -180
- build/lib/moat/kv/command/auth.py +0 -261
- build/lib/moat/kv/command/code.py +0 -293
- build/lib/moat/kv/command/codec.py +0 -186
- build/lib/moat/kv/command/data.py +0 -265
- build/lib/moat/kv/command/dump/__init__.py +0 -143
- build/lib/moat/kv/command/error.py +0 -149
- build/lib/moat/kv/command/internal.py +0 -248
- build/lib/moat/kv/command/job.py +0 -433
- build/lib/moat/kv/command/log.py +0 -53
- build/lib/moat/kv/command/server.py +0 -114
- build/lib/moat/kv/command/type.py +0 -201
- build/lib/moat/kv/config.py +0 -46
- build/lib/moat/kv/data.py +0 -216
- build/lib/moat/kv/errors.py +0 -561
- build/lib/moat/kv/exceptions.py +0 -126
- build/lib/moat/kv/mock/__init__.py +0 -101
- build/lib/moat/kv/mock/mqtt.py +0 -159
- build/lib/moat/kv/mock/tracer.py +0 -63
- build/lib/moat/kv/model.py +0 -1069
- build/lib/moat/kv/obj/__init__.py +0 -646
- build/lib/moat/kv/obj/command.py +0 -241
- build/lib/moat/kv/runner.py +0 -1347
- build/lib/moat/kv/server.py +0 -2809
- build/lib/moat/kv/types.py +0 -513
- ci/rtd-requirements.txt +0 -4
- ci/test-requirements.txt +0 -7
- ci/travis.sh +0 -96
- debian/.gitignore +0 -7
- debian/changelog +0 -1435
- debian/control +0 -43
- debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +0 -201
- debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +0 -45
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +0 -19
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +0 -93
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +0 -91
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +0 -98
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +0 -139
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +0 -444
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +0 -166
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +0 -234
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +0 -58
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +0 -67
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +0 -71
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +0 -1025
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +0 -236
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +0 -11
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +0 -1
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +0 -180
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +0 -261
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +0 -293
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +0 -186
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +0 -265
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +0 -143
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +0 -149
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +0 -248
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +0 -433
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +0 -53
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +0 -114
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +0 -201
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +0 -46
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +0 -216
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +0 -561
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +0 -126
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +0 -101
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +0 -159
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +0 -63
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +0 -1069
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +0 -646
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +0 -241
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +0 -1347
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +0 -2809
- debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +0 -513
- debian/moat-kv.postinst +0 -3
- debian/rules +0 -20
- debian/source/format +0 -1
- debian/watch +0 -4
- docs/Makefile +0 -20
- docs/make.bat +0 -36
- docs/source/TODO.rst +0 -61
- docs/source/_static/.gitkeep +0 -0
- docs/source/acls.rst +0 -80
- docs/source/auth.rst +0 -84
- docs/source/client_protocol.rst +0 -456
- docs/source/code.rst +0 -341
- docs/source/command_line.rst +0 -1187
- docs/source/common_protocol.rst +0 -47
- docs/source/conf.py +0 -201
- docs/source/debugging.rst +0 -70
- docs/source/extend.rst +0 -37
- docs/source/history.rst +0 -36
- docs/source/index.rst +0 -75
- docs/source/model.rst +0 -54
- docs/source/overview.rst +0 -83
- docs/source/related.rst +0 -89
- docs/source/server_protocol.rst +0 -450
- docs/source/startup.rst +0 -31
- docs/source/translator.rst +0 -244
- docs/source/tutorial.rst +0 -711
- docs/source/v3.rst +0 -168
- examples/code/transform.scale.yml +0 -21
- examples/code/transform.switch.yml +0 -82
- examples/code/transform.timeslot.yml +0 -63
- examples/pathify.py +0 -45
- moat/kv/codec.py +0 -11
- moat_kv-0.71.0.dist-info/RECORD +0 -188
- moat_kv-0.71.0.dist-info/top_level.txt +0 -9
- scripts/current +0 -15
- scripts/env +0 -8
- scripts/init +0 -39
- scripts/recover +0 -17
- scripts/rotate +0 -33
- scripts/run +0 -29
- scripts/run-all +0 -10
- scripts/run-any +0 -10
- scripts/run-single +0 -15
- scripts/success +0 -4
- systemd/moat-kv-recover.service +0 -21
- systemd/moat-kv-rotate.service +0 -20
- systemd/moat-kv-rotate.timer +0 -10
- systemd/moat-kv-run-all.service +0 -26
- systemd/moat-kv-run-all@.service +0 -25
- systemd/moat-kv-run-any.service +0 -26
- systemd/moat-kv-run-any@.service +0 -25
- systemd/moat-kv-run-single.service +0 -26
- systemd/moat-kv-run-single@.service +0 -25
- systemd/moat-kv.service +0 -27
- systemd/postinst +0 -7
- systemd/sysusers +0 -3
- {moat_kv-0.71.0.dist-info → moat_kv-0.71.7.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,1347 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
This module's job is to run code, resp. to keep it running.
|
3
|
-
|
4
|
-
|
5
|
-
"""
|
6
|
-
from __future__ import annotations
|
7
|
-
|
8
|
-
import time
|
9
|
-
from collections.abc import Mapping
|
10
|
-
from contextlib import AsyncExitStack, asynccontextmanager
|
11
|
-
from weakref import ref
|
12
|
-
|
13
|
-
import anyio
|
14
|
-
import psutil
|
15
|
-
from asyncactor import AuthPingEvent, NodeList, PingEvent, TagEvent, UntagEvent
|
16
|
-
from moat.util import (
|
17
|
-
NotGiven,
|
18
|
-
P,
|
19
|
-
Path,
|
20
|
-
attrdict,
|
21
|
-
combine_dict,
|
22
|
-
create_queue,
|
23
|
-
digits,
|
24
|
-
logger_for,
|
25
|
-
spawn,
|
26
|
-
)
|
27
|
-
|
28
|
-
from .actor import (
|
29
|
-
ActorState,
|
30
|
-
BrokenState,
|
31
|
-
ClientActor,
|
32
|
-
CompleteState,
|
33
|
-
DetachedState,
|
34
|
-
PartialState,
|
35
|
-
)
|
36
|
-
from .exceptions import ServerError
|
37
|
-
from .obj import AttrClientEntry, ClientRoot, MirrorRoot
|
38
|
-
|
39
|
-
try:
|
40
|
-
ClosedResourceError = anyio.exceptions.ClosedResourceError
|
41
|
-
except AttributeError:
|
42
|
-
ClosedResourceError = anyio.ClosedResourceError
|
43
|
-
|
44
|
-
import logging
|
45
|
-
|
46
|
-
logger = logging.getLogger(__name__)
|
47
|
-
|
48
|
-
QLEN = 10
|
49
|
-
|
50
|
-
|
51
|
-
class NotSelected(RuntimeError):
|
52
|
-
"""
|
53
|
-
This node has not been selected for a very long time. Something is amiss.
|
54
|
-
"""
|
55
|
-
|
56
|
-
pass
|
57
|
-
|
58
|
-
|
59
|
-
class ErrorRecorded(RuntimeError):
|
60
|
-
pass
|
61
|
-
|
62
|
-
|
63
|
-
class RunnerMsg(ActorState):
|
64
|
-
"""Superclass for runner-generated messages.
|
65
|
-
|
66
|
-
Not directly instantiated.
|
67
|
-
|
68
|
-
This message and its descendants take one opaque parameter: ``msg``.
|
69
|
-
"""
|
70
|
-
|
71
|
-
pass
|
72
|
-
|
73
|
-
|
74
|
-
class ChangeMsg(RunnerMsg):
|
75
|
-
"""A message telling your code that some entry has been updated.
|
76
|
-
|
77
|
-
Subclass this and use it as `CallAdmin.watch`'s ``cls`` parameter for easier
|
78
|
-
disambiguation.
|
79
|
-
|
80
|
-
The runner sets ``path`` and ``value`` attributes.
|
81
|
-
"""
|
82
|
-
|
83
|
-
pass
|
84
|
-
|
85
|
-
|
86
|
-
class MQTTmsg(RunnerMsg):
|
87
|
-
"""A message transporting some MQTT data.
|
88
|
-
|
89
|
-
`value` is the MsgPack-decoded content. If that doesn't exist the
|
90
|
-
message is not decodeable.
|
91
|
-
|
92
|
-
The runner also sets the ``path`` attribute.
|
93
|
-
"""
|
94
|
-
|
95
|
-
pass
|
96
|
-
|
97
|
-
|
98
|
-
class ReadyMsg(RunnerMsg):
|
99
|
-
"""
|
100
|
-
This message is queued when the last watcher has read all data.
|
101
|
-
"""
|
102
|
-
|
103
|
-
pass
|
104
|
-
|
105
|
-
|
106
|
-
class TimerMsg(RunnerMsg):
|
107
|
-
"""
|
108
|
-
A message telling your code that a timer triggers.
|
109
|
-
|
110
|
-
Subclass this and use it as `CallAdmin.timer`'s ``cls`` parameter for easier
|
111
|
-
disambiguation.
|
112
|
-
"""
|
113
|
-
|
114
|
-
pass
|
115
|
-
|
116
|
-
|
117
|
-
_CLASSES = attrdict()
|
118
|
-
for _c in (
|
119
|
-
DetachedState,
|
120
|
-
PartialState,
|
121
|
-
CompleteState,
|
122
|
-
ActorState,
|
123
|
-
BrokenState,
|
124
|
-
TimerMsg,
|
125
|
-
ReadyMsg,
|
126
|
-
ChangeMsg,
|
127
|
-
MQTTmsg,
|
128
|
-
RunnerMsg,
|
129
|
-
ErrorRecorded,
|
130
|
-
):
|
131
|
-
_CLASSES[_c.__name__] = _c
|
132
|
-
|
133
|
-
_CLASSES["NotGiven"] = NotGiven # ellipsis
|
134
|
-
|
135
|
-
|
136
|
-
class CallAdmin:
|
137
|
-
"""
|
138
|
-
This class collects some standard tasks which async MoaT-KV-embedded
|
139
|
-
code might want to do.
|
140
|
-
"""
|
141
|
-
|
142
|
-
_taskgroup = None
|
143
|
-
_stack = None
|
144
|
-
_restart = False
|
145
|
-
_n_watch: int = None
|
146
|
-
_n_watch_seen: int = None
|
147
|
-
|
148
|
-
def __init__(self, runner, state, data):
|
149
|
-
self._runner = runner
|
150
|
-
self._state = state
|
151
|
-
self._data = data
|
152
|
-
self._client = runner.root.client
|
153
|
-
self._err = runner.root.err
|
154
|
-
self._q = runner._q
|
155
|
-
self._path = runner._path
|
156
|
-
self._subpath = runner.subpath
|
157
|
-
self._logger = runner._logger
|
158
|
-
|
159
|
-
async def _run(self, code, data):
|
160
|
-
while True:
|
161
|
-
self._n_watch = 0
|
162
|
-
self._n_watch_seen = 0
|
163
|
-
|
164
|
-
res = await self._run2(code, data)
|
165
|
-
if self._restart:
|
166
|
-
self._restart = False
|
167
|
-
else:
|
168
|
-
return res
|
169
|
-
|
170
|
-
async def _run2(self, code, data):
|
171
|
-
"""Called by the runner to actually execute the code."""
|
172
|
-
self._logger.debug("Start %s with %s", self._runner._path, self._runner.code)
|
173
|
-
async with (
|
174
|
-
anyio.create_task_group() as tg,
|
175
|
-
AsyncExitStack() as stack,
|
176
|
-
):
|
177
|
-
self._stack = stack
|
178
|
-
self._taskgroup = tg
|
179
|
-
self._runner.scope = sc = tg.cancel_scope
|
180
|
-
|
181
|
-
data["_self"] = self
|
182
|
-
|
183
|
-
oka = getattr(self._runner, "ok_after", 0)
|
184
|
-
if oka > 0:
|
185
|
-
|
186
|
-
async def is_ok(oka):
|
187
|
-
await anyio.sleep(oka)
|
188
|
-
await self.setup_done()
|
189
|
-
|
190
|
-
tg.start_soon(is_ok, oka)
|
191
|
-
tg.start_soon(self._changed_code, code)
|
192
|
-
|
193
|
-
await self._runner.send_event(ReadyMsg(0))
|
194
|
-
res = code(**data)
|
195
|
-
if code.is_async is not None:
|
196
|
-
res = await res
|
197
|
-
|
198
|
-
sc.cancel()
|
199
|
-
return res
|
200
|
-
|
201
|
-
async def _pinger(self):
|
202
|
-
t = self._runner.ok_after or 10
|
203
|
-
await anyio.sleep(t / 2)
|
204
|
-
while True:
|
205
|
-
await self._runner.state.ping()
|
206
|
-
await anyio.sleep(t * 5)
|
207
|
-
|
208
|
-
async def _changed_code(self, code):
|
209
|
-
"""
|
210
|
-
Kill the job if the underlying code has changed
|
211
|
-
"""
|
212
|
-
await code.reload_event.wait()
|
213
|
-
self._restart = True
|
214
|
-
self.cancel()
|
215
|
-
|
216
|
-
def cancel(self):
|
217
|
-
"""
|
218
|
-
Cancel the running task
|
219
|
-
"""
|
220
|
-
self._taskgroup.cancel_scope.cancel()
|
221
|
-
|
222
|
-
async def spawn(self, proc, *a, **kw):
|
223
|
-
"""
|
224
|
-
Start a background subtask.
|
225
|
-
|
226
|
-
The task is auto-cancelled when your code ends.
|
227
|
-
|
228
|
-
Returns: an `anyio.abc.CancelScope` which you can use to cancel the
|
229
|
-
subtask.
|
230
|
-
"""
|
231
|
-
return await spawn(self._taskgroup, proc, *a, **kw)
|
232
|
-
|
233
|
-
async def setup_done(self, **kw):
|
234
|
-
"""
|
235
|
-
Call this when your code has successfully started up.
|
236
|
-
"""
|
237
|
-
self._state.backoff = 0
|
238
|
-
await self._state.save()
|
239
|
-
await self._err.record_working("run", self._runner._path, **kw)
|
240
|
-
|
241
|
-
async def error(self, path=None, **kw):
|
242
|
-
"""
|
243
|
-
Record that an error has occurred. This function records specific
|
244
|
-
error data, then raises `ErrorRecorded` which the code is not
|
245
|
-
supposed to catch.
|
246
|
-
|
247
|
-
See `moat.kv.errors.ErrorRoot.record_error` for keyword details. The
|
248
|
-
``path`` argument is auto-filled to point to the current task.
|
249
|
-
"""
|
250
|
-
if path is None:
|
251
|
-
path = self._path
|
252
|
-
r = await self._err.record_error("run", path, **kw)
|
253
|
-
await self._err.root.wait_chain(r.chain)
|
254
|
-
raise ErrorRecorded
|
255
|
-
|
256
|
-
async def open_context(self, ctx):
|
257
|
-
return await self._stack.enter_async_context(ctx)
|
258
|
-
|
259
|
-
async def watch(self, path, cls=ChangeMsg, **kw):
|
260
|
-
"""
|
261
|
-
Create a watcher. This path is monitored as per `moat.kv.client.Client.watch`;
|
262
|
-
messages are encapsulated in `ChangeMsg` objects.
|
263
|
-
A `ReadyMsg` will be sent when all watchers have transmitted their
|
264
|
-
initial state.
|
265
|
-
|
266
|
-
By default a watcher will only monitor a single entry. Set
|
267
|
-
``max_depth`` if you also want child entries.
|
268
|
-
|
269
|
-
By default a watcher will not report existing entries. Set
|
270
|
-
``fetch=True`` if you want them.
|
271
|
-
"""
|
272
|
-
|
273
|
-
class Watcher:
|
274
|
-
"""Helper class for watching an entry"""
|
275
|
-
|
276
|
-
# pylint: disable=no-self-argument
|
277
|
-
|
278
|
-
def __init__(slf, admin, runner, client, cls, path, kw):
|
279
|
-
kw.setdefault("fetch", True)
|
280
|
-
|
281
|
-
slf.admin = admin
|
282
|
-
slf.runner = runner
|
283
|
-
slf.client = client
|
284
|
-
slf.path = path
|
285
|
-
slf.kw = kw
|
286
|
-
slf.cls = cls
|
287
|
-
slf.scope = None
|
288
|
-
|
289
|
-
async def run(slf):
|
290
|
-
@asynccontextmanager
|
291
|
-
async def _watch(path, kw):
|
292
|
-
if path.mark == "r":
|
293
|
-
async with slf.client.msg_monitor(path, **kw) as watcher:
|
294
|
-
yield watcher
|
295
|
-
elif not path.mark:
|
296
|
-
async with slf.client.watch(path, **kw) as watcher:
|
297
|
-
yield watcher
|
298
|
-
else:
|
299
|
-
raise RuntimeError(f"What should I do with a path marked {path.mark!r}?")
|
300
|
-
|
301
|
-
with anyio.CancelScope() as sc:
|
302
|
-
slf.scope = sc
|
303
|
-
async with _watch(path, kw) as watcher:
|
304
|
-
async for msg in watcher:
|
305
|
-
if "path" in msg:
|
306
|
-
chg = cls(msg)
|
307
|
-
try:
|
308
|
-
chg.value = ( # pylint:disable=attribute-defined-outside-init
|
309
|
-
msg.value
|
310
|
-
)
|
311
|
-
except AttributeError:
|
312
|
-
pass
|
313
|
-
chg.path = ( # pylint:disable=attribute-defined-outside-init
|
314
|
-
msg.path
|
315
|
-
)
|
316
|
-
await slf.runner.send_event(chg)
|
317
|
-
|
318
|
-
elif msg.get("state", "") == "uptodate":
|
319
|
-
slf.admin._n_watch_seen += 1
|
320
|
-
if slf.admin._n_watch_seen == slf.admin._n_watch:
|
321
|
-
await slf.runner.send_event(ReadyMsg(slf.admin._n_watch_seen))
|
322
|
-
|
323
|
-
def cancel(slf):
|
324
|
-
if slf.scope is None:
|
325
|
-
return False
|
326
|
-
sc, slf.scope = slf.scope, None
|
327
|
-
sc.cancel()
|
328
|
-
|
329
|
-
if isinstance(path, (tuple, list)):
|
330
|
-
path = Path.build(path)
|
331
|
-
elif not isinstance(path, Path):
|
332
|
-
raise RuntimeError(f"You didn't pass in a path: {path!r}")
|
333
|
-
|
334
|
-
kw.setdefault("max_depth", 0)
|
335
|
-
if kw.setdefault("fetch", True):
|
336
|
-
self._n_watch += 1
|
337
|
-
|
338
|
-
w = Watcher(self, self._runner, self._client, cls, path, kw)
|
339
|
-
self._taskgroup.start_soon(w.run)
|
340
|
-
return w
|
341
|
-
|
342
|
-
async def send(self, path, value=NotGiven, raw=None):
|
343
|
-
"""
|
344
|
-
Publish an MQTT message.
|
345
|
-
|
346
|
-
Set either ``value`` or ``raw``.
|
347
|
-
"""
|
348
|
-
if isinstance(path, (tuple, list)):
|
349
|
-
path = Path.build(path)
|
350
|
-
elif not isinstance(path, Path):
|
351
|
-
raise RuntimeError(f"You didn't pass in a path: {path!r}")
|
352
|
-
|
353
|
-
await self._client.msg_send(topic=path, data=value, raw=raw)
|
354
|
-
|
355
|
-
async def set(self, path, value, chain=NotGiven):
|
356
|
-
"""
|
357
|
-
Set a MoaT-KV value.
|
358
|
-
"""
|
359
|
-
if path.mark == "r":
|
360
|
-
return await self.send(path, value)
|
361
|
-
elif path.mark:
|
362
|
-
raise RuntimeError(f"What should I do with a path marked {path.mark!r}")
|
363
|
-
|
364
|
-
if isinstance(path, (tuple, list)):
|
365
|
-
path = Path.build(path)
|
366
|
-
elif not isinstance(path, Path):
|
367
|
-
raise RuntimeError(f"You didn't pass in a path: {path!r}")
|
368
|
-
|
369
|
-
return await self._client.set(path, value=value, chain=chain)
|
370
|
-
|
371
|
-
async def get(self, path, value):
|
372
|
-
"""
|
373
|
-
Get a MoaT-KV value.
|
374
|
-
"""
|
375
|
-
if isinstance(path, (tuple, list)):
|
376
|
-
path = Path.build(path)
|
377
|
-
elif not isinstance(path, Path):
|
378
|
-
raise RuntimeError(f"You didn't pass in a path: {path!r}")
|
379
|
-
|
380
|
-
res = await self._client.get(path, value=value) # TODO chain
|
381
|
-
|
382
|
-
if "value" in res:
|
383
|
-
return res.value
|
384
|
-
else:
|
385
|
-
return KeyError(path)
|
386
|
-
|
387
|
-
async def monitor(self, path, cls=MQTTmsg, **kw):
|
388
|
-
"""
|
389
|
-
Create an MQTT monitor.
|
390
|
-
Messages are encapsulated in `MQTTmsg` objects.
|
391
|
-
|
392
|
-
By default a monitor will only monitor a single entry. You may use
|
393
|
-
MQTT wildcards.
|
394
|
-
|
395
|
-
The message is decoded and stored in the ``value`` attribute unless
|
396
|
-
it's either undecodeable or ``raw`` is set, in which case it's
|
397
|
-
stored in ``.msg``. The topic the message was sent to is in
|
398
|
-
``topic``.
|
399
|
-
"""
|
400
|
-
|
401
|
-
class Monitor:
|
402
|
-
"""Helper class for reading an MQTT topic"""
|
403
|
-
|
404
|
-
def __init__(self, admin, runner, client, cls, path, kw):
|
405
|
-
self.admin = admin
|
406
|
-
self.runner = runner
|
407
|
-
self.client = client
|
408
|
-
self.path = path
|
409
|
-
self.kw = kw
|
410
|
-
self.cls = cls
|
411
|
-
self.scope = None
|
412
|
-
|
413
|
-
async def run(self):
|
414
|
-
with anyio.CancelScope() as sc:
|
415
|
-
self.scope = sc
|
416
|
-
async with self.client.msg_monitor(path, **kw) as watcher:
|
417
|
-
async for msg in watcher:
|
418
|
-
if "topic" in msg:
|
419
|
-
# pylint:disable=attribute-defined-outside-init
|
420
|
-
chg = cls(msg.get("raw", None))
|
421
|
-
try:
|
422
|
-
chg.value = msg.data
|
423
|
-
except AttributeError:
|
424
|
-
pass
|
425
|
-
chg.path = Path.build(msg.topic)
|
426
|
-
await self.runner.send_event(chg)
|
427
|
-
|
428
|
-
async def cancel(self):
|
429
|
-
if self.scope is None:
|
430
|
-
return False
|
431
|
-
sc, self.scope = self.scope, None
|
432
|
-
sc.cancel()
|
433
|
-
|
434
|
-
if isinstance(path, (tuple, list)):
|
435
|
-
path = Path.build(path)
|
436
|
-
elif not isinstance(path, Path):
|
437
|
-
raise RuntimeError(f"You didn't pass in a path: {path!r}")
|
438
|
-
|
439
|
-
w = Monitor(self, self._runner, self._client, cls, path, kw)
|
440
|
-
self._taskgroup.start_soon(w.run)
|
441
|
-
return w
|
442
|
-
|
443
|
-
async def timer(self, delay, cls=TimerMsg):
|
444
|
-
class Timer:
|
445
|
-
def __init__(self, runner, cls, tg):
|
446
|
-
self.runner = runner
|
447
|
-
self.cls = cls
|
448
|
-
self.scope = None
|
449
|
-
self._taskgroup = tg
|
450
|
-
self.delay = None
|
451
|
-
|
452
|
-
async def _run(self):
|
453
|
-
with anyio.CancelScope() as sc:
|
454
|
-
self.scope = sc
|
455
|
-
await anyio.sleep(self.delay)
|
456
|
-
self.scope = None
|
457
|
-
await self.runner.send_event(self.cls(self))
|
458
|
-
|
459
|
-
def cancel(self):
|
460
|
-
if self.scope is None:
|
461
|
-
return False
|
462
|
-
sc, self.scope = self.scope, None
|
463
|
-
sc.cancel()
|
464
|
-
return True
|
465
|
-
|
466
|
-
async def run(self, delay):
|
467
|
-
self.cancel()
|
468
|
-
self.delay = delay
|
469
|
-
if self.delay > 0:
|
470
|
-
self._taskgroup.start_soon(t._run)
|
471
|
-
|
472
|
-
t = Timer(self._runner, cls, self._taskgroup)
|
473
|
-
await t.run(delay)
|
474
|
-
return t
|
475
|
-
|
476
|
-
|
477
|
-
class RunnerEntry(AttrClientEntry):
|
478
|
-
"""
|
479
|
-
An entry representing some hopefully-running code.
|
480
|
-
|
481
|
-
The code will run some time after ``target`` has passed.
|
482
|
-
On success, it will run again ``repeat`` seconds later (if >0).
|
483
|
-
On error, it will run ``delay`` seconds later (if >0), multiplied by 2**backoff.
|
484
|
-
|
485
|
-
Arguments:
|
486
|
-
code (list): pointer to the code that's to be started.
|
487
|
-
data (dict): additional data for the code.
|
488
|
-
delay (float): time before restarting the job on error.
|
489
|
-
Default 100.
|
490
|
-
repeat (float): time before restarting on success.
|
491
|
-
Default: zero: no restart.
|
492
|
-
target (float): time the job should be started at.
|
493
|
-
Default: zero: don't start.
|
494
|
-
ok_after (float): the job is marked OK if it has run this long.
|
495
|
-
Default: zero: the code will do that itself.
|
496
|
-
backoff (float): Exponential back-off factor on errors.
|
497
|
-
Default: 1.1.
|
498
|
-
|
499
|
-
The code runs with these additional keywords::
|
500
|
-
|
501
|
-
_self: the `CallEnv` object, which the task can use to actually do things.
|
502
|
-
_client: the MoaT-KV client connection.
|
503
|
-
_info: a queue which the task can use to receive events. A message of
|
504
|
-
``None`` signals that the queue was overflowing and no further
|
505
|
-
messages will be delivered. Your task should use that as its
|
506
|
-
mainloop.
|
507
|
-
_P: build a path from a string
|
508
|
-
_Path: build a path from its arguments
|
509
|
-
|
510
|
-
Some possible messages are defined in :mod:`moat.kv.actor`.
|
511
|
-
"""
|
512
|
-
|
513
|
-
ATTRS = "code data delay ok_after repeat backoff target".split()
|
514
|
-
|
515
|
-
delay = 100 # timedelta, before restarting
|
516
|
-
repeat = 0
|
517
|
-
target = 0
|
518
|
-
backoff = 1.1
|
519
|
-
ok_after = 0
|
520
|
-
|
521
|
-
code = () # what to execute
|
522
|
-
data = None
|
523
|
-
scope = None # scope to kill off
|
524
|
-
_comment = None # used for error entries, i.e. mainly Cancel
|
525
|
-
_q = None # send events to the running task. Async tasks only.
|
526
|
-
_running = False # .run is active. Careful with applying updates.
|
527
|
-
_task = None
|
528
|
-
retry = None
|
529
|
-
|
530
|
-
def __init__(self, *a, **k):
|
531
|
-
self.data = {} # local data
|
532
|
-
|
533
|
-
super().__init__(*a, **k)
|
534
|
-
|
535
|
-
self._logger = logger_for(self._path)
|
536
|
-
|
537
|
-
def __repr__(self):
|
538
|
-
return "<%s %r:%r>" % (self.__class__.__name__, self.subpath, self.code)
|
539
|
-
|
540
|
-
@property
|
541
|
-
def state(self):
|
542
|
-
return self.root.state.follow(self.subpath, create=None)
|
543
|
-
|
544
|
-
async def run(self):
|
545
|
-
if self.code is None:
|
546
|
-
return # nothing to do here
|
547
|
-
|
548
|
-
state = self.state
|
549
|
-
try:
|
550
|
-
self._running = True
|
551
|
-
try:
|
552
|
-
self._logger.debug("Start")
|
553
|
-
if state.node is not None:
|
554
|
-
raise RuntimeError(f"already running on {state.node}")
|
555
|
-
code = self.root.code.follow(self.code, create=False)
|
556
|
-
data = combine_dict(
|
557
|
-
self.data or {},
|
558
|
-
{} if code.value is NotGiven else code.value.get("default", {}),
|
559
|
-
deep=True,
|
560
|
-
)
|
561
|
-
|
562
|
-
if code.is_async:
|
563
|
-
data["_info"] = self._q = create_queue(QLEN)
|
564
|
-
data["_client"] = self.root.client
|
565
|
-
data["_cfg"] = self.root.client._cfg
|
566
|
-
data["_cls"] = _CLASSES
|
567
|
-
data["_P"] = P
|
568
|
-
data["_Path"] = Path
|
569
|
-
data["_log"] = self._logger
|
570
|
-
data["_digits"] = digits
|
571
|
-
|
572
|
-
state.started = time.time()
|
573
|
-
state.node = state.root.name
|
574
|
-
|
575
|
-
await state.save(wait=True)
|
576
|
-
if state.node != state.root.name:
|
577
|
-
raise RuntimeError("Rudely taken away from us.", state.node, state.root.name)
|
578
|
-
|
579
|
-
data["_self"] = calls = CallAdmin(self, state, data)
|
580
|
-
res = await calls._run(code, data)
|
581
|
-
|
582
|
-
except BaseException as exc:
|
583
|
-
self._logger.info("Error: %r", exc)
|
584
|
-
raise
|
585
|
-
else:
|
586
|
-
self._logger.debug("End")
|
587
|
-
finally:
|
588
|
-
self.scope = None
|
589
|
-
self._q = None
|
590
|
-
t = time.time()
|
591
|
-
|
592
|
-
except ErrorRecorded:
|
593
|
-
# record_error() has already been called
|
594
|
-
self._comment = None
|
595
|
-
state.backoff = min(state.backoff + 1, 20)
|
596
|
-
|
597
|
-
except BaseException as exc:
|
598
|
-
c, self._comment = self._comment, None
|
599
|
-
with anyio.move_on_after(5, shield=True):
|
600
|
-
r = await self.root.err.record_error(
|
601
|
-
"run",
|
602
|
-
self._path,
|
603
|
-
exc=exc,
|
604
|
-
data=self.data,
|
605
|
-
comment=c,
|
606
|
-
)
|
607
|
-
if r is not None:
|
608
|
-
await self.root.err.wait_chain(r.chain)
|
609
|
-
if isinstance(exc, Exception):
|
610
|
-
state.backoff = min(state.backoff + 1, 20)
|
611
|
-
else:
|
612
|
-
raise
|
613
|
-
|
614
|
-
else:
|
615
|
-
state.result = res
|
616
|
-
state.backoff = 0
|
617
|
-
await self.root.err.record_working("run", self._path)
|
618
|
-
|
619
|
-
finally:
|
620
|
-
with anyio.fail_after(2, shield=True):
|
621
|
-
if state.node == state.root.name:
|
622
|
-
state.node = None
|
623
|
-
self._running = False
|
624
|
-
state.stopped = t
|
625
|
-
|
626
|
-
if state.backoff > 0:
|
627
|
-
self.retry = t + (self.backoff**state.backoff) * self.delay
|
628
|
-
else:
|
629
|
-
self.retry = None
|
630
|
-
|
631
|
-
try:
|
632
|
-
await state.save()
|
633
|
-
except ClosedResourceError:
|
634
|
-
pass
|
635
|
-
except ServerError:
|
636
|
-
logger.exception("Could not save")
|
637
|
-
|
638
|
-
await self.root.trigger_rescan()
|
639
|
-
|
640
|
-
async def send_event(self, evt):
|
641
|
-
"""Send an event to the running process."""
|
642
|
-
if self._q is None:
|
643
|
-
if self._running:
|
644
|
-
self._logger.info("Discarding %r", evt)
|
645
|
-
elif self._q.qsize() < QLEN - 1:
|
646
|
-
self._logger.debug("Event: %r", evt)
|
647
|
-
await self._q.put(evt)
|
648
|
-
elif self._q.qsize() == QLEN - 1:
|
649
|
-
self._logger.warning("Queue full")
|
650
|
-
await self._q.put(None)
|
651
|
-
self._q = None
|
652
|
-
self._logger.info("Discarding %r", evt)
|
653
|
-
|
654
|
-
async def seems_down(self):
|
655
|
-
state = self.state
|
656
|
-
state.node = None
|
657
|
-
if not self._running:
|
658
|
-
await state.save(wait=True)
|
659
|
-
|
660
|
-
async def set_value(self, value):
|
661
|
-
"""Process incoming value changes"""
|
662
|
-
c = self.code
|
663
|
-
await super().set_value(value)
|
664
|
-
|
665
|
-
# Check whether running code needs to be killed off
|
666
|
-
if self.scope is not None:
|
667
|
-
if value is NotGiven or c is not self.code:
|
668
|
-
# The code changed.
|
669
|
-
self._comment = "Cancel: Code changed"
|
670
|
-
self.scope.cancel()
|
671
|
-
elif not getattr(self, "target", None):
|
672
|
-
self._comment = "Cancel: target zeroed"
|
673
|
-
self.scope.cancel()
|
674
|
-
|
675
|
-
await self.root.trigger_rescan()
|
676
|
-
|
677
|
-
async def seen_value(self):
|
678
|
-
await super().seen_value()
|
679
|
-
await self.root.trigger_rescan()
|
680
|
-
|
681
|
-
async def run_at(self, t: float):
|
682
|
-
"""Next run at this time."""
|
683
|
-
self.target = t
|
684
|
-
if not self._running:
|
685
|
-
await self.save()
|
686
|
-
|
687
|
-
def should_start(self):
|
688
|
-
"""Tell whether this job might want to be started.
|
689
|
-
|
690
|
-
Returns:
|
691
|
-
``False``: No, it's running (or has run and doesn't restart).
|
692
|
-
``0``: No, it should not start
|
693
|
-
``>0``: timestamp at which it should start, or should have started
|
694
|
-
|
695
|
-
"""
|
696
|
-
|
697
|
-
state = self.state
|
698
|
-
if self.code is None:
|
699
|
-
return False, "no code"
|
700
|
-
if state.node is not None:
|
701
|
-
# not our responsibility!
|
702
|
-
return None, "node set"
|
703
|
-
if state.started and not state.stopped:
|
704
|
-
raise RuntimeError("Running! should not be called")
|
705
|
-
|
706
|
-
if self.target is None:
|
707
|
-
return False, "no target"
|
708
|
-
elif self.target > state.started:
|
709
|
-
return self.target, "target > started"
|
710
|
-
elif state.backoff:
|
711
|
-
return state.stopped + self.delay * (self.backoff**state.backoff), "backoff"
|
712
|
-
elif self.repeat:
|
713
|
-
return state.stopped + self.repeat, "repeat"
|
714
|
-
elif state.started and state.started > state.stopped:
|
715
|
-
return False, "is started"
|
716
|
-
else:
|
717
|
-
return 0, "no target"
|
718
|
-
|
719
|
-
def __hash__(self):
|
720
|
-
return hash(self.subpath)
|
721
|
-
|
722
|
-
def __eq__(self, other):
|
723
|
-
other = getattr(other, "subpath", other)
|
724
|
-
return self.subpath == other
|
725
|
-
|
726
|
-
def __lt__(self, other):
|
727
|
-
other = getattr(other, "subpath", other)
|
728
|
-
return self.subpath < other
|
729
|
-
|
730
|
-
@property
|
731
|
-
def age(self):
|
732
|
-
return time.time() - self.state.started
|
733
|
-
|
734
|
-
|
735
|
-
class RunnerNode:
|
736
|
-
"""
|
737
|
-
Represents all nodes in this runner group.
|
738
|
-
|
739
|
-
This is used for load balancing and such. TODO.
|
740
|
-
"""
|
741
|
-
|
742
|
-
seen = 0
|
743
|
-
load = 999
|
744
|
-
|
745
|
-
def __new__(cls, root, name):
|
746
|
-
try:
|
747
|
-
self = root._nodes[name]
|
748
|
-
except KeyError:
|
749
|
-
self = object.__new__(cls)
|
750
|
-
self.root = root
|
751
|
-
self.name = name
|
752
|
-
root._nodes[name] = self
|
753
|
-
return self
|
754
|
-
|
755
|
-
def __init__(self, *a, **k):
|
756
|
-
pass
|
757
|
-
|
758
|
-
|
759
|
-
class StateEntry(AttrClientEntry):
|
760
|
-
"""
|
761
|
-
This is the actual state associated with a RunnerEntry.
|
762
|
-
It must only be managed by the node that actually runs the code.
|
763
|
-
|
764
|
-
Arguments:
|
765
|
-
started (float): timestamp when the job was last started
|
766
|
-
stopped (float): timestamp when the job last terminated
|
767
|
-
pinged (float): timestamp when the state was last verified by the runner
|
768
|
-
result (Any): the code's return value
|
769
|
-
node (str): the node running this code
|
770
|
-
backoff (float): on error, the multiplier to apply to the restart timeout
|
771
|
-
computed (float): computed start time
|
772
|
-
reason (str): reason why (not) starting
|
773
|
-
"""
|
774
|
-
|
775
|
-
ATTRS = "started stopped pinged computed reason result node backoff".split()
|
776
|
-
|
777
|
-
started = 0 # timestamp
|
778
|
-
stopped = 0 # timestamp
|
779
|
-
pinged = 0 # timestamp
|
780
|
-
node = None # on which the code is currently running
|
781
|
-
result = NotGiven
|
782
|
-
backoff = 0
|
783
|
-
computed = 0 # timestamp
|
784
|
-
reason = ""
|
785
|
-
|
786
|
-
@property
|
787
|
-
def runner(self):
|
788
|
-
return self.root.runner.follow(self.subpath, create=False)
|
789
|
-
|
790
|
-
async def startup(self):
|
791
|
-
try:
|
792
|
-
self.runner
|
793
|
-
except KeyError:
|
794
|
-
# The code entry doesn't exist any more.
|
795
|
-
await self.delete()
|
796
|
-
return
|
797
|
-
|
798
|
-
if self.node is None:
|
799
|
-
return
|
800
|
-
if self.node != self.root.name:
|
801
|
-
self.root.runner.get_node(self.node)
|
802
|
-
return
|
803
|
-
|
804
|
-
self.stopped = time.time()
|
805
|
-
self.node = None
|
806
|
-
self.backoff = min(20, self.backoff + 1)
|
807
|
-
await self.root.runner.err.record_error(
|
808
|
-
"run",
|
809
|
-
self.runner._path,
|
810
|
-
message="Runner restarted",
|
811
|
-
)
|
812
|
-
await self.save()
|
813
|
-
|
814
|
-
async def ping(self):
|
815
|
-
if self.node != self.root.name:
|
816
|
-
raise RuntimeError("Not our state!")
|
817
|
-
self.pinged = time.time()
|
818
|
-
await self.save()
|
819
|
-
|
820
|
-
async def stale(self):
|
821
|
-
self.stopped = time.time()
|
822
|
-
node, self.node = self.node, None
|
823
|
-
self.backoff = min(20, self.backoff + 2)
|
824
|
-
await self.root.runner.err.record_error(
|
825
|
-
"run",
|
826
|
-
self.runner._path,
|
827
|
-
message="Runner killed: {node} {state}",
|
828
|
-
data={"node": node, "state": "offline" if self.stopped else "stale"},
|
829
|
-
)
|
830
|
-
await self.save()
|
831
|
-
|
832
|
-
async def set_value(self, value):
|
833
|
-
n = self.node
|
834
|
-
|
835
|
-
await super().set_value(value)
|
836
|
-
if not self.root.runner_ready:
|
837
|
-
return
|
838
|
-
try:
|
839
|
-
run = self.runner
|
840
|
-
except KeyError:
|
841
|
-
return
|
842
|
-
|
843
|
-
# side effect: add to the global node list
|
844
|
-
if n is not None:
|
845
|
-
run.root.get_node(n)
|
846
|
-
|
847
|
-
# Check whether running code needs to be killed off
|
848
|
-
if run.scope is None:
|
849
|
-
return
|
850
|
-
if self.node is not None and self.node == n:
|
851
|
-
# Nothing changed.
|
852
|
-
return
|
853
|
-
elif self.node is None or n == self.root.runner.name:
|
854
|
-
# Owch. Our job got taken away from us.
|
855
|
-
run._comment = f"Cancel: Node set to {self.node!r}"
|
856
|
-
run.scope.cancel()
|
857
|
-
elif n is not None:
|
858
|
-
logger.warning(
|
859
|
-
"Runner %s at %r: running but node is %s",
|
860
|
-
self.root.name,
|
861
|
-
self.subpath,
|
862
|
-
n,
|
863
|
-
)
|
864
|
-
|
865
|
-
await run.root.trigger_rescan()
|
866
|
-
|
867
|
-
|
868
|
-
class StateRoot(MirrorRoot):
|
869
|
-
"""Base class for handling the state of entries.
|
870
|
-
|
871
|
-
This is separate from the RunnerRoot hierarchy because the latter may
|
872
|
-
be changed by anybody while this subtree may only be affected by the
|
873
|
-
actual runner. Otherwise we get interesting race conditions.
|
874
|
-
"""
|
875
|
-
|
876
|
-
_runner = None
|
877
|
-
|
878
|
-
@classmethod
|
879
|
-
def child_type(cls, name):
|
880
|
-
return StateEntry
|
881
|
-
|
882
|
-
@property
|
883
|
-
def name(self):
|
884
|
-
return self.client.client_name
|
885
|
-
|
886
|
-
@property
|
887
|
-
def runner(self):
|
888
|
-
return self._runner()
|
889
|
-
|
890
|
-
@property
|
891
|
-
def runner_ready(self):
|
892
|
-
r = self._runner
|
893
|
-
if r is None:
|
894
|
-
return False
|
895
|
-
r = r()
|
896
|
-
if r is None:
|
897
|
-
return False
|
898
|
-
return r.ready
|
899
|
-
|
900
|
-
def set_runner(self, runner):
|
901
|
-
self._runner = ref(runner)
|
902
|
-
|
903
|
-
async def runner_running(self):
|
904
|
-
for n in self.all_children:
|
905
|
-
await n.startup()
|
906
|
-
|
907
|
-
async def kill_stale_nodes(self, names):
|
908
|
-
"""States with node names in the "names" set are stale. Kill them."""
|
909
|
-
for s in self.all_children:
|
910
|
-
if s.node in names:
|
911
|
-
await s.stale()
|
912
|
-
|
913
|
-
_last_t = 0
|
914
|
-
|
915
|
-
async def ping(self):
|
916
|
-
t = time.time()
|
917
|
-
if t - self._last_t >= abs(self._cfg["ping"]):
|
918
|
-
self._last_t = t
|
919
|
-
if self._cfg["ping"] > 0:
|
920
|
-
val = self.value_or({}, Mapping)
|
921
|
-
val["alive"] = t
|
922
|
-
await self.update(val)
|
923
|
-
else:
|
924
|
-
await self.client.msg_send(
|
925
|
-
"run",
|
926
|
-
{"group": self.runner.group, "time": t, "node": self.name},
|
927
|
-
)
|
928
|
-
|
929
|
-
|
930
|
-
class _BaseRunnerRoot(ClientRoot):
|
931
|
-
"""common code for AnyRunnerRoot and SingleRunnerRoot"""
|
932
|
-
|
933
|
-
_trigger: anyio.abc.Event = None
|
934
|
-
_run_now_task: anyio.abc.CancelScope = None
|
935
|
-
_tagged: bool = True
|
936
|
-
|
937
|
-
err = None
|
938
|
-
code = None
|
939
|
-
ready = False
|
940
|
-
_run_next = 0
|
941
|
-
node_history = None
|
942
|
-
_start_delay = None
|
943
|
-
state = None
|
944
|
-
_act = None
|
945
|
-
|
946
|
-
CFG = "runner"
|
947
|
-
SUB = None
|
948
|
-
|
949
|
-
def __init__(self, *a, _subpath, err=None, code=None, nodes=0, **kw):
|
950
|
-
super().__init__(*a, **kw)
|
951
|
-
self.err = err
|
952
|
-
self.code = code
|
953
|
-
self._nodes = {}
|
954
|
-
self.n_nodes = nodes
|
955
|
-
self._trigger = anyio.Event()
|
956
|
-
self._x_subpath = _subpath
|
957
|
-
|
958
|
-
@classmethod
|
959
|
-
def child_type(cls, name):
|
960
|
-
return RunnerEntry
|
961
|
-
|
962
|
-
@classmethod
|
963
|
-
async def as_handler(cls, client, subpath, cfg=None, **kw): # pylint: disable=arguments-differ
|
964
|
-
assert cls.SUB is not None
|
965
|
-
if cfg is None:
|
966
|
-
cfg_ = client._cfg["runner"]
|
967
|
-
else:
|
968
|
-
cfg_ = combine_dict(cfg, client._cfg["runner"])
|
969
|
-
return await super().as_handler(client, subpath=subpath, _subpath=subpath, cfg=cfg_, **kw)
|
970
|
-
|
971
|
-
async def run_starting(self):
|
972
|
-
from .code import CodeRoot
|
973
|
-
from .errors import ErrorRoot
|
974
|
-
|
975
|
-
if self.err is None:
|
976
|
-
self.err = await ErrorRoot.as_handler(self.client)
|
977
|
-
if self.code is None:
|
978
|
-
self.code = await CodeRoot.as_handler(self.client)
|
979
|
-
|
980
|
-
await self._state_runner()
|
981
|
-
self.state.set_runner(self)
|
982
|
-
|
983
|
-
self.node_history = NodeList(0)
|
984
|
-
self._start_delay = self._cfg["start_delay"]
|
985
|
-
|
986
|
-
await super().run_starting()
|
987
|
-
|
988
|
-
async def _state_runner(self):
|
989
|
-
self.state = await StateRoot.as_handler(
|
990
|
-
self.client,
|
991
|
-
cfg=self._cfg,
|
992
|
-
subpath=self._x_subpath,
|
993
|
-
key="state",
|
994
|
-
)
|
995
|
-
|
996
|
-
@property
|
997
|
-
def name(self):
|
998
|
-
"""my node name"""
|
999
|
-
return self.client.client_name
|
1000
|
-
|
1001
|
-
def get_node(self, name): # pylint: disable=unused-argument
|
1002
|
-
"""
|
1003
|
-
If the runner keeps track of "foreign" nodes, allocate them
|
1004
|
-
"""
|
1005
|
-
return None
|
1006
|
-
|
1007
|
-
async def running(self):
|
1008
|
-
self._tg.start_soon(self._run_actor)
|
1009
|
-
|
1010
|
-
# the next block needs to be atomic
|
1011
|
-
self.ready = True
|
1012
|
-
|
1013
|
-
await self.state.runner_running()
|
1014
|
-
await super().running()
|
1015
|
-
|
1016
|
-
async def _run_actor(self):
|
1017
|
-
"""
|
1018
|
-
This method is started as a long-lived task of the root as soon as
|
1019
|
-
the subtree's data are loaded.
|
1020
|
-
|
1021
|
-
Its job is to control which tasks are started.
|
1022
|
-
"""
|
1023
|
-
raise RuntimeError("You want to override me.")
|
1024
|
-
|
1025
|
-
async def trigger_rescan(self):
|
1026
|
-
"""Tell the _run_actor task to rescan our job list prematurely"""
|
1027
|
-
if self._trigger is not None:
|
1028
|
-
self._trigger.set()
|
1029
|
-
|
1030
|
-
async def _run_now(self, evt=None):
|
1031
|
-
t_next = self._run_next
|
1032
|
-
with anyio.CancelScope() as sc:
|
1033
|
-
self._run_now_task = sc
|
1034
|
-
if evt is not None:
|
1035
|
-
evt.set()
|
1036
|
-
t = time.time()
|
1037
|
-
while True:
|
1038
|
-
if t_next > t:
|
1039
|
-
with anyio.move_on_after(t_next - t):
|
1040
|
-
await self._trigger.wait()
|
1041
|
-
self._trigger = anyio.Event()
|
1042
|
-
|
1043
|
-
t = time.time()
|
1044
|
-
t_next = t + 999
|
1045
|
-
for j in self.all_children:
|
1046
|
-
d, r = j.should_start()
|
1047
|
-
if d is None:
|
1048
|
-
continue
|
1049
|
-
if not d or d > t:
|
1050
|
-
j.state.computed = d
|
1051
|
-
j.state.reason = r
|
1052
|
-
if self._tagged:
|
1053
|
-
try:
|
1054
|
-
await j.state.save()
|
1055
|
-
except anyio.ClosedResourceError: # owch
|
1056
|
-
logger.error("Could not update state %r %r", j, j.state)
|
1057
|
-
return
|
1058
|
-
if d and t_next > d:
|
1059
|
-
t_next = d
|
1060
|
-
continue
|
1061
|
-
self._tg.start_soon(j.run)
|
1062
|
-
await anyio.sleep(self._start_delay)
|
1063
|
-
|
1064
|
-
async def notify_actor_state(self, msg=None):
|
1065
|
-
"""
|
1066
|
-
Notify all running jobs about a possible change in active status.
|
1067
|
-
"""
|
1068
|
-
ac = len(self.node_history)
|
1069
|
-
if ac == 0 or msg is None:
|
1070
|
-
ac = BrokenState
|
1071
|
-
elif self.name in self.node_history and ac == 1:
|
1072
|
-
ac = DetachedState
|
1073
|
-
elif self._act is not None and ac >= self.n_nodes: # TODO configureable
|
1074
|
-
ac = CompleteState
|
1075
|
-
else:
|
1076
|
-
ac = PartialState
|
1077
|
-
|
1078
|
-
logger.debug("State %r %r", ac, msg)
|
1079
|
-
|
1080
|
-
ac = ac(msg)
|
1081
|
-
for n in self.all_children:
|
1082
|
-
await n.send_event(ac)
|
1083
|
-
|
1084
|
-
|
1085
|
-
class AnyRunnerRoot(_BaseRunnerRoot):
|
1086
|
-
"""
|
1087
|
-
This class represents the root of a code runner. Its job is to start
|
1088
|
-
(and periodically restart, if required) the entry points stored under it.
|
1089
|
-
|
1090
|
-
:class:`AnyRunnerRoot` tries to ensure that the code in question runs
|
1091
|
-
on one single cluster member. In case of a network split, the code will
|
1092
|
-
run once in each split areas until the split is healed.
|
1093
|
-
"""
|
1094
|
-
|
1095
|
-
SUB = "group"
|
1096
|
-
|
1097
|
-
_stale_times = None
|
1098
|
-
tg = None
|
1099
|
-
seen_load = None
|
1100
|
-
_tagged: bool = False
|
1101
|
-
|
1102
|
-
def __init__(self, *a, **kw):
|
1103
|
-
super().__init__(*a, **kw)
|
1104
|
-
self.group = (
|
1105
|
-
P(self.client.config.server["root"]) + P(self._cfg["name"]) | "any" | self._path[-1]
|
1106
|
-
)
|
1107
|
-
|
1108
|
-
def get_node(self, name):
|
1109
|
-
return RunnerNode(self, name)
|
1110
|
-
|
1111
|
-
@property
|
1112
|
-
def max_age(self):
|
1113
|
-
"""Timeout after which we really should have gotten another go"""
|
1114
|
-
# allow one go-around without being in the list; happens when the
|
1115
|
-
# bus is slow
|
1116
|
-
return self._act.cycle_time_max * (self._act.history_size + 2.5)
|
1117
|
-
|
1118
|
-
async def _run_actor(self):
|
1119
|
-
"""
|
1120
|
-
Monitor the Actor state, run a :meth:`_run_now` subtask whenever we're 'it'.
|
1121
|
-
"""
|
1122
|
-
async with ClientActor(
|
1123
|
-
self.client,
|
1124
|
-
self.name,
|
1125
|
-
topic=self.group,
|
1126
|
-
cfg=self._cfg["actor"],
|
1127
|
-
) as act:
|
1128
|
-
self._act = act
|
1129
|
-
|
1130
|
-
age_q = create_queue(10)
|
1131
|
-
await self.spawn(self._age_killer, age_q)
|
1132
|
-
|
1133
|
-
psutil.cpu_percent(interval=None)
|
1134
|
-
await act.set_value(0)
|
1135
|
-
self.seen_load = None
|
1136
|
-
|
1137
|
-
async for msg in act:
|
1138
|
-
logger.debug("Actor %r", msg)
|
1139
|
-
if isinstance(msg, PingEvent):
|
1140
|
-
await act.set_value(100 - psutil.cpu_percent(interval=None))
|
1141
|
-
|
1142
|
-
node = self.get_node(msg.node)
|
1143
|
-
node.load = msg.value
|
1144
|
-
node.seen = time.time()
|
1145
|
-
if self.seen_load is not None:
|
1146
|
-
self.seen_load += msg.value
|
1147
|
-
self.node_history += node
|
1148
|
-
|
1149
|
-
elif isinstance(msg, TagEvent):
|
1150
|
-
self._tagged = True
|
1151
|
-
load = 100 - psutil.cpu_percent(interval=None)
|
1152
|
-
await act.set_value(load)
|
1153
|
-
if self.seen_load is not None:
|
1154
|
-
pass # TODO
|
1155
|
-
|
1156
|
-
self.get_node(self.name).seen = time.time()
|
1157
|
-
self.node_history += self.name
|
1158
|
-
evt = anyio.Event()
|
1159
|
-
await self.spawn(self._run_now, evt)
|
1160
|
-
await age_q.put(None)
|
1161
|
-
await evt.wait()
|
1162
|
-
|
1163
|
-
await self.state.ping()
|
1164
|
-
|
1165
|
-
elif isinstance(msg, UntagEvent):
|
1166
|
-
self._tagged = False
|
1167
|
-
await act.set_value(100 - psutil.cpu_percent(interval=None))
|
1168
|
-
self.seen_load = 0
|
1169
|
-
|
1170
|
-
self._run_now_task.cancel()
|
1171
|
-
# TODO if this is a DetagEvent, kill everything?
|
1172
|
-
|
1173
|
-
await self.notify_actor_state(msg)
|
1174
|
-
|
1175
|
-
pass # end of actor task
|
1176
|
-
|
1177
|
-
async def find_stale_nodes(self, cur):
|
1178
|
-
"""
|
1179
|
-
Find stale nodes (i.e. last seen < cur) and clean them.
|
1180
|
-
"""
|
1181
|
-
if self._stale_times is None:
|
1182
|
-
self._stale_times = []
|
1183
|
-
self._stale_times.append(cur)
|
1184
|
-
if self._stale_times[0] > cur - self.max_age:
|
1185
|
-
return
|
1186
|
-
if len(self._stale_times) <= 2 * self._cfg["actor"]["n_hosts"] + 1:
|
1187
|
-
return
|
1188
|
-
cut = self._stale_times.pop(0)
|
1189
|
-
|
1190
|
-
dropped = []
|
1191
|
-
for node in self._nodes.values():
|
1192
|
-
if node.seen < cut:
|
1193
|
-
dropped.append(node)
|
1194
|
-
names = set()
|
1195
|
-
for node in dropped:
|
1196
|
-
names.add(node.name)
|
1197
|
-
del self._nodes[node.name]
|
1198
|
-
if names:
|
1199
|
-
await self.state.kill_stale_nodes(names)
|
1200
|
-
|
1201
|
-
async def _age_killer(self, age_q):
|
1202
|
-
"""
|
1203
|
-
Subtask which cleans up stale tasks
|
1204
|
-
|
1205
|
-
TODO check where to use time.monotonic
|
1206
|
-
"""
|
1207
|
-
t0 = None
|
1208
|
-
t1 = time.time()
|
1209
|
-
while True:
|
1210
|
-
with anyio.move_on_after(self.max_age):
|
1211
|
-
await age_q.get()
|
1212
|
-
t1 = time.time()
|
1213
|
-
t2 = time.time()
|
1214
|
-
if t1 + self.max_age < t2:
|
1215
|
-
raise NotSelected(self.max_age, t2, t1)
|
1216
|
-
t0 = t1
|
1217
|
-
t1 = t2
|
1218
|
-
if t0 is not None:
|
1219
|
-
await self.find_stale_nodes(t0)
|
1220
|
-
|
1221
|
-
|
1222
|
-
class SingleRunnerRoot(_BaseRunnerRoot):
|
1223
|
-
"""
|
1224
|
-
This class represents the root of a code runner. Its job is to start
|
1225
|
-
(and periodically restart, if required) the entry points stored under it.
|
1226
|
-
|
1227
|
-
While :class:`AnyRunnerRoot` tries to ensure that the code in question runs
|
1228
|
-
on any cluster member, this class runs tasks on a single node.
|
1229
|
-
The code is able to check whether any and/or all of the cluster's main
|
1230
|
-
nodes are reachable; this way, the code can default to local operation
|
1231
|
-
if connectivity is lost.
|
1232
|
-
|
1233
|
-
Local data (dict):
|
1234
|
-
|
1235
|
-
Arguments:
|
1236
|
-
cores (tuple): list of nodes whose reachability may determine
|
1237
|
-
whether the code uses local/emergency/??? mode.
|
1238
|
-
|
1239
|
-
Config file:
|
1240
|
-
|
1241
|
-
Arguments:
|
1242
|
-
path (tuple): the location this entry is stored at. Defaults to
|
1243
|
-
``('.moat', 'kv', 'process')``.
|
1244
|
-
name (str): this runner's name. Defaults to the client's name plus
|
1245
|
-
the name stored in the root node, if any.
|
1246
|
-
actor (dict): the configuration for the underlying actor. See
|
1247
|
-
``asyncactor`` for details.
|
1248
|
-
"""
|
1249
|
-
|
1250
|
-
SUB = "single"
|
1251
|
-
|
1252
|
-
err = None
|
1253
|
-
code = None
|
1254
|
-
state = None
|
1255
|
-
tg = None
|
1256
|
-
|
1257
|
-
def __init__(self, *a, **kw):
|
1258
|
-
super().__init__(*a, **kw)
|
1259
|
-
self.group = (
|
1260
|
-
P(self.client.config.server["root"]) + P(self._cfg["name"])
|
1261
|
-
| "single"
|
1262
|
-
| self._path[-2]
|
1263
|
-
| self._path[-1]
|
1264
|
-
)
|
1265
|
-
|
1266
|
-
async def set_value(self, value):
|
1267
|
-
await super().set_value(value)
|
1268
|
-
try:
|
1269
|
-
cores = value["cores"]
|
1270
|
-
except (TypeError, KeyError):
|
1271
|
-
if self._act is not None:
|
1272
|
-
await self._act.disable(0)
|
1273
|
-
else:
|
1274
|
-
if self.name in cores:
|
1275
|
-
await self._act.enable(len(cores))
|
1276
|
-
else:
|
1277
|
-
await self._act.disable(len(cores))
|
1278
|
-
|
1279
|
-
@property
|
1280
|
-
def max_age(self):
|
1281
|
-
"""Timeout after which we really should have gotten another ping"""
|
1282
|
-
return self._act.cycle_time_max * 1.5
|
1283
|
-
|
1284
|
-
async def _run_actor(self):
|
1285
|
-
async with anyio.create_task_group() as tg:
|
1286
|
-
age_q = create_queue(1)
|
1287
|
-
|
1288
|
-
async with ClientActor(self.client, self.name, topic=self.group, cfg=self._cfg) as act:
|
1289
|
-
self._act = act
|
1290
|
-
tg.start_soon(self._age_notifier, age_q)
|
1291
|
-
await self.spawn(self._run_now)
|
1292
|
-
await act.set_value(0)
|
1293
|
-
|
1294
|
-
async for msg in act:
|
1295
|
-
if isinstance(msg, AuthPingEvent):
|
1296
|
-
# Some node, not necessarily us, is "it".
|
1297
|
-
# We're the SingleNode runner: we manage our jobs
|
1298
|
-
# when triggered by this, no matter whether we're
|
1299
|
-
# "it" or not.
|
1300
|
-
self.node_history += msg.node
|
1301
|
-
await age_q.put(None)
|
1302
|
-
await self.notify_actor_state(msg)
|
1303
|
-
|
1304
|
-
await self.state.ping()
|
1305
|
-
|
1306
|
-
pass # end of actor task
|
1307
|
-
|
1308
|
-
async def _age_notifier(self, age_q):
|
1309
|
-
"""
|
1310
|
-
This background job triggers :meth:`notify_actor_state` when too much
|
1311
|
-
time has passed without an AuthPing.
|
1312
|
-
"""
|
1313
|
-
while True:
|
1314
|
-
try:
|
1315
|
-
with anyio.fail_after(self.max_age):
|
1316
|
-
await age_q.get()
|
1317
|
-
except TimeoutError:
|
1318
|
-
await self.notify_actor_state()
|
1319
|
-
|
1320
|
-
|
1321
|
-
class AllRunnerRoot(SingleRunnerRoot):
|
1322
|
-
"""
|
1323
|
-
This class represents the root of a code runner. Its job is to start
|
1324
|
-
(and periodically restart, if required) the entry points stored under it.
|
1325
|
-
|
1326
|
-
This class behaves like `SingleRunner`, except that it runs tasks on all nodes.
|
1327
|
-
"""
|
1328
|
-
|
1329
|
-
SUB = "all"
|
1330
|
-
|
1331
|
-
err = None
|
1332
|
-
_act = None
|
1333
|
-
code = None
|
1334
|
-
|
1335
|
-
def __init__(self, *a, **kw):
|
1336
|
-
super().__init__(*a, **kw)
|
1337
|
-
self.group = (
|
1338
|
-
P(self.client.config.server["root"]) + P(self._cfg["name"]) | "all" | self._path[-1]
|
1339
|
-
)
|
1340
|
-
|
1341
|
-
async def _state_runner(self):
|
1342
|
-
self.state = await StateRoot.as_handler(
|
1343
|
-
self.client,
|
1344
|
-
cfg=self._cfg,
|
1345
|
-
subpath=self._x_subpath + (self.client.client_name,),
|
1346
|
-
key="state",
|
1347
|
-
)
|