fleetmesh-node 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,353 @@
1
+ """The pulls this node is configured to make, and every reason one did not happen.
2
+
3
+ This was seventy lines in the middle of the node's tick, and it was the worst
4
+ place in the node to be a stranger. Five things can stop a scheduled pull: the
5
+ peer has no peering yet and the proposal raised; the proposal came back
6
+ declined; this node is over its own ceiling and has stopped pulling as the last
7
+ shed rung; the grant this node holds covers no session this window; or the
8
+ session itself failed. Every one of them was appended to the tick's return value
9
+ as a tuple, printed once, and then gone. `meshnode refusals` could not show any
10
+ of them, `health` counted none of them, and an operator watching a peer go quiet
11
+ had to read the journal back far enough to find the line - if the journal still
12
+ had it.
13
+
14
+ A refusal that is not recorded is indistinguishable from silence. This
15
+ compartment is the five of them given a channel.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional
20
+
21
+ if TYPE_CHECKING:
22
+ from node.planes.peering.budget.core import Budget
23
+ from node.planes.peering.capacity.compartment import Capacity
24
+ from node.planes.peering.strain.compartment import StrainLedger
25
+
26
+ import dataclasses
27
+ from node.interfaces import ThrottledRow
28
+ from node.planes.store.sync.shape import SyncSessionResult
29
+
30
+
31
+ from .types import SchedulerStateReport
32
+
33
+ from fleetmesh_core.accounting import accounting
34
+ from node.compartment import stores as st
35
+ from node.storage import grants as grants_mod
36
+ from node.channels import Channel, NOTE
37
+ from node.metrics import COUNTER, Metric
38
+ from node.compartment import Command, Compartment, Need
39
+ from node.config.parameter import Parameter, ParamBlock
40
+
41
+ # A decline names a reason that does not change in ten minutes: a digest, a
42
+ # The cadence when a peer has no grant to derive one from: there is no
43
+ # granted rate to pace against until one is issued.
44
+ DEFAULT_PULL_EVERY = 900
45
+
46
+ # missing protocol, a closed door. Ask again in an hour, not at the pull cadence.
47
+ DECLINE_RETRY: int = 3600
48
+
49
+
50
+ @dataclasses.dataclass(frozen=True)
51
+ class SchedulerConfig(ParamBlock):
52
+ key = 'scheduler'
53
+ name = 'scheduler'
54
+ DERIVED: bool = False
55
+ PARAMS = (
56
+ Parameter(key='default_pull_every', name='Default Pull Cadence', description='Seconds between pulls when peer grant has no rate', type=int, default=DEFAULT_PULL_EVERY, tags=('scheduler', 'pull')),
57
+ Parameter(key='decline_retry', name='Decline Retry Horizon', description='Seconds before re-proposing to declined peer', type=int, default=DECLINE_RETRY, tags=('scheduler', 'pull')),
58
+ )
59
+ default_pull_every: int = DEFAULT_PULL_EVERY
60
+ decline_retry: int = DECLINE_RETRY
61
+
62
+
63
+ class Scheduler(Compartment):
64
+ requires: tuple[str, ...] = ('budget', 'capacity', 'strain')
65
+
66
+ name: str = 'scheduler'
67
+ reads: tuple[str, ...] = ('peers',)
68
+ live: tuple[str, ...] = ('peers',)
69
+ config_cls = SchedulerConfig
70
+ CHANNELS = (
71
+ Channel('pull_scheduled', 'Pull scheduled for peer and shard', level=NOTE, say='scheduled pull for peer {peer} on shard {shard}', tags=('scheduler', 'pull')),
72
+ Channel('pull_refused', 'Scheduled pull refused', level=NOTE, say='pull refused for peer {peer} on shard {shard}: {reason}', tags=('scheduler', 'pull')),
73
+ )
74
+ METRICS = (
75
+ Metric(name='scheduler_pulls_scheduled', description='Times pulls were scheduled.', kind=COUNTER, counts='pull_scheduled', tags=('scheduler', 'pull')),
76
+ Metric(name='scheduler_pulls_refused', description='Times scheduled pulls were refused.', kind=COUNTER, counts='pull_refused', tags=('scheduler', 'pull')),
77
+ )
78
+ NEEDS = (
79
+ Need('grant_store', 'stores.grants'),
80
+ Need('budget'),
81
+ Need('capacity'),
82
+ Need('propose'),
83
+ Need('pull'),
84
+ Need('strain'),
85
+ )
86
+ STORES = (
87
+ st.Store(
88
+ key='_last_pull',
89
+ description='when a session last ran for (peer, shard)',
90
+ bound=1024,
91
+ evict=st.LRU,
92
+ kind=st.TABLE,
93
+ keyed_by='pull',
94
+ ),
95
+ st.Store(
96
+ key='_declined',
97
+ description='when a declined proposal may be tried again for (peer, shard)',
98
+ bound=1024,
99
+ evict=st.LRU,
100
+ kind=st.TABLE,
101
+ keyed_by='pull',
102
+ ),
103
+ st.Store(
104
+ key='_throttled',
105
+ description='(epoch, why) last recorded for (peer, shard)',
106
+ bound=1024,
107
+ evict=st.LRU,
108
+ kind=st.TABLE,
109
+ keyed_by='pull',
110
+ ),
111
+ st.Store(
112
+ key='_shed',
113
+ description='whether the pull is held under the ceiling for (peer, shard)',
114
+ bound=1024,
115
+ evict=st.LRU,
116
+ kind=st.TABLE,
117
+ keyed_by='pull',
118
+ ),
119
+ )
120
+ COMMANDS = (
121
+ Command(
122
+ key='show',
123
+ name='Scheduler',
124
+ description='every scheduled pull, when it last ran, and what is holding it back now',
125
+ tags=('scheduler', 'status'),
126
+ permission='scheduler/show',
127
+ run='state',
128
+ reply='{pulls, throttled, held, declined_until}',
129
+ ),
130
+ )
131
+
132
+ def __init__(
133
+ self,
134
+ grant_store: grants_mod.GrantStore,
135
+ budget: Budget,
136
+ capacity: Optional[Capacity],
137
+ propose: Callable[..., tuple[str, Any]],
138
+ pull: Callable[..., SyncSessionResult],
139
+ strain: Optional[StrainLedger] = None,
140
+ clock: Optional[Callable[[], int]] = None,
141
+ node: Optional[Any] = None,
142
+ ) -> None:
143
+ super().__init__(node, clock)
144
+ self.grants: grants_mod.GrantStore = grant_store
145
+ self.budget: Budget = budget
146
+ self.capacity: Optional[Capacity] = capacity
147
+ self.propose: Callable[..., tuple[str, Any]] = propose
148
+ self.pull: Callable[..., SyncSessionResult] = pull
149
+ self.strain: Optional[StrainLedger] = strain
150
+ self._last_pull: st.Table[tuple[str, str], int] # (peer, shard) -> when a session last ran
151
+ self._declined: st.Table[tuple[str, str], int] # (peer, shard) -> when a declined proposal may be tried again
152
+ self._throttled: st.Table[tuple[str, str], tuple[int, str]] # (peer, shard) -> (epoch, why) last recorded
153
+ self._shed: st.Table[tuple[str, str], bool] # (peer, shard) -> whether the pull is held under the ceiling
154
+
155
+ def tick(self, now: int) -> list[tuple[Any, ...]]:
156
+ did: list[tuple[Any, ...]] = []
157
+ for p in self.config('peers') or []:
158
+ key = (p['pubkey'], p['shard'])
159
+ held = self.grants.get(p['pubkey'], grants_mod.RECEIVED)
160
+ last = self._last_pull.get(key)
161
+ if held is not None and held['status'] == 'live':
162
+ # The budget is a ceiling, not a timetable. `conn_rate` is a
163
+ # rate limit - so many sessions per window - and a node is
164
+ # entitled to spend it as fast as it likes up to the limit.
165
+ # An earlier draft of this spaced the remaining sessions
166
+ # evenly across the remaining window, which is a politeness
167
+ # policy nothing asked for and which slowed exactly the node a
168
+ # tier promotion was meant to speed up.
169
+ #
170
+ # So: a spent budget is a hard stop, and the cadence is a
171
+ # preference whose DEFAULT is derived from the grant -
172
+ # window / conn_rate. An unconfigured node therefore paces at
173
+ # the rate it was granted, and a promotion from four sessions
174
+ # an hour to twelve moves it from 900s to 300s by itself,
175
+ # where before the two agreed only by the coincidence that
176
+ # 3600/4 is the configured 900.
177
+ #
178
+ # The hard stop is also what closes a defect. `_last_pull` is
179
+ # in memory, so after a restart `last is None` and no cadence
180
+ # gate can fire - the node pulled immediately however recently
181
+ # it had pulled, spending budget the window had already
182
+ # allocated. Three restarts inside window 497614 is how that
183
+ # window came to carry more sessions than the grant allowed.
184
+ # `sessions_left` reads the meter, which is persisted and is
185
+ # the record the peer prices, so a restart inherits what was
186
+ # spent instead of forgetting it.
187
+ left, closes = self.budget.sessions_left(p['pubkey'], now)
188
+ if left <= 0:
189
+ # Spent: pace, do not attempt. Before this the node tried
190
+ # anyway and `throttled()` below refused it, recording
191
+ # "the grant covers no session this window" for what is
192
+ # ordinary pacing. That path stays as the backstop for a
193
+ # real overrun on another counter.
194
+ continue
195
+ every = self._cadence(p)
196
+ if last is not None and now - last < int(every):
197
+ continue
198
+ if held is None or held['status'] != 'live':
199
+ # No grant, or one a terminate zeroed. `terminate` leaves the
200
+ # revoked row so a restart knows the peering ended, and `peer`
201
+ # afterwards is the operator saying it should begin again; a
202
+ # row that is not live is as much a reason to propose as none,
203
+ # and pull_every is a cadence for pulling, not for asking. A
204
+ # decline still holds its own retry below.
205
+ if now < self._declined.get(key, 0):
206
+ continue
207
+ try:
208
+ kw = {'timeout': p['timeout']} if 'timeout' in p else {}
209
+ try:
210
+ status, detail = self.propose(p['pubkey'], p['shard'], p['filter'], **kw)
211
+ except TypeError:
212
+ status, detail = self.propose(p['pubkey'], p['shard'], p['filter'])
213
+ except Exception as e:
214
+ # A peer with no descriptor yet raised out of the whole tick,
215
+ # so every other peer's receipts waited on it.
216
+ self.refusals.refuse('propose', 'the handshake raised', subject=p['pubkey'],
217
+ detail={'shard': p['shard'],
218
+ 'error': '%s: %s' % (type(e).__name__, e)}, at=now)
219
+ did.append(('propose failed', p['pubkey'], '%s: %s' % (type(e).__name__, e)))
220
+ self._last_pull[key] = now
221
+ continue
222
+ did.append(('proposed', p['pubkey'], status))
223
+ if status != 'peered':
224
+ self._last_pull[key] = now
225
+ retry = int(self.strain.reserve(DECLINE_RETRY)) if self.strain is not None else DECLINE_RETRY
226
+ self._declined[key] = now + retry
227
+ self.refusals.refuse('propose', 'the peer declined', subject=p['pubkey'],
228
+ detail={'shard': p['shard'], 'status': status,
229
+ 'retry_at': self._declined[key]}, at=now)
230
+ continue
231
+ self._declined.pop(key, None)
232
+ if self.capacity is not None and self.capacity.leafing():
233
+ # The last rung. A node over its own ceiling stops pulling, which
234
+ # is the only load it chooses. It keeps answering receipts and
235
+ # complaints, because accounting is what makes a shed legible
236
+ # rather than a partition.
237
+ if self._shed.get(key) is not True:
238
+ self._shed[key] = True
239
+ self.refusals.refuse('pull', 'this node is over its own ceiling',
240
+ subject=p['pubkey'], detail={'shard': p['shard']}, at=now)
241
+ did.append(('pull held', p['pubkey'], 'this node is over its own ceiling'))
242
+ self._last_pull[key] = now
243
+ continue
244
+ self._shed.pop(key, None)
245
+ # The grant this node holds is read before it acts, not only after.
246
+ # pull_every is the operator's ceiling; the grant's is the lower one.
247
+ why = self.budget.throttled(p['pubkey'], now)
248
+ if why is not None:
249
+ epoch = self.budget.epoch(p['pubkey'], grants_mod.RECEIVED, now)
250
+ if self._throttled.get(key) != (epoch, why):
251
+ self._throttled[key] = (epoch, why)
252
+ # The reason is the category and the numbers are the detail:
253
+ # "sync_ranges 48 over the grant's 40" as a reason would put
254
+ # a new key in the counter for every window that overran.
255
+ self.refusals.refuse('pull', 'the grant covers no session this window',
256
+ subject=p['pubkey'],
257
+ detail={'shard': p['shard'], 'epoch': epoch, 'over': why}, at=now)
258
+ did.append(('throttled', p['pubkey'], why))
259
+ self._last_pull[key] = now
260
+ continue
261
+ try:
262
+ res = self.pull(p['pubkey'], p['shard'], p['filter'])
263
+ did.append(('pulled', p['pubkey'], res['status'], res['counters'].get('events_in', 0)))
264
+ except Exception as e:
265
+ self.refusals.refuse('pull', 'the session raised', subject=p['pubkey'],
266
+ detail={'shard': p['shard'],
267
+ 'error': '%s: %s' % (type(e).__name__, e)}, at=now)
268
+ did.append(('pull failed', p['pubkey'], str(e)))
269
+ self._last_pull[key] = now
270
+ return did
271
+
272
+ # ---- being watched --------------------------------------------------------
273
+ def throttled(self) -> list[ThrottledRow]:
274
+ """The pulls this node is holding back this window, and until when.
275
+
276
+ A throttled peer goes quiet in both directions: no sessions, so no
277
+ usage, so no receipts due either way. The relay line then says "no
278
+ event yet" and an operator reads a dead node. Four nodes on three
279
+ machines produced exactly that reading for twenty minutes. Nothing was
280
+ wrong; the grant's ceiling had been reached. Say so, and say when it
281
+ turns.
282
+ """
283
+ out: list[ThrottledRow] = []
284
+ for (pk, shard), (epoch, why) in sorted(self._throttled.items()):
285
+ try:
286
+ w = self.budget._window_of(pk, grants_mod.RECEIVED)
287
+ if self.budget.epoch(pk, grants_mod.RECEIVED) != epoch:
288
+ continue
289
+ until = accounting.window_bounds(epoch, w)[1]
290
+ except Exception:
291
+ continue
292
+ out.append({'peer': pk, 'shard': shard, 'why': why, 'until': until})
293
+ return out
294
+
295
+ def _cadence(self, p: Mapping[str, Any]) -> int:
296
+ """Seconds between pulls of this peer: the operator's number, or the
297
+ grant's own rate when there is none.
298
+
299
+ Here rather than inline because `state()` reports it and the loop
300
+ obeys it, and the two read a hardcoded 900 and a derived value
301
+ respectively - so an unconfigured node pulled every window/conn_rate
302
+ while telling its operator 900. Two places agreeing by hand is how
303
+ that happened; one method is the fix.
304
+ """
305
+ every = p.get('pull_every')
306
+ if every is not None:
307
+ return int(every)
308
+ allowed, w = self.budget.granted_rate(p['pubkey'])
309
+ return (w // allowed) if allowed > 0 else DEFAULT_PULL_EVERY
310
+
311
+ def state(self) -> SchedulerStateReport:
312
+ return {
313
+ 'pulls': [
314
+ {
315
+ 'peer': p['pubkey'][:16],
316
+ 'shard': p['shard'],
317
+ 'every': self._cadence(p),
318
+ 'last': self._last_pull.get((p['pubkey'], p['shard'])),
319
+ }
320
+ for p in self.config('peers') or []
321
+ ],
322
+ 'throttled': self.throttled(),
323
+ 'held': sorted('%s %s' % (pk[:16], shard) for (pk, shard), v in self._shed.items() if v),
324
+ 'declined_until': {
325
+ '%s %s' % (pk[:16], shard): at
326
+ for (pk, shard), at in self._declined.items()
327
+ },
328
+ }
329
+
330
+ def _live_with(self, peer: str) -> bool:
331
+ held = self.grants.get(peer, grants_mod.RECEIVED)
332
+ return held is not None and held['status'] == 'live'
333
+
334
+ def trouble(self) -> list[str]:
335
+ out: list[str] = []
336
+ now = self.clock()
337
+ failed = self.refusals.within(self.TROUBLE_WINDOW, now, kind='pull', why='the session raised')
338
+ if failed:
339
+ out.append('%d scheduled pull(s) failed: %s'
340
+ % (len(failed), ', '.join(sorted({(r.subject or '')[:16] for r in failed}))))
341
+ raised = self.refusals.within(self.TROUBLE_WINDOW, now, kind='propose', why='the handshake raised')
342
+ # A proposal that got no answer because the peer was not up yet - the
343
+ # first minute of a mesh deploy, every time - is answered by the
344
+ # retry. Once the grant is live the failure is history, not trouble.
345
+ raised = [r for r in raised if not self._live_with(r.subject or '')]
346
+ if raised:
347
+ out.append('%d peering(s) could not be proposed: %s'
348
+ % (len(raised), ', '.join(sorted({(r.subject or '')[:16] for r in raised}))))
349
+ held = sorted(k for k, v in self._shed.items() if v)
350
+ if held:
351
+ out.append('%d scheduled pull(s) held because this node is over its own ceiling: %s'
352
+ % (len(held), ', '.join('%s %s' % (pk[:16], shard) for pk, shard in held)))
353
+ return out
@@ -0,0 +1,21 @@
1
+ """Types and records for the Scheduler compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional, TypedDict
5
+ from node.interfaces import ThrottledRow
6
+
7
+
8
+ class SchedulerPullStatus(TypedDict, total=False):
9
+ """Status of one scheduled pull."""
10
+ peer: str
11
+ shard: str
12
+ every: int
13
+ last: Optional[int]
14
+
15
+
16
+ class SchedulerStateReport(TypedDict, total=False):
17
+ """The report Scheduler.state() gives of itself."""
18
+ pulls: list[SchedulerPullStatus]
19
+ throttled: list[ThrottledRow]
20
+ held: list[str]
21
+ declined_until: dict[str, int]
@@ -0,0 +1,17 @@
1
+ """The `updates` compartment: check-and-report. See docs/updates.md, Tier 2.
2
+
3
+ release.py a kind 9900 release event -> ReleaseInfo, or a refusal reason
4
+ store.py what was seen and acknowledged, durable across restarts
5
+ core.py Updates, the compartment: check, trouble, commands
6
+
7
+ What it does: asks the channel relays for release events signed by the key
8
+ the operator pinned, keeps the newest one that speaks this node's protocol,
9
+ and says so where the node says things. What it does not do: fetch an
10
+ artefact, run anything, or treat a track policy as more than a refused
11
+ config line. That is the apply tier, and it is not here.
12
+ """
13
+ from fleetmesh_core.peering.release import ReleaseInfo, parse_release_event, parse_revocation
14
+ from .store import UpdateStore
15
+ from .core import Updates
16
+
17
+ __all__ = ['ReleaseInfo', 'parse_release_event', 'parse_revocation', 'UpdateStore', 'Updates']