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,499 @@
|
|
|
1
|
+
"""A sidecar, and the bridge compartment that is the node's only way to reach it.
|
|
2
|
+
|
|
3
|
+
Three words, kept apart because the node had one word for two things (audit
|
|
4
|
+
2026-10-07-3, finding "two things called sidecar"):
|
|
5
|
+
|
|
6
|
+
compartment in-process. Built, wired, started, ticked, restarted and
|
|
7
|
+
retired by the Registry; its whole lifecycle is the node's.
|
|
8
|
+
|
|
9
|
+
sidecar a separate process the node reaches but does not contain:
|
|
10
|
+
fips (Rust), libp2p (sixty-three packages, trio, will not
|
|
11
|
+
install on 3.14), bacalhau, grasp, strfry. Kept out because its
|
|
12
|
+
codebase, its dependencies, its runtime or its licence do not
|
|
13
|
+
belong in `node/` - which is standard library plus
|
|
14
|
+
`cryptography`, so that a second implementer never inherits
|
|
15
|
+
any of it. The node never starts or supervises one
|
|
16
|
+
(`supervised` is False, and stays False until code honours it).
|
|
17
|
+
|
|
18
|
+
bridge the ONE compartment that fronts one sidecar. Everything on the
|
|
19
|
+
node that needs a sidecar goes through its bridge - names it in
|
|
20
|
+
`requires`/`uses` and calls `bridge.call()` or a method the
|
|
21
|
+
bridge offers - and nothing else opens the sidecar's socket.
|
|
22
|
+
So the sidecar's settings (PARAMS: endpoint, timeout), what it
|
|
23
|
+
says (CHANNELS), what it measures (METRICS), what an operator
|
|
24
|
+
can ask of it (COMMANDS) and how it fails (LIFECYCLE) are
|
|
25
|
+
declared once, on the bridge, the way every other compartment
|
|
26
|
+
declares them.
|
|
27
|
+
|
|
28
|
+
What a bridge gets from this base, so that every sidecar is reached the same way:
|
|
29
|
+
|
|
30
|
+
- `SIDECAR`, a manifest: the protocol it speaks (`family/major`), what it
|
|
31
|
+
wraps upstream, the command that runs it standalone, and the request that
|
|
32
|
+
proves it is there.
|
|
33
|
+
- a link state - unknown, up, down, incompatible - moved only by `call()`,
|
|
34
|
+
said once each way on three declared channels and counted:
|
|
35
|
+
`<name>.sidecar.reachable|unreachable|incompatible`.
|
|
36
|
+
- the version gate (audit S1): every reply carries the protocol the sidecar
|
|
37
|
+
speaks, and a different family or major is refused - a bridge that read a
|
|
38
|
+
`/2` reply as `/1` would be wrong silently. On a link that is not up the
|
|
39
|
+
manifest's `probe` goes first as a handshake, so a sidecar that speaks
|
|
40
|
+
another major is refused before it is sent anything that carries data.
|
|
41
|
+
- two commands on the control socket: `<name>.sidecar` (the manifest and the
|
|
42
|
+
link now) and `<name>.probe` (ask the sidecar now).
|
|
43
|
+
- `Lifecycle(restart=True)`: a bridge holds a link and nothing more, so a
|
|
44
|
+
restart forgets the link and the next call finds out afresh. A wedged link
|
|
45
|
+
that trips the breaker is restarted by its quarantine trial.
|
|
46
|
+
- trouble when the sidecar is wanted and down or incompatible, naming what
|
|
47
|
+
this node loses (`CONSEQUENCE`).
|
|
48
|
+
|
|
49
|
+
Two ways a sidecar runs, both described by the same manifest:
|
|
50
|
+
|
|
51
|
+
bridge mode the sidecar is already running - ours, or one on the market
|
|
52
|
+
(a strfry, a bacalhau) - and the bridge compartment in the
|
|
53
|
+
node talks to it.
|
|
54
|
+
standalone `SIDECAR.standalone` is the command that runs it on its own,
|
|
55
|
+
beside the node or on another host; the bridge does not care
|
|
56
|
+
who started it, only that its endpoint answers.
|
|
57
|
+
|
|
58
|
+
The transport is a unix socket carrying one JSON object per line, which is what
|
|
59
|
+
announce, p2p and fips already speak. HTTP (the class-A manifests in
|
|
60
|
+
node/sidecars: grasp, ncontainer, magpie) arrives with the first bridge that
|
|
61
|
+
needs it; `TRANSPORTS` says what is implemented and a manifest naming another
|
|
62
|
+
is refused at import rather than at the first call.
|
|
63
|
+
|
|
64
|
+
Named `sidecar_bridge` because `node/bridge` is the Universal Nostr Relay
|
|
65
|
+
Bridge, a compartment called `bridge` that fronts relays rather than a sidecar.
|
|
66
|
+
A package rather than a root module for the reason node/lifecycle is one.
|
|
67
|
+
Standard library only.
|
|
68
|
+
"""
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
import dataclasses
|
|
72
|
+
import json
|
|
73
|
+
import socket
|
|
74
|
+
import threading
|
|
75
|
+
import time
|
|
76
|
+
from typing import Any, Callable, Mapping, Optional, TypedDict
|
|
77
|
+
|
|
78
|
+
from .. import channels as ch_mod
|
|
79
|
+
from .. import metrics as mx_mod
|
|
80
|
+
from . import lifecycle as lifecycle_mod
|
|
81
|
+
from .base import Compartment
|
|
82
|
+
from .command import Command
|
|
83
|
+
|
|
84
|
+
TRANSPORTS: tuple[str, ...] = ('unix',)
|
|
85
|
+
MAX_LINE: int = 1 << 20
|
|
86
|
+
|
|
87
|
+
# The link, as the bridge last found it.
|
|
88
|
+
UNKNOWN = 'unknown' # not asked since start (or restart)
|
|
89
|
+
UP = 'up' # the last call was answered, in a protocol this bridge speaks
|
|
90
|
+
DOWN = 'down' # the last call was not answered, or was refused
|
|
91
|
+
INCOMPATIBLE = 'incompatible' # answered, in a protocol family or major this bridge does not speak
|
|
92
|
+
LINKS: tuple[str, ...] = (UNKNOWN, UP, DOWN, INCOMPATIBLE)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SidecarError(Exception):
|
|
96
|
+
"""No sidecar, a sidecar that will not answer, or one that says no."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SidecarIncompatible(SidecarError):
|
|
100
|
+
"""A sidecar that answered in a protocol this bridge does not speak."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SidecarRecord(TypedDict):
|
|
104
|
+
name: str
|
|
105
|
+
protocol: str
|
|
106
|
+
upstream: str
|
|
107
|
+
transport: str
|
|
108
|
+
standalone: str
|
|
109
|
+
probe: str
|
|
110
|
+
supervised: bool
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class LinkRecord(TypedDict):
|
|
114
|
+
sidecar: SidecarRecord
|
|
115
|
+
endpoint: str
|
|
116
|
+
link: str
|
|
117
|
+
since: int
|
|
118
|
+
protocol: str # what the sidecar last said it speaks; '' when it did not say
|
|
119
|
+
last_error: str
|
|
120
|
+
calls: int
|
|
121
|
+
failures: int
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ProbeRecord(TypedDict, total=False):
|
|
125
|
+
"""What `<name>.probe` answers: the link, and the status or the error."""
|
|
126
|
+
link: str
|
|
127
|
+
protocol: str
|
|
128
|
+
status: object
|
|
129
|
+
error: str
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclasses.dataclass(frozen=True)
|
|
133
|
+
class Sidecar:
|
|
134
|
+
"""What a bridge fronts. Declared on the bridge as `SIDECAR`.
|
|
135
|
+
|
|
136
|
+
name the sidecar's name; also the bridge compartment's
|
|
137
|
+
protocol `family/major` this bridge speaks, e.g. 'fleetmesh-announce/1'
|
|
138
|
+
upstream what the sidecar wraps, for a person: 'py-libp2p gossipsub'
|
|
139
|
+
transport how it is reached; one of TRANSPORTS
|
|
140
|
+
standalone the command that runs it on its own, for the docs and the
|
|
141
|
+
`sidecar` command; '' when it has none
|
|
142
|
+
probe the request that proves it is there and returns its status
|
|
143
|
+
supervised False: the node depends on it and never starts or restarts
|
|
144
|
+
it. Refused when True until the code that would honour it
|
|
145
|
+
exists - a declared supervisor nothing runs is worse than none.
|
|
146
|
+
"""
|
|
147
|
+
name: str
|
|
148
|
+
protocol: str
|
|
149
|
+
upstream: str
|
|
150
|
+
transport: str = 'unix'
|
|
151
|
+
standalone: str = ''
|
|
152
|
+
probe: str = 'status'
|
|
153
|
+
supervised: bool = False
|
|
154
|
+
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
family, _, major = self.protocol.rpartition('/')
|
|
157
|
+
if not family or not major.isdigit():
|
|
158
|
+
raise ValueError('sidecar %s: protocol is family/major, got %r' % (self.name, self.protocol))
|
|
159
|
+
if self.transport not in TRANSPORTS:
|
|
160
|
+
raise ValueError('sidecar %s: transport %r is not implemented (have: %s)'
|
|
161
|
+
% (self.name, self.transport, ', '.join(TRANSPORTS)))
|
|
162
|
+
if self.supervised:
|
|
163
|
+
raise ValueError('sidecar %s: supervised=True is not honoured yet; the node depends on a '
|
|
164
|
+
'sidecar and never starts it' % self.name)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def family(self) -> str:
|
|
168
|
+
return self.protocol.rpartition('/')[0]
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def major(self) -> str:
|
|
172
|
+
return self.protocol.rpartition('/')[2]
|
|
173
|
+
|
|
174
|
+
def compatible(self, said: str) -> bool:
|
|
175
|
+
"""Whether a sidecar that says it speaks `said` speaks this bridge's
|
|
176
|
+
protocol. Same family, same major; a minor (`/1.3`) is additive. An
|
|
177
|
+
unstated protocol is accepted - the sidecars predate the field - and
|
|
178
|
+
the link records that it was not stated."""
|
|
179
|
+
if not said:
|
|
180
|
+
return True
|
|
181
|
+
family, _, version = said.rpartition('/')
|
|
182
|
+
return family == self.family and version.split('.')[0] == self.major
|
|
183
|
+
|
|
184
|
+
def describe(self) -> SidecarRecord:
|
|
185
|
+
return {'name': self.name, 'protocol': self.protocol, 'upstream': self.upstream,
|
|
186
|
+
'transport': self.transport, 'standalone': self.standalone, 'probe': self.probe,
|
|
187
|
+
'supervised': self.supervised}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def jsonline_call(path: str, cmd: str, timeout: float = 5.0, **args: Any) -> tuple[Any, str]:
|
|
191
|
+
"""One request, one reply, one connection, over a unix socket.
|
|
192
|
+
|
|
193
|
+
Returns (result, the protocol the reply said it speaks, or ''). Raises
|
|
194
|
+
SidecarError for all of no sidecar, a sidecar that will not answer, and a
|
|
195
|
+
sidecar that says no. The wire was announce's; it is every unix sidecar's.
|
|
196
|
+
"""
|
|
197
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
198
|
+
s.settimeout(timeout)
|
|
199
|
+
try:
|
|
200
|
+
try:
|
|
201
|
+
s.connect(path)
|
|
202
|
+
except OSError as e:
|
|
203
|
+
raise SidecarError('no sidecar at %s (%s)' % (path, e.strerror or e))
|
|
204
|
+
try:
|
|
205
|
+
s.sendall((json.dumps({'cmd': cmd, 'args': args}) + '\n').encode('utf-8'))
|
|
206
|
+
buf = b''
|
|
207
|
+
while not buf.endswith(b'\n'):
|
|
208
|
+
chunk = s.recv(65536)
|
|
209
|
+
if not chunk:
|
|
210
|
+
break
|
|
211
|
+
buf += chunk
|
|
212
|
+
if len(buf) > MAX_LINE:
|
|
213
|
+
raise SidecarError('the sidecar answered with more than %d bytes' % MAX_LINE)
|
|
214
|
+
except socket.timeout:
|
|
215
|
+
raise SidecarError('the sidecar did not answer within %.0fs' % timeout)
|
|
216
|
+
except OSError as e:
|
|
217
|
+
raise SidecarError('the sidecar closed the socket (%s)' % (e.strerror or e))
|
|
218
|
+
finally:
|
|
219
|
+
s.close()
|
|
220
|
+
if not buf:
|
|
221
|
+
raise SidecarError('the sidecar closed the socket without answering')
|
|
222
|
+
try:
|
|
223
|
+
reply = json.loads(buf.decode('utf-8'))
|
|
224
|
+
except (ValueError, UnicodeDecodeError) as e:
|
|
225
|
+
raise SidecarError('the sidecar answered something that is not JSON (%s)' % e)
|
|
226
|
+
if not isinstance(reply, dict):
|
|
227
|
+
raise SidecarError('a reply is an object')
|
|
228
|
+
said = str(reply.get('protocol') or '')
|
|
229
|
+
if not reply.get('ok'):
|
|
230
|
+
raise SidecarError(str(reply.get('error') or 'refused'))
|
|
231
|
+
return reply.get('result'), said
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def probe_unix_socket(path: str, request: Mapping[str, Any], timeout: float = 1.0) -> Optional[Mapping[str, Any]]:
|
|
235
|
+
"""Send one JSON line over a unix socket and return the parsed JSON object, or None on error."""
|
|
236
|
+
try:
|
|
237
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
238
|
+
s.settimeout(timeout)
|
|
239
|
+
s.connect(path)
|
|
240
|
+
s.sendall(json.dumps(dict(request)).encode('utf-8') + b'\n')
|
|
241
|
+
from ..transport import lineproto
|
|
242
|
+
resp = lineproto.read_json_line(s, cap=MAX_LINE)
|
|
243
|
+
s.close()
|
|
244
|
+
if isinstance(resp, dict):
|
|
245
|
+
return resp
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---- what every bridge declares, derived from its manifest ---------------------
|
|
252
|
+
def bridge_channels(name: str) -> tuple[ch_mod.Channel, ...]:
|
|
253
|
+
return (
|
|
254
|
+
ch_mod.Channel(
|
|
255
|
+
name='%s.sidecar.reachable' % name,
|
|
256
|
+
description='The %s sidecar answered, in a protocol this bridge speaks.' % name,
|
|
257
|
+
level=ch_mod.INFO, wire=lambda endpoint, protocol: {'endpoint': endpoint, 'protocol': protocol},
|
|
258
|
+
say='sidecar up at {endpoint} ({protocol})', tags=(name, 'sidecar', 'bridge')),
|
|
259
|
+
ch_mod.Channel(
|
|
260
|
+
name='%s.sidecar.unreachable' % name,
|
|
261
|
+
description='The %s sidecar did not answer, or refused; what needs it waits.' % name,
|
|
262
|
+
level=ch_mod.NOTE, wire=lambda why, endpoint: {'why': why, 'endpoint': endpoint},
|
|
263
|
+
say='sidecar not reachable at {endpoint} ({why})', tags=(name, 'sidecar', 'bridge')),
|
|
264
|
+
ch_mod.Channel(
|
|
265
|
+
name='%s.sidecar.incompatible' % name,
|
|
266
|
+
description='The %s sidecar answered in a protocol this bridge does not speak.' % name,
|
|
267
|
+
level=ch_mod.TROUBLE, wire=lambda said, wants: {'said': said, 'wants': wants},
|
|
268
|
+
say='sidecar speaks {said}; this bridge speaks {wants}', tags=(name, 'sidecar', 'bridge')),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def bridge_metrics(name: str) -> tuple[mx_mod.Metric, ...]:
|
|
273
|
+
return (
|
|
274
|
+
mx_mod.Metric(name='%s_sidecar_reachable' % name,
|
|
275
|
+
description='Times the %s sidecar was found reachable.' % name,
|
|
276
|
+
kind=mx_mod.COUNTER, counts='%s.sidecar.reachable' % name, tags=(name, 'sidecar')),
|
|
277
|
+
mx_mod.Metric(name='%s_sidecar_unreachable' % name,
|
|
278
|
+
description='Times the %s sidecar was found unreachable.' % name,
|
|
279
|
+
kind=mx_mod.COUNTER, counts='%s.sidecar.unreachable' % name, tags=(name, 'sidecar')),
|
|
280
|
+
mx_mod.Metric(name='%s_sidecar_incompatible' % name,
|
|
281
|
+
description='Times the %s sidecar answered in a protocol this bridge does not speak.' % name,
|
|
282
|
+
kind=mx_mod.COUNTER, counts='%s.sidecar.incompatible' % name, tags=(name, 'sidecar')),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def bridge_commands(sc: Sidecar) -> tuple[Command, ...]:
|
|
287
|
+
return (
|
|
288
|
+
Command(
|
|
289
|
+
key='sidecar', name='%s sidecar' % sc.name.capitalize(),
|
|
290
|
+
description='the sidecar this bridge fronts - what it wraps, the protocol it must speak, how to '
|
|
291
|
+
'run it standalone - and the link to it as last found',
|
|
292
|
+
tags=(sc.name, 'sidecar', 'bridge'), permission='%s/sidecar' % sc.name,
|
|
293
|
+
run='bridge_state',
|
|
294
|
+
reply='{sidecar, endpoint, link, since, protocol, last_error, calls, failures}'),
|
|
295
|
+
Command(
|
|
296
|
+
key='probe', name='Probe %s sidecar' % sc.name,
|
|
297
|
+
description='ask the sidecar now (its `%s` request) rather than wait for the next tick' % sc.probe,
|
|
298
|
+
tags=(sc.name, 'sidecar', 'bridge'), permission='%s/probe' % sc.name,
|
|
299
|
+
process=(sc.name,), run='probe',
|
|
300
|
+
reply='{link, protocol, status | error}'),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _merged(base: tuple[Any, ...], own: tuple[Any, ...], key: Callable[[Any], str]) -> tuple[Any, ...]:
|
|
305
|
+
"""The bridge's own declarations, then the base's it did not declare itself."""
|
|
306
|
+
have = {key(x) for x in own}
|
|
307
|
+
return tuple(own) + tuple(x for x in base if key(x) not in have)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class Bridge(Compartment, abstract=True):
|
|
311
|
+
"""The compartment that fronts one sidecar. Subclass, declare `SIDECAR`,
|
|
312
|
+
implement `endpoint()`, and reach the sidecar only through `call()`.
|
|
313
|
+
|
|
314
|
+
Optional hooks: `wanted()` (False turns the link checks and trouble off,
|
|
315
|
+
e.g. a sidecar the config has not enabled) and `forget()` (what the bridge
|
|
316
|
+
must forget when its link is reset, at start and on every restart).
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
SIDECAR: Optional[Sidecar] = None
|
|
320
|
+
# What this node loses while the sidecar is down, for the trouble line.
|
|
321
|
+
CONSEQUENCE: str = 'what needs it waits'
|
|
322
|
+
LIFECYCLE: lifecycle_mod.Lifecycle = lifecycle_mod.Lifecycle(restart=True)
|
|
323
|
+
timeout: float = 5.0
|
|
324
|
+
|
|
325
|
+
def __init_subclass__(cls, **kw: Any) -> None:
|
|
326
|
+
sc = cls.__dict__.get('SIDECAR')
|
|
327
|
+
if isinstance(sc, Sidecar):
|
|
328
|
+
if cls.name and cls.name != sc.name:
|
|
329
|
+
raise TypeError('bridge %s fronts sidecar %s: a bridge is named for its sidecar'
|
|
330
|
+
% (cls.name, sc.name))
|
|
331
|
+
name = sc.name
|
|
332
|
+
cls.CHANNELS = _merged(bridge_channels(name), tuple(cls.__dict__.get('CHANNELS', ())),
|
|
333
|
+
lambda c: c.name)
|
|
334
|
+
cls.METRICS = _merged(bridge_metrics(name), tuple(cls.__dict__.get('METRICS', ())),
|
|
335
|
+
lambda m: m.name)
|
|
336
|
+
cls.COMMANDS = _merged(bridge_commands(sc), tuple(cls.__dict__.get('COMMANDS', ())),
|
|
337
|
+
lambda c: c.key)
|
|
338
|
+
super().__init_subclass__(**kw)
|
|
339
|
+
|
|
340
|
+
def __init__(self, node: Any, clock: Optional[Callable[[], int]] = None) -> None:
|
|
341
|
+
super().__init__(node, clock)
|
|
342
|
+
self._link_lock: threading.Lock = threading.Lock()
|
|
343
|
+
self._reset_link()
|
|
344
|
+
|
|
345
|
+
# ---- what a subclass provides ---------------------------------------------
|
|
346
|
+
@property
|
|
347
|
+
def sidecar(self) -> Sidecar:
|
|
348
|
+
if self.SIDECAR is None:
|
|
349
|
+
raise TypeError('%s declares no SIDECAR' % type(self).__name__)
|
|
350
|
+
return self.SIDECAR
|
|
351
|
+
|
|
352
|
+
def endpoint(self) -> str:
|
|
353
|
+
raise NotImplementedError('%s must say where its sidecar listens' % type(self).__name__)
|
|
354
|
+
|
|
355
|
+
def wanted(self) -> bool:
|
|
356
|
+
return True
|
|
357
|
+
|
|
358
|
+
def forget(self) -> None:
|
|
359
|
+
"""What a reset link must take with it. Nothing, by default."""
|
|
360
|
+
|
|
361
|
+
# ---- the link --------------------------------------------------------------
|
|
362
|
+
def _reset_link(self) -> None:
|
|
363
|
+
self.link: str = UNKNOWN
|
|
364
|
+
self.link_since: int = self._now()
|
|
365
|
+
self.link_protocol: str = ''
|
|
366
|
+
self.link_error: str = ''
|
|
367
|
+
self.calls: int = 0
|
|
368
|
+
self.call_failures: int = 0
|
|
369
|
+
self._up: Optional[bool] = None # the reachability the journal was last told
|
|
370
|
+
self._said: Optional[str] = None # the last unreachable reason it was told
|
|
371
|
+
|
|
372
|
+
def start(self) -> None:
|
|
373
|
+
# A fresh link: the next call finds out. Nothing is dialled here, so a
|
|
374
|
+
# sidecar that hangs cannot hold the node's start.
|
|
375
|
+
with self._link_lock:
|
|
376
|
+
self._reset_link()
|
|
377
|
+
self.forget()
|
|
378
|
+
|
|
379
|
+
def stop(self) -> Any:
|
|
380
|
+
with self._link_lock:
|
|
381
|
+
self._reset_link()
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
def _now(self) -> int:
|
|
385
|
+
return int(getattr(self, 'clock', time.time)())
|
|
386
|
+
|
|
387
|
+
def call(self, cmd: str, **args: Any) -> Any:
|
|
388
|
+
"""The only way to the sidecar. Moves the link and says so once each way.
|
|
389
|
+
|
|
390
|
+
On a link that is not up, the manifest's `probe` request goes first,
|
|
391
|
+
as a handshake: the version gate must refuse BEFORE a request carrying
|
|
392
|
+
data reaches a sidecar that would misread it, not after. One extra
|
|
393
|
+
round trip per (re)connection, none while the link is up.
|
|
394
|
+
|
|
395
|
+
Raises SidecarError (SidecarIncompatible for a protocol this bridge
|
|
396
|
+
does not speak); the caller decides what a missing sidecar costs it.
|
|
397
|
+
"""
|
|
398
|
+
if self.link != UP and cmd != self.sidecar.probe:
|
|
399
|
+
self._exchange(self.sidecar.probe, {})
|
|
400
|
+
return self._exchange(cmd, args)
|
|
401
|
+
|
|
402
|
+
def _exchange(self, cmd: str, args: Mapping[str, Any]) -> Any:
|
|
403
|
+
now = self._now()
|
|
404
|
+
kw = dict(args)
|
|
405
|
+
to = float(kw.pop('timeout', self.timeout))
|
|
406
|
+
try:
|
|
407
|
+
result, said = jsonline_call(self.endpoint(), cmd, timeout=to, **kw)
|
|
408
|
+
except SidecarError as e:
|
|
409
|
+
self.call_failures += 1
|
|
410
|
+
self._down(str(e), now)
|
|
411
|
+
raise
|
|
412
|
+
if not self.sidecar.compatible(said):
|
|
413
|
+
self.call_failures += 1
|
|
414
|
+
self._incompatible(said, now)
|
|
415
|
+
raise SidecarIncompatible('the %s sidecar speaks %s; this bridge speaks %s'
|
|
416
|
+
% (self.sidecar.name, said, self.sidecar.protocol))
|
|
417
|
+
self.calls += 1
|
|
418
|
+
self.link_protocol = said
|
|
419
|
+
self._reachable()
|
|
420
|
+
return result
|
|
421
|
+
|
|
422
|
+
def probe(self) -> ProbeRecord:
|
|
423
|
+
"""Ask the sidecar now. Never raises: the answer says what happened."""
|
|
424
|
+
try:
|
|
425
|
+
status = self.call(self.sidecar.probe)
|
|
426
|
+
except SidecarError as e:
|
|
427
|
+
return {'link': self.link, 'protocol': self.link_protocol, 'error': str(e)}
|
|
428
|
+
return {'link': self.link, 'protocol': self.link_protocol, 'status': status}
|
|
429
|
+
|
|
430
|
+
def _move_link(self, to: str, now: int, error: str = '') -> bool:
|
|
431
|
+
with self._link_lock:
|
|
432
|
+
moved = to != self.link
|
|
433
|
+
if moved:
|
|
434
|
+
self.link, self.link_since = to, now
|
|
435
|
+
self.link_error = error
|
|
436
|
+
return moved
|
|
437
|
+
|
|
438
|
+
def _down(self, why: str, now: int) -> None:
|
|
439
|
+
self._move_link(DOWN, now, why)
|
|
440
|
+
if self._up is not False or self._said != why:
|
|
441
|
+
self.say('%s.sidecar.unreachable' % self.sidecar.name, why, self.endpoint())
|
|
442
|
+
self._said = why
|
|
443
|
+
self.refusals.refuse('sidecar', 'not reachable', subject=self.endpoint(),
|
|
444
|
+
detail={'error': why}, at=now)
|
|
445
|
+
self._up = False
|
|
446
|
+
self.forget() # it must be told again what it was told before
|
|
447
|
+
|
|
448
|
+
def _incompatible(self, said: str, now: int) -> None:
|
|
449
|
+
if self._move_link(INCOMPATIBLE, now, 'speaks %s' % said):
|
|
450
|
+
self.say('%s.sidecar.incompatible' % self.sidecar.name, said, self.sidecar.protocol)
|
|
451
|
+
self.refusals.refuse('sidecar', 'incompatible', subject=self.endpoint(),
|
|
452
|
+
detail={'said': said, 'wants': self.sidecar.protocol}, at=now)
|
|
453
|
+
self._up = False
|
|
454
|
+
self.forget()
|
|
455
|
+
|
|
456
|
+
def _reachable(self) -> None:
|
|
457
|
+
self._move_link(UP, self._now())
|
|
458
|
+
if self._up is not True:
|
|
459
|
+
self.say('%s.sidecar.reachable' % self.sidecar.name, self.endpoint(),
|
|
460
|
+
self.link_protocol or 'protocol unstated')
|
|
461
|
+
self._up = True
|
|
462
|
+
self._said = None
|
|
463
|
+
|
|
464
|
+
@property
|
|
465
|
+
def available(self) -> bool:
|
|
466
|
+
"""Whether a compartment that depends on this sidecar should try now."""
|
|
467
|
+
return self.link == UP
|
|
468
|
+
|
|
469
|
+
# ---- being watched ---------------------------------------------------------
|
|
470
|
+
def bridge_state(self) -> LinkRecord:
|
|
471
|
+
return {'sidecar': self.sidecar.describe(), 'endpoint': self.endpoint(), 'link': self.link,
|
|
472
|
+
'since': self.link_since, 'protocol': self.link_protocol, 'last_error': self.link_error,
|
|
473
|
+
'calls': self.calls, 'failures': self.call_failures}
|
|
474
|
+
|
|
475
|
+
def sections(self) -> Mapping[str, Any]:
|
|
476
|
+
return {'bridge': self.bridge_state()}
|
|
477
|
+
|
|
478
|
+
def trouble(self) -> list[str]:
|
|
479
|
+
if not self.wanted():
|
|
480
|
+
return []
|
|
481
|
+
if self.link == INCOMPATIBLE:
|
|
482
|
+
return ['the %s sidecar at %s %s; this bridge speaks %s - upgrade one of them'
|
|
483
|
+
% (self.sidecar.name, self.endpoint(), self.link_error, self.sidecar.protocol)]
|
|
484
|
+
if self.link == DOWN:
|
|
485
|
+
return ['the %s sidecar is not reachable at %s; %s'
|
|
486
|
+
% (self.sidecar.name, self.endpoint(), self.CONSEQUENCE)]
|
|
487
|
+
return []
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def bridges(compartments: Any) -> list[Bridge]:
|
|
491
|
+
"""The bridges among a node's compartments: every sidecar it reaches."""
|
|
492
|
+
return [c for c in compartments if isinstance(c, Bridge)]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
__all__ = [
|
|
496
|
+
'Bridge', 'DOWN', 'INCOMPATIBLE', 'LINKS', 'LinkRecord', 'MAX_LINE', 'ProbeRecord', 'Sidecar', 'SidecarError',
|
|
497
|
+
'SidecarIncompatible', 'SidecarRecord', 'TRANSPORTS', 'UNKNOWN', 'UP', 'bridge_channels',
|
|
498
|
+
'bridge_commands', 'bridge_metrics', 'bridges', 'jsonline_call',
|
|
499
|
+
]
|