moat-kv 0.70.23__py3-none-any.whl → 0.71.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.
Files changed (177) hide show
  1. build/lib/docs/source/conf.py +201 -0
  2. build/lib/examples/pathify.py +45 -0
  3. build/lib/moat/kv/__init__.py +19 -0
  4. build/lib/moat/kv/_cfg.yaml +93 -0
  5. build/lib/moat/kv/_main.py +91 -0
  6. build/lib/moat/kv/actor/__init__.py +98 -0
  7. build/lib/moat/kv/actor/deletor.py +139 -0
  8. build/lib/moat/kv/auth/__init__.py +444 -0
  9. build/lib/moat/kv/auth/_test.py +166 -0
  10. build/lib/moat/kv/auth/password.py +234 -0
  11. build/lib/moat/kv/auth/root.py +58 -0
  12. build/lib/moat/kv/backend/__init__.py +67 -0
  13. build/lib/moat/kv/backend/mqtt.py +71 -0
  14. build/lib/moat/kv/client.py +1025 -0
  15. build/lib/moat/kv/code.py +236 -0
  16. build/lib/moat/kv/codec.py +11 -0
  17. build/lib/moat/kv/command/__init__.py +1 -0
  18. build/lib/moat/kv/command/acl.py +180 -0
  19. build/lib/moat/kv/command/auth.py +261 -0
  20. build/lib/moat/kv/command/code.py +293 -0
  21. build/lib/moat/kv/command/codec.py +186 -0
  22. build/lib/moat/kv/command/data.py +265 -0
  23. build/lib/moat/kv/command/dump/__init__.py +143 -0
  24. build/lib/moat/kv/command/error.py +149 -0
  25. build/lib/moat/kv/command/internal.py +248 -0
  26. build/lib/moat/kv/command/job.py +433 -0
  27. build/lib/moat/kv/command/log.py +53 -0
  28. build/lib/moat/kv/command/server.py +114 -0
  29. build/lib/moat/kv/command/type.py +201 -0
  30. build/lib/moat/kv/config.py +46 -0
  31. build/lib/moat/kv/data.py +216 -0
  32. build/lib/moat/kv/errors.py +561 -0
  33. build/lib/moat/kv/exceptions.py +126 -0
  34. build/lib/moat/kv/mock/__init__.py +101 -0
  35. build/lib/moat/kv/mock/mqtt.py +159 -0
  36. build/lib/moat/kv/mock/tracer.py +63 -0
  37. build/lib/moat/kv/model.py +1069 -0
  38. build/lib/moat/kv/obj/__init__.py +646 -0
  39. build/lib/moat/kv/obj/command.py +241 -0
  40. build/lib/moat/kv/runner.py +1347 -0
  41. build/lib/moat/kv/server.py +2809 -0
  42. build/lib/moat/kv/types.py +513 -0
  43. ci/rtd-requirements.txt +4 -0
  44. ci/test-requirements.txt +7 -0
  45. ci/travis.sh +96 -0
  46. debian/.gitignore +7 -0
  47. debian/changelog +1435 -0
  48. debian/control +43 -0
  49. debian/moat-kv/usr/lib/python3/dist-packages/docs/source/conf.py +201 -0
  50. debian/moat-kv/usr/lib/python3/dist-packages/examples/pathify.py +45 -0
  51. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/__init__.py +19 -0
  52. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_cfg.yaml +93 -0
  53. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/_main.py +91 -0
  54. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/__init__.py +98 -0
  55. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/actor/deletor.py +139 -0
  56. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/__init__.py +444 -0
  57. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/_test.py +166 -0
  58. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/password.py +234 -0
  59. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/auth/root.py +58 -0
  60. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/__init__.py +67 -0
  61. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/backend/mqtt.py +71 -0
  62. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/client.py +1025 -0
  63. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/code.py +236 -0
  64. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/codec.py +11 -0
  65. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/__init__.py +1 -0
  66. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/acl.py +180 -0
  67. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/auth.py +261 -0
  68. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/code.py +293 -0
  69. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/codec.py +186 -0
  70. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/data.py +265 -0
  71. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/dump/__init__.py +143 -0
  72. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/error.py +149 -0
  73. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/internal.py +248 -0
  74. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/job.py +433 -0
  75. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/log.py +53 -0
  76. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/server.py +114 -0
  77. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/command/type.py +201 -0
  78. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/config.py +46 -0
  79. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/data.py +216 -0
  80. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/errors.py +561 -0
  81. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/exceptions.py +126 -0
  82. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/__init__.py +101 -0
  83. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/mqtt.py +159 -0
  84. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/mock/tracer.py +63 -0
  85. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/model.py +1069 -0
  86. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/__init__.py +646 -0
  87. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/obj/command.py +241 -0
  88. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/runner.py +1347 -0
  89. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/server.py +2809 -0
  90. debian/moat-kv/usr/lib/python3/dist-packages/moat/kv/types.py +513 -0
  91. debian/moat-kv.postinst +3 -0
  92. debian/rules +20 -0
  93. debian/source/format +1 -0
  94. debian/watch +4 -0
  95. docs/Makefile +20 -0
  96. docs/make.bat +36 -0
  97. docs/source/TODO.rst +61 -0
  98. docs/source/_static/.gitkeep +0 -0
  99. docs/source/acls.rst +80 -0
  100. docs/source/auth.rst +84 -0
  101. docs/source/client_protocol.rst +456 -0
  102. docs/source/code.rst +341 -0
  103. docs/source/command_line.rst +1187 -0
  104. docs/source/common_protocol.rst +47 -0
  105. docs/source/conf.py +201 -0
  106. docs/source/debugging.rst +70 -0
  107. docs/source/extend.rst +37 -0
  108. docs/source/history.rst +36 -0
  109. docs/source/index.rst +75 -0
  110. docs/source/model.rst +54 -0
  111. docs/source/overview.rst +83 -0
  112. docs/source/related.rst +89 -0
  113. docs/source/server_protocol.rst +450 -0
  114. docs/source/startup.rst +31 -0
  115. docs/source/translator.rst +244 -0
  116. docs/source/tutorial.rst +711 -0
  117. docs/source/v3.rst +168 -0
  118. examples/code/transform.scale.yml +21 -0
  119. examples/code/transform.switch.yml +82 -0
  120. examples/code/transform.timeslot.yml +63 -0
  121. examples/pathify.py +45 -0
  122. moat/kv/_cfg.yaml +2 -6
  123. moat/kv/actor/__init__.py +98 -0
  124. moat/kv/actor/deletor.py +139 -0
  125. moat/kv/auth/__init__.py +444 -0
  126. moat/kv/auth/_test.py +166 -0
  127. moat/kv/auth/password.py +234 -0
  128. moat/kv/auth/root.py +58 -0
  129. moat/kv/backend/__init__.py +67 -0
  130. moat/kv/backend/mqtt.py +71 -0
  131. moat/kv/command/__init__.py +1 -0
  132. moat/kv/command/acl.py +180 -0
  133. moat/kv/command/auth.py +261 -0
  134. moat/kv/command/code.py +293 -0
  135. moat/kv/command/codec.py +186 -0
  136. moat/kv/command/data.py +265 -0
  137. moat/kv/command/dump/__init__.py +143 -0
  138. moat/kv/command/error.py +149 -0
  139. moat/kv/command/internal.py +248 -0
  140. moat/kv/command/job.py +433 -0
  141. moat/kv/command/log.py +53 -0
  142. moat/kv/command/server.py +114 -0
  143. moat/kv/command/type.py +201 -0
  144. moat/kv/mock/__init__.py +101 -0
  145. moat/kv/mock/mqtt.py +159 -0
  146. moat/kv/mock/tracer.py +63 -0
  147. moat/kv/obj/__init__.py +646 -0
  148. moat/kv/obj/command.py +241 -0
  149. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/METADATA +2 -5
  150. moat_kv-0.71.0.dist-info/RECORD +188 -0
  151. moat_kv-0.71.0.dist-info/top_level.txt +9 -0
  152. scripts/current +15 -0
  153. scripts/env +8 -0
  154. scripts/init +39 -0
  155. scripts/recover +17 -0
  156. scripts/rotate +33 -0
  157. scripts/run +29 -0
  158. scripts/run-all +10 -0
  159. scripts/run-any +10 -0
  160. scripts/run-single +15 -0
  161. scripts/success +4 -0
  162. systemd/moat-kv-recover.service +21 -0
  163. systemd/moat-kv-rotate.service +20 -0
  164. systemd/moat-kv-rotate.timer +10 -0
  165. systemd/moat-kv-run-all.service +26 -0
  166. systemd/moat-kv-run-all@.service +25 -0
  167. systemd/moat-kv-run-any.service +26 -0
  168. systemd/moat-kv-run-any@.service +25 -0
  169. systemd/moat-kv-run-single.service +26 -0
  170. systemd/moat-kv-run-single@.service +25 -0
  171. systemd/moat-kv.service +27 -0
  172. systemd/postinst +7 -0
  173. systemd/sysusers +3 -0
  174. moat_kv-0.70.23.dist-info/RECORD +0 -19
  175. moat_kv-0.70.23.dist-info/top_level.txt +0 -1
  176. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/WHEEL +0 -0
  177. {moat_kv-0.70.23.dist-info → moat_kv-0.71.0.dist-info}/licenses/LICENSE.txt +0 -0
