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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- 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']
|