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,522 @@
|
|
|
1
|
+
"""What this node tells the mesh it is and what it holds, as a compartment.
|
|
2
|
+
|
|
3
|
+
Two publications with one shape: a kind 30166 per endpoint for NIP-66 readers,
|
|
4
|
+
and a kind 30803 per shard for anything looking for data rather than for nodes.
|
|
5
|
+
Both are addressable, both are republished only when what they say changes, and
|
|
6
|
+
both can be refused - a discovery event that contradicts the descriptor, a
|
|
7
|
+
coverage claim a leaf may not make.
|
|
8
|
+
|
|
9
|
+
Those refusals went into the tick's return value and nowhere else. `coverage
|
|
10
|
+
claim refused` was printed once and then gone: an operator who had added a shard
|
|
11
|
+
to the config and never saw it claimed had the reason in a journal line that had
|
|
12
|
+
already scrolled, and nothing to ask.
|
|
13
|
+
|
|
14
|
+
The forward clock lives here because both of these are addressable events under
|
|
15
|
+
a stable `d`, and so is the report the reporter publishes, which borrows it.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Mapping, MutableMapping, Optional, Sequence
|
|
20
|
+
|
|
21
|
+
import dataclasses
|
|
22
|
+
from fleetmesh_core.peering import coverage
|
|
23
|
+
from fleetmesh_core.services import discovery as ref_discovery
|
|
24
|
+
from fleetmesh_core.storage import store as ref_store
|
|
25
|
+
from fleetmesh_core.wire.wire_types import HandoverLink
|
|
26
|
+
from node.compartment import stores as st
|
|
27
|
+
from node.channels import Channel, NOTE, TROUBLE
|
|
28
|
+
from node.metrics import COUNTER, Metric
|
|
29
|
+
from node.protocols.nostr import client as relay
|
|
30
|
+
from node.planes.store.sync import rules as handover
|
|
31
|
+
from node.planes.store.sync.rules import CoverageShardConfig
|
|
32
|
+
from node.interfaces import NostrEvent
|
|
33
|
+
from node.compartment import Command, Compartment, Every, Need
|
|
34
|
+
from node.config.parameter import Parameter, ParamBlock
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
from .types import DiscoveryStateReport
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _range_of(claim: NostrEvent) -> tuple[Optional[str], Optional[str]]:
|
|
41
|
+
"""The (since, until) a kind 30803 declares, as tagged."""
|
|
42
|
+
tags = {t[0]: t[1] for t in claim['tags'] if len(t) > 1 and t[0] in ('since', 'until')}
|
|
43
|
+
return tags.get('since'), tags.get('until')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclasses.dataclass(frozen=True)
|
|
47
|
+
class DiscoveryConfig(ParamBlock):
|
|
48
|
+
key = 'discovery'
|
|
49
|
+
name = 'discovery'
|
|
50
|
+
DERIVED: bool = False
|
|
51
|
+
PARAMS = (
|
|
52
|
+
Parameter(
|
|
53
|
+
key='coverage_interval', name='Coverage Publication Interval',
|
|
54
|
+
description='Seconds between coverage claim publications',
|
|
55
|
+
type=int, default=60, minimum=10,
|
|
56
|
+
tags=('discovery', 'coverage'),
|
|
57
|
+
),
|
|
58
|
+
Parameter(
|
|
59
|
+
key='discovery_interval', name='Discovery Publication Interval',
|
|
60
|
+
description='Seconds between discovery endpoint publications',
|
|
61
|
+
type=int, default=300, minimum=30,
|
|
62
|
+
tags=('discovery', 'announcement'),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
coverage_interval: int = 60
|
|
66
|
+
discovery_interval: int = 300
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Discovery(Compartment):
|
|
70
|
+
requires: tuple[str, ...] = ('capacity',)
|
|
71
|
+
|
|
72
|
+
name: str = 'discovery'
|
|
73
|
+
reads: tuple[str, ...] = ('coverage', 'class')
|
|
74
|
+
config_cls = DiscoveryConfig
|
|
75
|
+
live: tuple[str, ...] = ('coverage',)
|
|
76
|
+
CHANNELS = (
|
|
77
|
+
Channel('coverage_narrowed', 'Coverage claim narrowed under capacity pressure', level=NOTE,
|
|
78
|
+
say='shard {shard} is no longer claimed while this node sheds: it is held through the mesh and the node is at its ceiling'),
|
|
79
|
+
Channel('coverage_downgraded', 'Coverage completeness downgraded to best-effort', level=NOTE,
|
|
80
|
+
say='shard {shard} is claimed best-effort, not asserted: some of it arrived through the mesh and this node cannot stand behind it'),
|
|
81
|
+
Channel('handover_dropped', 'Handover chain dropped for shard', level=TROUBLE,
|
|
82
|
+
say='shard {shard} handover from {from_peer} dropped: {why}'),
|
|
83
|
+
)
|
|
84
|
+
METRICS = (
|
|
85
|
+
Metric(name='discovery_coverage_narrowed', description='Times coverage claims were narrowed.',
|
|
86
|
+
kind=COUNTER, counts='coverage_narrowed', tags=('discovery', 'coverage')),
|
|
87
|
+
Metric(name='discovery_coverage_downgraded', description='Times coverage claims were downgraded.',
|
|
88
|
+
kind=COUNTER, counts='coverage_downgraded', tags=('discovery', 'coverage')),
|
|
89
|
+
Metric(name='discovery_handover_dropped', description='Times handover chains were dropped.',
|
|
90
|
+
kind=COUNTER, counts='handover_dropped', tags=('discovery', 'handover')),
|
|
91
|
+
)
|
|
92
|
+
EVERY = (
|
|
93
|
+
Every(seconds=60, run='publish_coverage', name='coverage'),
|
|
94
|
+
Every(seconds=300, run='publish_discovery', name='discovery'),
|
|
95
|
+
)
|
|
96
|
+
NEEDS = (
|
|
97
|
+
Need('keystore', 'ks'),
|
|
98
|
+
Need('store', 'stores.events'),
|
|
99
|
+
Need('face'),
|
|
100
|
+
Need('relays'),
|
|
101
|
+
Need('capacity'),
|
|
102
|
+
Need('timeout'),
|
|
103
|
+
Need('persist_cfg'),
|
|
104
|
+
Need('descriptor'),
|
|
105
|
+
Need('accounts', 'stores.accounts'),
|
|
106
|
+
Need('cfg'),
|
|
107
|
+
)
|
|
108
|
+
STORES = (
|
|
109
|
+
st.Store(
|
|
110
|
+
key='_discovery',
|
|
111
|
+
description='d -> what was last put on the relays',
|
|
112
|
+
bound=1024,
|
|
113
|
+
evict=st.LRU,
|
|
114
|
+
kind=st.TABLE,
|
|
115
|
+
keyed_by='d',
|
|
116
|
+
),
|
|
117
|
+
st.Store(
|
|
118
|
+
key='_coverage',
|
|
119
|
+
description='shard -> (fingerprint, completeness, range) last published',
|
|
120
|
+
bound=1024,
|
|
121
|
+
evict=st.LRU,
|
|
122
|
+
kind=st.TABLE,
|
|
123
|
+
keyed_by='shard',
|
|
124
|
+
),
|
|
125
|
+
st.Store(
|
|
126
|
+
key='_claims',
|
|
127
|
+
description='shard -> the signed kind 30803 last published',
|
|
128
|
+
bound=1024,
|
|
129
|
+
evict=st.LRU,
|
|
130
|
+
kind=st.TABLE,
|
|
131
|
+
keyed_by='shard',
|
|
132
|
+
),
|
|
133
|
+
st.Store(
|
|
134
|
+
key='_handed',
|
|
135
|
+
description='shard -> pubkey of the peer whose handover the claim rests on',
|
|
136
|
+
bound=1024,
|
|
137
|
+
evict=st.LRU,
|
|
138
|
+
kind=st.TABLE,
|
|
139
|
+
keyed_by='shard',
|
|
140
|
+
),
|
|
141
|
+
st.Store(
|
|
142
|
+
key='_narrowed',
|
|
143
|
+
description='shard -> whether it is currently held back under pressure',
|
|
144
|
+
bound=1024,
|
|
145
|
+
evict=st.LRU,
|
|
146
|
+
kind=st.TABLE,
|
|
147
|
+
keyed_by='shard',
|
|
148
|
+
),
|
|
149
|
+
st.Store(
|
|
150
|
+
key='_downgraded',
|
|
151
|
+
description='shard -> the size at which it was last said to be best-effort',
|
|
152
|
+
bound=1024,
|
|
153
|
+
evict=st.LRU,
|
|
154
|
+
kind=st.TABLE,
|
|
155
|
+
keyed_by='shard',
|
|
156
|
+
),
|
|
157
|
+
st.Store(
|
|
158
|
+
key='_forward',
|
|
159
|
+
description='addressable coordinate -> last created_at stamped',
|
|
160
|
+
bound=1024,
|
|
161
|
+
evict=st.LRU,
|
|
162
|
+
kind=st.TABLE,
|
|
163
|
+
keyed_by='coordinate',
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
COMMANDS = (
|
|
167
|
+
Command(
|
|
168
|
+
key='show',
|
|
169
|
+
name='Discovery',
|
|
170
|
+
description='what this node has published about itself and about the shards it holds',
|
|
171
|
+
tags=('discovery', 'status'),
|
|
172
|
+
permission='discovery/show',
|
|
173
|
+
run='state',
|
|
174
|
+
reply='{discovery, coverage, handed, narrowed, downgraded}',
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
keystore: Any,
|
|
181
|
+
store: Any,
|
|
182
|
+
face: Any,
|
|
183
|
+
relays: Sequence[str],
|
|
184
|
+
capacity: Optional[Any] = None,
|
|
185
|
+
clock: Optional[Callable[[], int]] = None,
|
|
186
|
+
timeout: float = 15.0,
|
|
187
|
+
node: Optional[Any] = None,
|
|
188
|
+
persist_cfg: Optional[Callable[[], None]] = None,
|
|
189
|
+
descriptor: Optional[Callable[[], Optional[NostrEvent]]] = None,
|
|
190
|
+
accounts: Optional[Any] = None,
|
|
191
|
+
cfg: Optional[MutableMapping[str, Any]] = None,
|
|
192
|
+
publisher: Optional[Callable[[Sequence[str], Any, float], Any]] = None,
|
|
193
|
+
) -> None:
|
|
194
|
+
super().__init__(node, clock)
|
|
195
|
+
# How a discovery or coverage event reaches the relays; given so a
|
|
196
|
+
# suite can record them without a socket.
|
|
197
|
+
self.publisher: Callable[[Sequence[str], Any, float], Any] = publisher or relay.publish_all
|
|
198
|
+
# Named from ctx in a node: the descriptor getter (built in start(),
|
|
199
|
+
# after this), the account store a handover chain is read from, and
|
|
200
|
+
# the live config dict a closed range is written into before
|
|
201
|
+
# persist_cfg saves it. A fake node in a suite supplies the same.
|
|
202
|
+
self.descriptor: Callable[[], Optional[NostrEvent]] = descriptor or (lambda: getattr(node, 'descriptor', None))
|
|
203
|
+
self.accounts: Optional[Any] = accounts if accounts is not None else getattr(node, 'accounts', None)
|
|
204
|
+
self._live_cfg: Optional[MutableMapping[str, Any]] = cfg if cfg is not None else getattr(node, 'cfg', None)
|
|
205
|
+
self.ks: Any = keystore
|
|
206
|
+
self.store: Any = store
|
|
207
|
+
self.face: Any = face
|
|
208
|
+
self.relays: list[str] = list(relays) # not a store: the configured relays
|
|
209
|
+
self.capacity: Optional[Any] = capacity
|
|
210
|
+
# How a changed coverage claim is written back; the node's writer
|
|
211
|
+
# when assembled, else the legacy reach into node.save_coverage.
|
|
212
|
+
self.persist_cfg: Callable[[], None] = persist_cfg or (
|
|
213
|
+
lambda: getattr(node, 'save_coverage', lambda: None)())
|
|
214
|
+
self.timeout: float = timeout
|
|
215
|
+
self._discovery: st.Table[str, str] # d -> what was last put on the relays, so a tick republishes only a change
|
|
216
|
+
self._coverage: st.Table[str, tuple[str, str, tuple[Optional[str], Optional[str]]]] # shard -> (fingerprint, completeness, range) last published
|
|
217
|
+
# shard -> the signed kind 30803 last published, which is what a handover copies its terms from
|
|
218
|
+
self._claims: st.Table[str, NostrEvent]
|
|
219
|
+
self._handed: st.Table[str, str] # shard -> pubkey of the peer whose handover the claim rests on
|
|
220
|
+
self._narrowed: st.Table[str, bool] # shard -> whether it is currently held back under pressure
|
|
221
|
+
self._downgraded: st.Table[str, int] # shard -> the size at which it was last said to be best-effort
|
|
222
|
+
self._forward: st.Table[str, int] # addressable coordinate -> last created_at stamped
|
|
223
|
+
|
|
224
|
+
_tick_runs_every = True
|
|
225
|
+
|
|
226
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
227
|
+
return self.publish_coverage(now) + self.publish_discovery(now)
|
|
228
|
+
|
|
229
|
+
# ---- the forward clock ----------------------------------------------------
|
|
230
|
+
def forward_clock(self, key: str, now: int) -> int:
|
|
231
|
+
"""A timestamp strictly later than the last one used for this key.
|
|
232
|
+
|
|
233
|
+
Every addressable event a node republishes under a stable `d` - the
|
|
234
|
+
grant envelope, the coverage claim, the report - replaces the last one
|
|
235
|
+
at that coordinate, and NIP-01 breaks a created_at tie on the *lower*
|
|
236
|
+
id. Two written in the same second therefore leave the relay holding
|
|
237
|
+
whichever id happens to sort first, which is the older one about half
|
|
238
|
+
the time. Supersession only runs forwards, so the clock this node
|
|
239
|
+
stamps them with only runs forwards too.
|
|
240
|
+
"""
|
|
241
|
+
last = self._forward.get(key)
|
|
242
|
+
if last is not None and now <= last:
|
|
243
|
+
now = last + 1
|
|
244
|
+
self._forward[key] = now
|
|
245
|
+
return now
|
|
246
|
+
|
|
247
|
+
# ---- the node's own face --------------------------------------------------
|
|
248
|
+
def publish_discovery(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
249
|
+
"""The node's face described for NIP-66 readers: one kind 30166 per
|
|
250
|
+
ws or wss endpoint the descriptor declares. Bridge 1 in the
|
|
251
|
+
specification. It says what the face is and accepts and measures
|
|
252
|
+
nothing, so it changes only when the descriptor, the NIP-11 document
|
|
253
|
+
or the claimed shards do, and is republished only then."""
|
|
254
|
+
now_val = now if now is not None else self.clock()
|
|
255
|
+
did: list[tuple[Any, ...]] = []
|
|
256
|
+
descriptor = self.descriptor()
|
|
257
|
+
if not self.relays or descriptor is None or self.face is None:
|
|
258
|
+
return did
|
|
259
|
+
shards = [c['shard'] for c in self.config('coverage') or [] if isinstance(c, dict) and c.get('shard')]
|
|
260
|
+
for ev in ref_discovery.discovery(descriptor, self.face.nip11(), shards, now_val):
|
|
261
|
+
d = [t[1] for t in ev['tags'] if t[0] == 'd'][0]
|
|
262
|
+
fingerprint = ref_discovery.settled(ev)
|
|
263
|
+
if self._discovery.get(d) == fingerprint:
|
|
264
|
+
continue
|
|
265
|
+
problems = ref_discovery.check_discovery(ev, descriptor)
|
|
266
|
+
if problems:
|
|
267
|
+
self.refusals.refuse('discovery', 'contradicts the descriptor', subject=d,
|
|
268
|
+
detail={'problems': problems[:2]}, at=now_val)
|
|
269
|
+
did.append(('discovery refused', d, '; '.join(problems[:2])))
|
|
270
|
+
continue
|
|
271
|
+
at = self.forward_clock('discovery:' + d, now_val)
|
|
272
|
+
signed = ref_store.make_signed_event(self.ks.node_sk, at, ref_discovery.KIND,
|
|
273
|
+
ev['tags'], ev['content'])
|
|
274
|
+
self.publisher(self.relays, signed, self.timeout)
|
|
275
|
+
self._discovery[d] = fingerprint
|
|
276
|
+
did.append(('discovery', d, '%d nips' % len([t for t in ev['tags'] if t[0] == 'N'])))
|
|
277
|
+
return did
|
|
278
|
+
|
|
279
|
+
# ---- what this node holds -------------------------------------------------
|
|
280
|
+
def publish_coverage(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
281
|
+
"""Tell the mesh which shards this node holds, as kind 30803.
|
|
282
|
+
|
|
283
|
+
Nothing published one of these before, so no peer could discover what
|
|
284
|
+
any node held: the whole of data discovery, as distinct from node
|
|
285
|
+
discovery, was missing.
|
|
286
|
+
|
|
287
|
+
`asserted` is a falsifiable claim - a peer may sample the shard, find a
|
|
288
|
+
hole, and file an S3 with the missing event id as evidence - and the
|
|
289
|
+
specification forbids asserting completeness over a shard held only
|
|
290
|
+
through mesh replication, because that is how third-party replication
|
|
291
|
+
launders unverified peer data into an authoritative record. So the
|
|
292
|
+
operator's `asserted` is a request and not a decision: it is honoured
|
|
293
|
+
only when every event in the shard arrived here directly, and downgraded
|
|
294
|
+
to `best-effort` otherwise, with the reason logged. A leaf publishes
|
|
295
|
+
neither, having nothing to claim.
|
|
296
|
+
|
|
297
|
+
The one door through that is a handover: the first-hand holder's signed
|
|
298
|
+
statement over the set, received on the sync thread that pulled it and
|
|
299
|
+
kept in the account store. With one held for the shard, and only while
|
|
300
|
+
what this node holds over the handed range still fingerprints to what
|
|
301
|
+
was handed, the claim goes out `asserted` over exactly that range with
|
|
302
|
+
the handover inline. The moment the set moves - a late event in a
|
|
303
|
+
closed range, a retention pass - the handover is stale, and a claim
|
|
304
|
+
that carried it would be a false one, so it is dropped and the claim
|
|
305
|
+
falls back to what this node can stand behind on its own.
|
|
306
|
+
"""
|
|
307
|
+
now_val = now if now is not None else self.clock()
|
|
308
|
+
did: list[tuple[Any, ...]] = []
|
|
309
|
+
if self.config('class') == 'leaf' or not self.relays:
|
|
310
|
+
return did
|
|
311
|
+
for shard in self.config('coverage') or []:
|
|
312
|
+
filt = shard['filter']
|
|
313
|
+
since, until = shard.get('since', 0), shard.get('until', 2 ** 31)
|
|
314
|
+
ids = self.store.enumerate_range(filt, since, until, now_val)
|
|
315
|
+
if self.capacity is not None and self.capacity.narrowing() and self.store.any_replicated(ids):
|
|
316
|
+
# Under pressure a node stops advertising what it holds only
|
|
317
|
+
# second-hand. What it holds first-hand it keeps claiming,
|
|
318
|
+
# because that is the part it can stand behind. The claims
|
|
319
|
+
# already published lapse at their own expiration.
|
|
320
|
+
if self._narrowed.get(shard['shard']) is not True:
|
|
321
|
+
self._narrowed[shard['shard']] = True
|
|
322
|
+
self.say('coverage_narrowed', shard=shard['shard'])
|
|
323
|
+
self.refusals.refuse('coverage', 'held back while this node sheds',
|
|
324
|
+
subject=shard['shard'], at=now_val)
|
|
325
|
+
# Stopping the republication is not the same as taking the
|
|
326
|
+
# claim back. The one already on the relays is what readers
|
|
327
|
+
# act on, and it says this node holds the shard for as long
|
|
328
|
+
# as it sits there. The rung means to stop being asked for
|
|
329
|
+
# what this node can no longer serve, so the claim is
|
|
330
|
+
# withdrawn rather than left to stand unrefreshed.
|
|
331
|
+
did += self._withdraw_one(shard['shard'], now_val)
|
|
332
|
+
did.append(('coverage narrowed', shard['shard'], 'held through the mesh'))
|
|
333
|
+
continue
|
|
334
|
+
self._narrowed.pop(shard['shard'], None)
|
|
335
|
+
wanted = shard.get('completeness', 'best-effort')
|
|
336
|
+
completeness = wanted
|
|
337
|
+
handover: Optional[list[HandoverLink]] = None
|
|
338
|
+
if wanted == 'asserted' and self.store.any_replicated(ids):
|
|
339
|
+
handover, h_since, h_until, h_ids = self._handed_set(shard, filt, now_val)
|
|
340
|
+
if handover is not None:
|
|
341
|
+
since, until, ids = h_since, h_until, h_ids
|
|
342
|
+
if handover is None and wanted == 'asserted' and self.store.any_replicated(ids):
|
|
343
|
+
completeness = 'best-effort'
|
|
344
|
+
if self._downgraded.get(shard['shard']) != len(ids):
|
|
345
|
+
self._downgraded[shard['shard']] = len(ids)
|
|
346
|
+
self.say('coverage_downgraded', shard=shard['shard'])
|
|
347
|
+
self.refusals.refuse('coverage', 'asserted was downgraded to best-effort',
|
|
348
|
+
subject=shard['shard'], detail={'events': len(ids)}, at=now_val)
|
|
349
|
+
at = self.forward_clock('coverage:' + shard['shard'], now_val)
|
|
350
|
+
if handover is not None:
|
|
351
|
+
claim = coverage.coverage_claim(self.ks.node_pk, shard['shard'], filt, completeness,
|
|
352
|
+
ids, at, since=since, until=until, handover=handover)
|
|
353
|
+
else:
|
|
354
|
+
claim = coverage.coverage_claim(self.ks.node_pk, shard['shard'], filt, completeness,
|
|
355
|
+
ids, at, since=shard.get('since'), until=shard.get('until'))
|
|
356
|
+
problems = coverage.check_coverage(claim, self.config('class'))
|
|
357
|
+
if problems:
|
|
358
|
+
self.refusals.refuse('coverage', 'the claim is not one this node may make',
|
|
359
|
+
subject=shard['shard'], detail={'problems': problems[:2]}, at=now_val)
|
|
360
|
+
did.append(('coverage claim refused', shard['shard'], '; '.join(problems[:2])))
|
|
361
|
+
continue
|
|
362
|
+
signed = ref_store.make_signed_event(self.ks.node_sk, at, claim['kind'],
|
|
363
|
+
claim['tags'], claim['content'])
|
|
364
|
+
fingerprint = [t[1] for t in signed['tags'] if t[0] == 'fingerprint'][0]
|
|
365
|
+
self._claims[shard['shard']] = signed
|
|
366
|
+
if handover is not None:
|
|
367
|
+
self._handed[shard['shard']] = handover[-1]['from']
|
|
368
|
+
else:
|
|
369
|
+
self._handed.pop(shard['shard'], None)
|
|
370
|
+
# The range is part of what changed: close_ranges() bounds a shard
|
|
371
|
+
# whose set and completeness are exactly as before, and a handover
|
|
372
|
+
# signs for that closed set - a dedupe on the set alone kept the
|
|
373
|
+
# closed claim in memory and never sent it.
|
|
374
|
+
key = (fingerprint, completeness, _range_of(signed))
|
|
375
|
+
if self._coverage.get(shard['shard']) == key:
|
|
376
|
+
continue # nothing about the shard has changed
|
|
377
|
+
self.publisher(self.relays, signed, self.timeout)
|
|
378
|
+
self._coverage[shard['shard']] = key
|
|
379
|
+
did.append(('coverage', shard['shard'], completeness, '%d events' % len(ids))
|
|
380
|
+
+ (('handed over by ' + handover[-1]['from'][:16],) if handover is not None else ()))
|
|
381
|
+
return did
|
|
382
|
+
|
|
383
|
+
def _handed_set(
|
|
384
|
+
self,
|
|
385
|
+
shard: CoverageShardConfig | Mapping[str, Any],
|
|
386
|
+
filt: Mapping[str, Any],
|
|
387
|
+
now: int,
|
|
388
|
+
) -> tuple[Optional[list[HandoverLink]], Optional[int], Optional[int], list[str]]:
|
|
389
|
+
"""(chain, since, until, ids) for a shard held under a handover, or
|
|
390
|
+
(None, None, None, []) when there is none or it no longer fits.
|
|
391
|
+
|
|
392
|
+
`handover.still_fits` decides; this drops what no longer does and says
|
|
393
|
+
why, because the operator asked for asserted and is about to get
|
|
394
|
+
best-effort without having changed anything.
|
|
395
|
+
"""
|
|
396
|
+
if self.accounts is None:
|
|
397
|
+
return None, None, None, []
|
|
398
|
+
chain = self.accounts.handover(shard['shard'])
|
|
399
|
+
if not chain:
|
|
400
|
+
return None, None, None, []
|
|
401
|
+
art = chain[-1]
|
|
402
|
+
ids = self.store.enumerate_range(filt, art.get('since') or 0, art['until'], now)
|
|
403
|
+
why = handover.still_fits(chain, filt, ids)
|
|
404
|
+
if why is None:
|
|
405
|
+
return chain, art.get('since'), art['until'], ids
|
|
406
|
+
self.accounts.drop_handover(shard['shard'])
|
|
407
|
+
self.say('handover_dropped', shard=shard['shard'], from_peer=art['from'][:16], why=why)
|
|
408
|
+
self.refusals.refuse('coverage', 'handover dropped: ' + why, subject=shard['shard'],
|
|
409
|
+
detail={'from': art['from'], 'held': len(ids), 'handed': art['count']}, at=now)
|
|
410
|
+
return None, None, None, []
|
|
411
|
+
|
|
412
|
+
# ---- retiring --------------------------------------------------------------
|
|
413
|
+
def close_ranges(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
414
|
+
"""Give every asserted shard a bounded range that ends now, and publish.
|
|
415
|
+
|
|
416
|
+
A handover signs for a closed set, so a shard whose `until` is open or
|
|
417
|
+
in the future cannot be handed to anybody. That is the right rule while
|
|
418
|
+
a node is running and the wrong state to retire in: the shards worth
|
|
419
|
+
passing on are exactly the ones nobody has ever bounded. Closing them
|
|
420
|
+
is the first step of a retirement and the only one that changes what
|
|
421
|
+
this node says about itself while it is still serving.
|
|
422
|
+
|
|
423
|
+
Written to the config, because the claim is republished from the config
|
|
424
|
+
on the next tick and a range closed only in memory would reopen.
|
|
425
|
+
"""
|
|
426
|
+
now_val = now if now is not None else self.clock()
|
|
427
|
+
did: list[tuple[Any, ...]] = []
|
|
428
|
+
cov = [dict(c) for c in self.config('coverage') or [] if isinstance(c, dict)]
|
|
429
|
+
touched = False
|
|
430
|
+
for shard in cov:
|
|
431
|
+
if shard.get('completeness') != 'asserted':
|
|
432
|
+
continue
|
|
433
|
+
if shard.get('until') is not None and int(shard['until']) <= now_val:
|
|
434
|
+
continue
|
|
435
|
+
shard['until'] = now_val
|
|
436
|
+
touched = True
|
|
437
|
+
did.append(('range closed', shard['shard'], now_val))
|
|
438
|
+
if not touched:
|
|
439
|
+
return did
|
|
440
|
+
if self._live_cfg is not None:
|
|
441
|
+
self._live_cfg['coverage'] = cov
|
|
442
|
+
self.persist_cfg()
|
|
443
|
+
return did + self.publish_coverage(now_val)
|
|
444
|
+
|
|
445
|
+
def _withdraw_one(self, shard: str, now: int, lapses_in: int = 86400) -> list[tuple[Any, ...]]:
|
|
446
|
+
"""Take one published claim back. Never raises: every caller is either
|
|
447
|
+
shedding or stopping, and neither may be stopped by a relay."""
|
|
448
|
+
claim = self._claims.get(shard)
|
|
449
|
+
if claim is None:
|
|
450
|
+
return []
|
|
451
|
+
try:
|
|
452
|
+
out = coverage.withdraw_coverage(claim, self.forward_clock('coverage:' + shard, now), lapses_in)
|
|
453
|
+
signed = ref_store.make_signed_event(self.ks.node_sk, out['created_at'], out['kind'],
|
|
454
|
+
out['tags'], out['content'])
|
|
455
|
+
self.publisher(self.relays, signed, self.timeout)
|
|
456
|
+
self._coverage.pop(shard, None)
|
|
457
|
+
self._claims[shard] = signed
|
|
458
|
+
return [('coverage withdrawn', shard, 'lapses in %ds' % lapses_in)]
|
|
459
|
+
except Exception as e:
|
|
460
|
+
return [('coverage withdrawal failed', shard, '%s: %s' % (type(e).__name__, e))]
|
|
461
|
+
|
|
462
|
+
def withdraw_coverage(self, now: Optional[int] = None, lapses_in: int = 86400) -> list[tuple[Any, ...]]:
|
|
463
|
+
"""Take back every claim this node published, so none outlives it.
|
|
464
|
+
|
|
465
|
+
A kind 30803 that outlives its node is the black hole: readers see a
|
|
466
|
+
holder, pull, and time out, and every one of them records strain
|
|
467
|
+
against a node that is simply gone. The claim is addressable, so a
|
|
468
|
+
republication at the same `d` replaces it; the replacement says
|
|
469
|
+
best-effort, which promises nothing, and carries an expiration so it
|
|
470
|
+
lapses too.
|
|
471
|
+
|
|
472
|
+
Last of the retirement, after the handovers: a withdrawal before them
|
|
473
|
+
would take back the very claim a handover copies its terms from.
|
|
474
|
+
"""
|
|
475
|
+
now_val = now if now is not None else self.clock()
|
|
476
|
+
did: list[tuple[Any, ...]] = []
|
|
477
|
+
for shard in sorted(self._claims):
|
|
478
|
+
did += self._withdraw_one(shard, now_val, lapses_in)
|
|
479
|
+
return did
|
|
480
|
+
|
|
481
|
+
def asserted_claims(self) -> list[NostrEvent]:
|
|
482
|
+
"""Every published claim this node may hand over: asserted, with a
|
|
483
|
+
range that has closed. What `close_ranges` has just made of them."""
|
|
484
|
+
now = self.clock()
|
|
485
|
+
out: list[NostrEvent] = []
|
|
486
|
+
for _shard, claim in sorted(self._claims.items()):
|
|
487
|
+
if not coverage.reportable(claim):
|
|
488
|
+
continue
|
|
489
|
+
until = [t for t in claim['tags'] if t[0] == 'until']
|
|
490
|
+
if until and len(until[0]) > 1 and int(until[0][1]) <= now:
|
|
491
|
+
out.append(claim)
|
|
492
|
+
return out
|
|
493
|
+
|
|
494
|
+
def published_claim(self, shard: str) -> Optional[NostrEvent]:
|
|
495
|
+
"""The signed kind 30803 this node last published for a shard, or None.
|
|
496
|
+
What a handover copies its terms from: a node hands over what it has
|
|
497
|
+
said in public and nothing else."""
|
|
498
|
+
return self._claims.get(shard)
|
|
499
|
+
|
|
500
|
+
# ---- being watched --------------------------------------------------------
|
|
501
|
+
def state(self) -> DiscoveryStateReport:
|
|
502
|
+
return {
|
|
503
|
+
'discovery': sorted(self._discovery),
|
|
504
|
+
'coverage': {s: c for s, (_, c, _r) in self._coverage.items()},
|
|
505
|
+
'handed': dict(self._handed),
|
|
506
|
+
'narrowed': sorted(s for s, v in self._narrowed.items() if v),
|
|
507
|
+
'downgraded': sorted(self._downgraded),
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
def trouble(self) -> list[str]:
|
|
511
|
+
out: list[str] = []
|
|
512
|
+
narrowed = sorted(s for s, v in self._narrowed.items() if v)
|
|
513
|
+
if narrowed:
|
|
514
|
+
out.append('%d shard(s) are no longer claimed while this node sheds: %s. '
|
|
515
|
+
'A peer looking for that data will not find this node'
|
|
516
|
+
% (len(narrowed), ', '.join(narrowed)))
|
|
517
|
+
refused = self.refusals.within(self.TROUBLE_WINDOW, self.clock(),
|
|
518
|
+
why='the claim is not one this node may make')
|
|
519
|
+
if refused:
|
|
520
|
+
out.append('%d coverage claim(s) refused: %s'
|
|
521
|
+
% (len(refused), ', '.join(sorted({(r.subject or '') for r in refused}))))
|
|
522
|
+
return out
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Types and records for the Discovery compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DiscoveryStateReport(TypedDict, total=False):
|
|
8
|
+
"""The report Discovery.state() gives of itself."""
|
|
9
|
+
discovery: list[str]
|
|
10
|
+
coverage: dict[str, str]
|
|
11
|
+
handed: dict[str, str]
|
|
12
|
+
narrowed: list[str]
|
|
13
|
+
downgraded: list[str]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The node's schema.org face: read-only JSON-LD projections over HTTP."""
|