docs/source/v3.rst ADDED
@@ -0,0 +1,168 @@
1
+ =================
2
+ MoaT-KV Version 3
3
+ =================
4
+
5
+ +++++++++
6
+ Rationale
7
+ +++++++++
8
+
9
+ The V2 servers worked, for the most part, but they had a couple of problems.
10
+
11
+ * sometimes there are strange deadlocks
12
+
13
+ * msgpack isn't that widely supported compared to CBOR
14
+
15
+ * the initial client/server negotiation isn't versioned
16
+
17
+ * the sync protocol is somewhat overengineered
18
+
19
+ * speed of updates is limited by the client>server>MQTT>server>client
20
+ chain; two possibly-high-load servers in between cause too much delay
21
+
22
+ * Messages tended to be more verbose than necessary
23
+
24
+ ++++++++++++++++++
25
+ V3 design overview
26
+ ++++++++++++++++++
27
+
28
+ Server start
29
+ ++++++++++++
30
+
31
+ * connect to MQTT, listen to all MoaT messages,
32
+ update internal state as messages come in
33
+
34
+ * ask for a server link on the Join/Actor topic
35
+
36
+ * load the local backup if present
37
+
38
+ * fetch full tree from designated server is replied
39
+
40
+ * merge data
41
+
42
+ * Join actor topic
43
+
44
+
45
+ Client connection setup A
46
+ +++++++++++++++++++++++++
47
+
48
+ * Client connects to MQTT and sends Query message
49
+ * Designated server replies with connection data
50
+ * Client connects to server
51
+
52
+ * Server greets client
53
+ * Client authenticates
54
+ * Server sends MQTT connection information
55
+ * Client connects to MQTT and sends Birth message
56
+ * Server sees the client's message and sends ACK to the client
57
+
58
+ Client connection setup B
59
+ +++++++++++++++++++++++++
60
+
61
+ This method is slower; it can be used when the configured server doesn't work.
62
+
63
+ * Client connects to MQTT and sends Query message
64
+ * Designated server sends connection data
65
+
66
+ Reading initial data
67
+ ++++++++++++++++++++
68
+
69
+ * Client subscribes to MQTT topic
70
+ * Client requests initial data from server
71
+ * Server sends data, advises client that no data exist, or tells the client
72
+ that MQTT uses retaining (if so, which codec to use)
73
+
74
+ Updates
75
+ +++++++
76
+
77
+ The most notable pain point of the old design is the speed of updates.
78
+ Thus in V3 all updates will be MQTT messages.
79
+
80
+ This section does not apply if the MQTT server retains the data.
81
+
82
+ MoaT update messages are CBOR maps. Keys are small integers for message brevity and
83
+ decode speed.
84
+
85
+ * 0: tock
86
+
87
+ The system-wide update counter. May be missing.
88
+
89
+ * 1: value
90
+
91
+ Contents: Whatever is CBOR-encodeable. A missing value means that the
92
+ object shall be deleted.
93
+
94
+ * 2: tick.
95
+
96
+ Update tracker. This is a ``((name, seq, counter), …)`` list of tuples.
97
+
98
+ * name
99
+ The server's name
100
+
101
+ * seq
102
+ Client connection. Zero is reserved for server-generated content.
103
+
104
+ * counter
105
+ A per-client update counter, managed by the client. Must start at 1 and
106
+ increment in steps of 1, in order to detect skipped updates.
107
+
108
+
109
+ The server sees the update and sends an ACK message to the client, assuming
110
+ that there was no conflict.
111
+
112
+
113
+ Skipped updates
114
+ +++++++++++++++
115
+
116
+ Servers listen to all messages. If there's a gap in a client's sequence
117
+ numbers, the server will ask it to repeat the message.
118
+
119
+
120
+ Update conflict resolution
121
+ ++++++++++++++++++++++++++
122
+
123
+ If a client sends a message which the server determines
124
+
125
+
126
+ MQTT topics
127
+ +++++++++++
128
+
129
+ All are under a common configured prefix, the default is "moat/main".
130
+
131
+ svc/act
132
+ -------
133
+
134
+ The Actor topic for server identification.
135
+
136
+ The transmitted value contains the server's name, host and port.
137
+
138
+
139
+ svc/query
140
+ ---------
141
+
142
+ Connect requests from clients.
143
+
144
+ svc/server
145
+ ----------
146
+
147
+ Reply queue for messages to ``svc/query``. Contains the server value as
148
+ above.
149
+
150
+
151
+ d/*
152
+ ---
153
+
154
+ Update messages.
155
+
156
+ Topic translation
157
+ +++++++++++++++++
158
+
159
+ Topics are encoded like MoaT paths, except for these differences:
160
+
161
+ * The path separator is ``/`` instead of ``.``
162
+ * Slashes are escaped as ``:_``.
163
+ * Spaces in paths are never escaped: that would collide with the previous rule
164
+ * Dots are not escaped, obviously.
165
+ * `None` is encoded as "$NULL" when it's a top-level element.
166
+ * The sequence ``:.`` is used to shield both wildcards and strings with a
167
+ leading ``$`` character. It translates back to an empty string, not a
168
+ dot, and may be treated as an illegal sequence otherwise.
@@ -0,0 +1,21 @@
1
+ code: |
2
+ await _self.watch(src, fetch=False)
3
+ async for msg in _info:
4
+ if isinstance(msg, _cls.ChangeMsg):
5
+ try:
6
+ val = msg.value * factor + offset
7
+ except AttributeError:
8
+ continue
9
+ await _client.set(dst, value=val, idem=False)
10
+ info: Apply factor+offset
11
+ is_async: true
12
+ vars:
13
+ - src
14
+ - dst
15
+ - factor
16
+ - offset
17
+
18
+ # Whenever the value at 'src', changes, this code multiplies it by
19
+ # 'factor', adds 'offset', and writes it to 'dst'.
20
+ #
21
+ # This takes ~0.025 seconds, end-to-end, on a Raspberry Pi 3.
@@ -0,0 +1,82 @@
1
+ code: |
2
+ await _self.watch(src, fetch=False)
3
+ inv = high < low
4
+ if inv:
5
+ high,low = low,high
6
+ res = await _client.get(flip)
7
+ if "value" in res:
8
+ is_high = res.value != inv
9
+ else:
10
+ await _client.set(flip, value=inv)
11
+ is_high = False
12
+
13
+ async for msg in _info:
14
+ if isinstance(msg, _cls.ChangeMsg):
15
+ try:
16
+ val = msg.value
17
+ except AttributeError:
18
+ continue
19
+ if is_high and val < low:
20
+ is_high = False
21
+ await _client.set(flip, value=inv)
22
+ elif not is_high and val > high:
23
+ is_high = True
24
+ await _client.set(flip, value=not inv)
25
+ else:
26
+ if is_high:
27
+ val += high-low
28
+ await _client.set(dst, value=val)
29
+ info: Switch input between triggers
30
+ is_async: true
31
+ vars:
32
+ - src
33
+ - dst
34
+ - flip
35
+ - high
36
+ - low
37
+
38
+ # Consider this simple hook-up of a photoresistor:
39
+ #
40
+ # +5V --- PHOTO --+-- R1 --+-- R2 --- GND
41
+ # | |
42
+ # sensor port
43
+ #
44
+ # The port is a pull-down output: thus, this setup switches resistance
45
+ # between R1 and R1+R2, which allows us to measure the wide range of
46
+ # resistance which a typical photoresistor has, without requiring more
47
+ # fancy circuitry.
48
+ # ‹low› and ‹high› need to be calculated so that
49
+ #
50
+ # * when the photoresistor's resistance is such that the voltage at the
51
+ # sensor is at ‹high›, switching on the port will set the voltage to
52
+ # ‹low›, so you don't get a break in the brightness curve
53
+ # * the rate of change is roughly equal, so you don't get a kink in that
54
+ # curve.
55
+ #
56
+ # These conditions are satisfied when R(photo) = √(R1*(R1+R2)).
57
+ # So if R1=100Ω and R2=10kΩ, R(photo) at the switching point would be 1005Ω,
58
+ # thus you can calculate V(high) as 4.55 Volt and V(low) as 0.45 Volt.
59
+ #
60
+ # The actual choice of resistors is up to you and depends on the
61
+ # photoresistor's behavior in "interesting" lighting conditions. In general
62
+ # you'd want a high-resolution capture of the values at both ends of the
63
+ # range. Thus the above resistor values are sensible if the photoresistor
64
+ # is at 5kΩ when the light gets bright enough to read the newspaper (you
65
+ # want to capture as many nuances of the dark phase as possible), and at
66
+ # 50Ω on a bright summer day just as a cloud has obscured the sun.
67
+ # Or something like that.
68
+ #
69
+ # 'src' is the path which the sensor value is written to. Presumably it's
70
+ # polled periodically.
71
+ #
72
+ # 'dst' is the entry which receives the adjusted value.
73
+ #
74
+ # 'flip' is the entry which corresponds to the port. It receives a `bool`
75
+ # value (`False` on startup).
76
+ #
77
+ # 'low' and 'high' are the threshold values. If 'low' is greater than
78
+ # 'high', the port is inverted (i.e. it's set to `True` on startup).
79
+ #
80
+ # When a ‹src› update causes the port to be inverted, writing to ‹dst› is
81
+ # skipped: if the new value is close to the threshold, the delay doesn't
82
+ # matter much, and if it's not the value would be too inaccurate.
@@ -0,0 +1,63 @@
1
+ code: |
2
+ last_val = None
3
+ this_val = None
4
+ timer = None
5
+ await _client.set(dst, value=0, idem=True)
6
+ await _self.watch(src, fetch=True)
7
+
8
+ async for msg in _info:
9
+ if isinstance(msg, _cls.ChangeMsg):
10
+ try:
11
+ val = msg.value
12
+ except AttributeError:
13
+ continue
14
+ if last_val is None:
15
+ last_val = val
16
+ this_val = val
17
+ continue
18
+ this_val = val
19
+ delta = this_val - last_val
20
+ if delta > 0 and timer is None:
21
+ # fire an immediate update at first change
22
+ await _client.set(dst, value=delta*factor, idem=(delta == 0))
23
+ timer = await _self.timer(seconds)
24
+ elif delta < 0:
25
+ # wraparound or whatever
26
+ last_val = this_val
27
+ if timer is not None:
28
+ await timer.cancel()
29
+ timer = None
30
+ elif isinstance(msg, _cls.TimerMsg):
31
+ delta = this_val - last_val
32
+ if delta >= 0:
33
+ await _client.set(dst, value=delta*factor, idem=(delta == 0))
34
+ if delta > 0:
35
+ await timer.run(seconds)
36
+ else:
37
+ timer = None
38
+ last_val = this_val
39
+ info: generate timeslots for counter deltas
40
+ is_async: true
41
+ vars:
42
+ - src
43
+ - dst
44
+ - seconds
45
+ - factor
46
+
47
+ # This code converts a randomly-updating counter into one that carries a
48
+ # defined meaning.
49
+ #
50
+ # Consider a rain meter. The counter triggers whenever the meter's counter
51
+ # triggers possibly aggregated so that you don't get more than one update
52
+ # every ten seconds even if it's raining buckets.
53
+ #
54
+ # However, in your display you want the rate of rain over, say, the last
55
+ # minute, so you can show some approximation of "how much rain is there
56
+ # right now".
57
+ #
58
+ # This code sends an initial update immediately so that receiving code sees
59
+ # some value > 0 ASAP, then another update every ‹seconds›.
60
+ #
61
+ # Updates are scaled by ‹factor› so you can translate the counter's output
62
+ # to something understandable like "how many mm of water would there be on
63
+ # the ground if this amount of rain continued for an hour".
examples/pathify.py ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/python3
2
+
3
+ # Batch-convert data. In this case I had some entries which were stored as
4
+ # a list, but using Path made much more sense (esp when you need to
5
+ # view/edit the yaml export).
6
+
7
+ import anyio
8
+ from moat.kv.client import open_client
9
+ from moat.util import P, yload, Path
10
+ import asyncclick as click
11
+
12
+
13
+ def conv(m, s: str) -> bool:
14
+ try:
15
+ d = m.value[s]
16
+ except KeyError:
17
+ return 0
18
+ if isinstance(d, Path):
19
+ return 0
20
+ if not isinstance(d, Sequence):
21
+ return 0
22
+ d = Path.build(d)
23
+ m.value[s] = d
24
+ return 1
25
+
26
+
27
+ @click.command()
28
+ @click.argument("path", type=P)
29
+ @click.argument("keys", type=str, nargs=-1)
30
+ async def main(path, keys):
31
+ if not keys:
32
+ keys = "src dest dst state".split()
33
+ with open("/etc/moat.kv.cfg") as cff:
34
+ cfg = yload(cff)
35
+ async with open_client(**cfg) as client:
36
+ async for m in client.get_tree(path, nchain=2):
37
+ n = 0
38
+ for k in keys:
39
+ n += conv(m, k)
40
+ if n:
41
+ await client.set(ORIG + m.path, value=m.value, chain=m.chain)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()
moat/kv/_cfg.yaml CHANGED
@@ -26,7 +26,6 @@ runner: # for moat.kv.runner.RunnerRoot
26
26
  state: !P :.moat.kv.state"
27
27
 
28
28
  name: "run"
29
- # Serf event name, suffixed by subpath
30
29
 
31
30
  start_delay: 1
32
31
  # time to wait between job starts. Not optional.
@@ -56,9 +55,6 @@ server:
56
55
  # default
57
56
  mqtt:
58
57
  uri: "mqtt://localhost:1883"
59
- serf:
60
- host: "localhost"
61
- port: 7373
62
58
 
63
59
  # event message path/topic prefix
64
60
  root: !P moat.kv
@@ -79,10 +75,10 @@ server:
79
75
  ping:
80
76
  cycle: 10
81
77
  gap: 2
82
- # asyncserf.Actor config timing for server sync
78
+ # asyncactor config timing for server sync
83
79
  # ping also controls minimum server startup time
84
80
  delete:
85
- # asyncserf.Actor config timing for deletion
81
+ # asyncactor config timing for deletion
86
82
  cycle: 100
87
83
  gap: 10
88
84
  version: 1
@@ -0,0 +1,98 @@
1
+ """
2
+ This module implements a :class:`asyncactor.Actor` which works on top of
3
+ a MoaT-KV client.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from asyncactor import Actor
8
+ from asyncactor.abc import MonitorStream, Transport
9
+
10
+ __all__ = [
11
+ "ClientActor",
12
+ "ActorState",
13
+ "BrokenState",
14
+ "DetachedState",
15
+ "PartialState",
16
+ "CompleteState",
17
+ ]
18
+
19
+
20
+ class ClientActor(Actor):
21
+ def __init__(self, client, *a, topic, **kw):
22
+ super().__init__(ClientTransport(client, topic), *a, **kw)
23
+
24
+
25
+ class ClientTransport(Transport):
26
+ """
27
+ This class exports the client's direct messaging interface to the
28
+ actor.
29
+ """
30
+
31
+ def __init__(self, client, topic):
32
+ self.client = client
33
+ self.topic = topic
34
+
35
+ def monitor(self):
36
+ return ClientMonitor(self)
37
+
38
+ async def send(self, payload):
39
+ await self.client.msg_send(self.topic, payload)
40
+
41
+
42
+ class ClientMonitor(MonitorStream):
43
+ _mon1 = None
44
+ _mon2 = None
45
+ _it = None
46
+
47
+ async def __aenter__(self):
48
+ self._mon1 = self.transport.client.msg_monitor(self.transport.topic)
49
+ self._mon2 = await self._mon1.__aenter__()
50
+ return self
51
+
52
+ async def __aexit__(self, *tb):
53
+ return await self._mon1.__aexit__(*tb)
54
+
55
+ def __aiter__(self):
56
+ self._it = self._mon2.__aiter__()
57
+ return self
58
+
59
+ async def __anext__(self):
60
+ msg = await self._it.__anext__()
61
+ return msg.data
62
+
63
+
64
+ # The following events are used by Runner etc. to notify running jobs
65
+ # about the current connectivity state.
66
+ #
67
+ class ActorState:
68
+ """base class for states"""
69
+
70
+ def __init__(self, msg=None):
71
+ self.msg = msg
72
+
73
+ def __repr__(self):
74
+ return "<%s:%r>" % (self.__class__.__name__, self.msg)
75
+
76
+
77
+ class BrokenState(ActorState):
78
+ """I have no idea what's happening, probably nothing good"""
79
+
80
+ pass
81
+
82
+
83
+ class DetachedState(ActorState):
84
+ """I am detached, my actor group is not visible"""
85
+
86
+ pass
87
+
88
+
89
+ class PartialState(ActorState):
90
+ """Some but not all members of my actor group are visible"""
91
+
92
+ pass
93
+
94
+
95
+ class CompleteState(ActorState):
96
+ """All members of my actor group are visible"""
97
+
98
+ pass
@@ -0,0 +1,139 @@
1
+ """
2
+ This module implements additional code for the server-side DeleteActor,
3
+ which is used to clean up the list of deleted nodes.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import weakref
9
+ from collections import deque
10
+
11
+ import anyio
12
+ from asyncactor import Actor, PingEvent, TagEvent
13
+ from asyncactor.backend import get_transport
14
+
15
+ TAGS = 4
16
+
17
+
18
+ class DeleteActor:
19
+ _enabled = None
20
+
21
+ def __init__(self, server):
22
+ self._server = weakref.ref(server)
23
+ self.deleted = deque()
24
+ self.tags = []
25
+ self.actor = None
26
+
27
+ self.max_seen = 0
28
+ self.n_tags = 0
29
+ self.n_pings = 0
30
+ self.n_nodes = 0
31
+
32
+ @property
33
+ def server(self):
34
+ return self._server()
35
+
36
+ async def tock_me(self):
37
+ """
38
+ Add the current tock to our buffer.
39
+
40
+ This is updated whenever a new leader is selected.
41
+ """
42
+ self.tags.append(self.server.tock)
43
+ self.tags = self.tags[-TAGS:]
44
+ await self.actor.set_value((self.tags[0], self.tags[-1]))
45
+
46
+ def add_deleted(self, nodes: NodeSet): # noqa: F821
47
+ """
48
+ These nodes are deleted. Remember them for some time.
49
+ """
50
+ if self.n_nodes == 0:
51
+ return
52
+ self.deleted.append((self.server.tock, nodes))
53
+
54
+ def purge_to(self, tock):
55
+ """
56
+ Sufficient time has passed since this tock was seen, while all
57
+ Delete actor nodes were active. Finally flush the entries that have
58
+ been deleted before it.
59
+ """
60
+ while self.deleted and self.deleted[0][0] < tock:
61
+ d = self.deleted.popleft()
62
+ self.server.purge_deleted(d[1])
63
+
64
+ async def enable(self, n):
65
+ """
66
+ Enable this actor, as a group of N.
67
+ """
68
+ if self.actor is None:
69
+ self._enabled = True
70
+ else:
71
+ await self.actor.enable(n)
72
+ self.n_tags = 0
73
+ self.n_pings = 0
74
+ self.n_nodes = n
75
+
76
+ async def disable(self, n: int = 0):
77
+ """
78
+ Disable this actor. It will still listen, and require N Delete
79
+ actor members in order to flush its deletion entries.
80
+
81
+ Completely disable deletion flushing by passing n=0.
82
+ """
83
+ if self.actor is None:
84
+ self._enabled = False
85
+ else:
86
+ await self.actor.disable()
87
+ self.n_tags = 0
88
+ self.n_pings = 0
89
+ self.n_nodes = n
90
+
91
+ async def run(self, evt: anyio.abc.Event = None):
92
+ """
93
+ The task that monitors the Delete actor.
94
+ """
95
+ try:
96
+ T = get_transport("moat_kv")
97
+ async with Actor(
98
+ T(self.server.backend, *self.server.cfg.server.root, "del"),
99
+ name=self.server.node.name,
100
+ cfg=self.server.cfg.server.delete,
101
+ enabled=False,
102
+ ) as actor:
103
+ self.actor = actor
104
+ if self._enabled is not None:
105
+ if self._enabled:
106
+ await actor.enable()
107
+ else:
108
+ await actor.disable()
109
+ if evt is not None:
110
+ evt.set()
111
+ async for evt in actor:
112
+ if isinstance(evt, PingEvent):
113
+ val = evt.value
114
+ if val is None:
115
+ self.n_pings = self.n_tags = 0
116
+ continue
117
+ if len(evt.msg.history) < self.n_nodes:
118
+ self.n_pings = self.n_tags = 0
119
+ continue
120
+ self.n_pings += 1
121
+ if self.n_pings > self.n_nodes:
122
+ mx, self.max_seen = (
123
+ self.max_seen,
124
+ max(self.max_seen, val[1]),
125
+ )
126
+ if val[0] > mx > 0:
127
+ await self.server.resync_deleted(evt.msg.history)
128
+ continue
129
+ self.purge_to(val[0])
130
+ self.max_seen = max(self.max_seen, val[1])
131
+
132
+ elif isinstance(evt, TagEvent):
133
+ if actor.history_size == self.n_nodes:
134
+ self.n_tags += 1
135
+ if self.n_tags > 2:
136
+ self.purge_to(self.tags[0])
137
+ await self.tock_me()
138
+ finally:
139
+ self.actor = None