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,258 @@
|
|
|
1
|
+
"""ProjectedDB compartment: operator visibility, audit verification, and
|
|
2
|
+
on-demand rebuilds for co-transactional SQL projections over `ProjectedNodeStore`.
|
|
3
|
+
|
|
4
|
+
The write path itself is synchronous and co-transactional inside
|
|
5
|
+
`ProjectedNodeStore._put`, `_drop`, and `forget`. This compartment exposes:
|
|
6
|
+
- `projected/show`: registered `TableProjector`s, row counts, causal logs, and
|
|
7
|
+
recent payload refusals
|
|
8
|
+
- `projected/verify`: audits every projected SQL table against the live `events`
|
|
9
|
+
table (`verify_projection()`) and raises `trouble()` if any drift is found
|
|
10
|
+
- `projected/rebuild`: rebuilds one or all projected SQL tables from `events`
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
from typing import Any, Mapping, Optional
|
|
16
|
+
|
|
17
|
+
from node.channels import NOTE, Channel
|
|
18
|
+
from node.compartment import Command, Compartment, Every, Need
|
|
19
|
+
from node.compartment import stores as st
|
|
20
|
+
from node.config.parameter import ParamBlock, Parameter
|
|
21
|
+
from node.metrics import COUNTER, Metric
|
|
22
|
+
from node.storage.projected import (
|
|
23
|
+
CausalProjector,
|
|
24
|
+
ProjectedNodeStore,
|
|
25
|
+
ProjectionVerifyReport,
|
|
26
|
+
ProjectorSpec,
|
|
27
|
+
TableProjector,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .types import ProjectedRebuildReply, ProjectedStateReport, ProjectedTableSummary
|
|
31
|
+
|
|
32
|
+
VERIFY_SECONDS: int = 3600
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass(frozen=True)
|
|
36
|
+
class ProjectedDBConfig(ParamBlock):
|
|
37
|
+
key = 'projected'
|
|
38
|
+
name = 'projected'
|
|
39
|
+
DERIVED: bool = False
|
|
40
|
+
PARAMS = (
|
|
41
|
+
Parameter(
|
|
42
|
+
key='verify_seconds',
|
|
43
|
+
name='Verify Cadence',
|
|
44
|
+
description='Seconds between background projection consistency audits',
|
|
45
|
+
type=int,
|
|
46
|
+
default=VERIFY_SECONDS,
|
|
47
|
+
tags=('projected', 'store'),
|
|
48
|
+
),
|
|
49
|
+
Parameter(
|
|
50
|
+
key='auto_heal',
|
|
51
|
+
name='Auto-Heal Drift',
|
|
52
|
+
description='Automatically rebuild projected SQL tables if verification detects drift',
|
|
53
|
+
type=bool,
|
|
54
|
+
default=True,
|
|
55
|
+
tags=('projected', 'store'),
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
verify_seconds: int = VERIFY_SECONDS
|
|
59
|
+
auto_heal: bool = True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ProjectedDB(Compartment):
|
|
63
|
+
"""Operator surface and consistency auditor for `ProjectedNodeStore`."""
|
|
64
|
+
|
|
65
|
+
name = 'projected'
|
|
66
|
+
reads = ('projected', 'coverage')
|
|
67
|
+
live = ('projected',)
|
|
68
|
+
config_cls = ProjectedDBConfig
|
|
69
|
+
CHANNELS = (
|
|
70
|
+
Channel(
|
|
71
|
+
'table_rebuilt',
|
|
72
|
+
'Projected SQL table rebuilt from events',
|
|
73
|
+
level=NOTE,
|
|
74
|
+
say='projected table {table} rebuilt ({rows} rows)',
|
|
75
|
+
tags=('projected', 'store'),
|
|
76
|
+
),
|
|
77
|
+
Channel(
|
|
78
|
+
'projection_mismatch',
|
|
79
|
+
'Projected SQL table drifted from events store',
|
|
80
|
+
level=NOTE,
|
|
81
|
+
say='projection mismatch detected ({missing} missing, {orphan} orphan)',
|
|
82
|
+
tags=('projected', 'store'),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
METRICS = (
|
|
86
|
+
Metric(
|
|
87
|
+
name='projected_tables_rebuilt',
|
|
88
|
+
description='Times a projected SQL table was rebuilt from events.',
|
|
89
|
+
kind=COUNTER,
|
|
90
|
+
counts='table_rebuilt',
|
|
91
|
+
tags=('projected', 'store'),
|
|
92
|
+
),
|
|
93
|
+
Metric(
|
|
94
|
+
name='projected_mismatches',
|
|
95
|
+
description='Times projection verification found drift.',
|
|
96
|
+
kind=COUNTER,
|
|
97
|
+
counts='projection_mismatch',
|
|
98
|
+
tags=('projected', 'store'),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
NEEDS = (
|
|
102
|
+
Need('store', 'stores.events'),
|
|
103
|
+
Need('cfg', 'cfg', getter=True),
|
|
104
|
+
)
|
|
105
|
+
EVERY = (Every(seconds=VERIFY_SECONDS, run='verify'),)
|
|
106
|
+
COMMANDS = (
|
|
107
|
+
Command(
|
|
108
|
+
key='show',
|
|
109
|
+
name='Projected tables',
|
|
110
|
+
description='registered SQL table projectors, row counts, and payload refusals',
|
|
111
|
+
tags=('projected', 'status'),
|
|
112
|
+
permission='projected/show',
|
|
113
|
+
run='state',
|
|
114
|
+
reply='{enabled, tables, causal_logs, refusals, verified_at, last_verify_ok}',
|
|
115
|
+
),
|
|
116
|
+
Command(
|
|
117
|
+
key='verify',
|
|
118
|
+
name='Verify projections',
|
|
119
|
+
description='audit projected SQL tables against the live events table',
|
|
120
|
+
tags=('projected',),
|
|
121
|
+
permission='projected/verify',
|
|
122
|
+
run='verify',
|
|
123
|
+
reply='{ok, tables, missing_events, orphan_rows, mismatched_rows}',
|
|
124
|
+
),
|
|
125
|
+
Command(
|
|
126
|
+
key='rebuild',
|
|
127
|
+
name='Rebuild projections',
|
|
128
|
+
description='rebuild one or all projected SQL tables from the live events table',
|
|
129
|
+
tags=('projected',),
|
|
130
|
+
parameters=(
|
|
131
|
+
Parameter(
|
|
132
|
+
key='table_name',
|
|
133
|
+
name='table_name',
|
|
134
|
+
description='optional projected SQL table name to rebuild (omit to rebuild all)',
|
|
135
|
+
type=str,
|
|
136
|
+
default='',
|
|
137
|
+
required=False,
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
permission='projected/rebuild',
|
|
141
|
+
run='rebuild',
|
|
142
|
+
reply='{rebuilt: {table: rows}}',
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
STORES = (
|
|
146
|
+
st.Store(
|
|
147
|
+
key='rebuilds',
|
|
148
|
+
description='total rebuilds performed per projected table',
|
|
149
|
+
kind=st.TABLE,
|
|
150
|
+
shape=int,
|
|
151
|
+
bound=256,
|
|
152
|
+
keyed_by='table',
|
|
153
|
+
evict=st.LRU,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def __init__(self, store: Any, cfg: Any, clock: Any, node: Any = None) -> None:
|
|
158
|
+
Compartment.__init__(self, node, clock)
|
|
159
|
+
self.store = store
|
|
160
|
+
self._cfg = cfg
|
|
161
|
+
self.rebuilds: st.Table[str, int]
|
|
162
|
+
self.verified_at: Optional[int] = None
|
|
163
|
+
self.last_verify_ok: Optional[bool] = None
|
|
164
|
+
|
|
165
|
+
def is_projected_store(self) -> bool:
|
|
166
|
+
return isinstance(self.store, ProjectedNodeStore)
|
|
167
|
+
|
|
168
|
+
def register_projector(self, projector: TableProjector | ProjectorSpec) -> Optional[TableProjector]:
|
|
169
|
+
if not isinstance(self.store, ProjectedNodeStore):
|
|
170
|
+
return None
|
|
171
|
+
return self.store.register_projector(projector)
|
|
172
|
+
|
|
173
|
+
def register_causal_projector(self, causal: CausalProjector) -> Optional[CausalProjector]:
|
|
174
|
+
if not isinstance(self.store, ProjectedNodeStore):
|
|
175
|
+
return None
|
|
176
|
+
return self.store.register_causal_projector(causal)
|
|
177
|
+
|
|
178
|
+
def verify(self, now: Optional[int] = None) -> ProjectionVerifyReport:
|
|
179
|
+
now_ts = now if now is not None else self.clock()
|
|
180
|
+
self.verified_at = now_ts
|
|
181
|
+
if not isinstance(self.store, ProjectedNodeStore):
|
|
182
|
+
self.last_verify_ok = True
|
|
183
|
+
return {
|
|
184
|
+
'ok': True,
|
|
185
|
+
'tables': {},
|
|
186
|
+
'missing_events': [],
|
|
187
|
+
'orphan_rows': [],
|
|
188
|
+
'mismatched_rows': [],
|
|
189
|
+
}
|
|
190
|
+
report = self.store.verify_projection()
|
|
191
|
+
self.last_verify_ok = report['ok']
|
|
192
|
+
if not report['ok']:
|
|
193
|
+
self.say(
|
|
194
|
+
'projection_mismatch',
|
|
195
|
+
missing=len(report['missing_events']),
|
|
196
|
+
orphan=len(report['orphan_rows']),
|
|
197
|
+
)
|
|
198
|
+
raw_cfg = (self._cfg() or {}).get('projected') or {}
|
|
199
|
+
auto_heal = raw_cfg.get('auto_heal', True) if isinstance(raw_cfg, Mapping) else True
|
|
200
|
+
if auto_heal:
|
|
201
|
+
self.rebuild(now=now_ts)
|
|
202
|
+
report = self.store.verify_projection()
|
|
203
|
+
self.last_verify_ok = report['ok']
|
|
204
|
+
return report
|
|
205
|
+
|
|
206
|
+
def rebuild(
|
|
207
|
+
self,
|
|
208
|
+
table_name: Optional[str | Mapping[str, Any]] = None,
|
|
209
|
+
now: Optional[int] = None,
|
|
210
|
+
) -> ProjectedRebuildReply:
|
|
211
|
+
if not isinstance(self.store, ProjectedNodeStore):
|
|
212
|
+
return {'rebuilt': {}}
|
|
213
|
+
target_table: Optional[str]
|
|
214
|
+
if isinstance(table_name, Mapping):
|
|
215
|
+
raw_tbl = str(table_name.get('table_name') or '').strip()
|
|
216
|
+
target_table = raw_tbl or None
|
|
217
|
+
else:
|
|
218
|
+
target_table = (table_name or '').strip() or None if isinstance(table_name, str) else None
|
|
219
|
+
rebuilt = self.store.backfill(table_name=target_table)
|
|
220
|
+
for tbl, rows in rebuilt.items():
|
|
221
|
+
self.rebuilds[tbl] = self.rebuilds.get(tbl, 0) + 1
|
|
222
|
+
self.say('table_rebuilt', table=tbl, rows=rows)
|
|
223
|
+
return {'rebuilt': rebuilt}
|
|
224
|
+
|
|
225
|
+
def state(self) -> ProjectedStateReport:
|
|
226
|
+
if not isinstance(self.store, ProjectedNodeStore):
|
|
227
|
+
return {
|
|
228
|
+
'enabled': False,
|
|
229
|
+
'tables': {},
|
|
230
|
+
'causal_logs': [],
|
|
231
|
+
'refusals': [],
|
|
232
|
+
'verified_at': self.verified_at,
|
|
233
|
+
'last_verify_ok': self.last_verify_ok,
|
|
234
|
+
}
|
|
235
|
+
summaries: dict[str, ProjectedTableSummary] = {}
|
|
236
|
+
for tp in self.store.projectors():
|
|
237
|
+
summaries[tp.table_name] = {
|
|
238
|
+
'table': tp.table_name,
|
|
239
|
+
'kind': tp.kind,
|
|
240
|
+
'mode': tp.spec.mode(),
|
|
241
|
+
'primary_key': list(tp.spec.primary_key),
|
|
242
|
+
'rows': self.store.table_count(tp.table_name),
|
|
243
|
+
'projected_total': self.store.projected_counts.get(tp.table_name, 0),
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
'enabled': True,
|
|
247
|
+
'tables': summaries,
|
|
248
|
+
'causal_logs': [cp.log_id for cp in self.store.causal_projectors()],
|
|
249
|
+
'refusals': list(self.store.projection_refusals[-20:]),
|
|
250
|
+
'verified_at': self.verified_at,
|
|
251
|
+
'last_verify_ok': self.last_verify_ok,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
def trouble(self) -> list[str]:
|
|
255
|
+
out: list[str] = []
|
|
256
|
+
if self.last_verify_ok is False:
|
|
257
|
+
out.append('one or more projected SQL tables failed verification against the events store')
|
|
258
|
+
return out
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Types and status records for the ProjectedDB compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
from node.storage.projected import ProjectionRefusal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProjectedTableSummary(TypedDict):
|
|
10
|
+
table: str
|
|
11
|
+
kind: int
|
|
12
|
+
mode: str
|
|
13
|
+
primary_key: list[str]
|
|
14
|
+
rows: int
|
|
15
|
+
projected_total: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProjectedStateReport(TypedDict):
|
|
19
|
+
enabled: bool
|
|
20
|
+
tables: dict[str, ProjectedTableSummary]
|
|
21
|
+
causal_logs: list[str]
|
|
22
|
+
refusals: list[ProjectionRefusal]
|
|
23
|
+
verified_at: Optional[int]
|
|
24
|
+
last_verify_ok: Optional[bool]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProjectedRebuildReply(TypedDict):
|
|
28
|
+
rebuilt: dict[str, int]
|
|
29
|
+
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""What this node stops keeping, and the one thing it must never stop keeping.
|
|
2
|
+
|
|
3
|
+
An open relay face collects what an open face invites. On the alpha mesh one
|
|
4
|
+
node was holding 6,025 events to every other node's 38, of which 5,625 were
|
|
5
|
+
NIP-59 gift wraps between strangers using it as an ordinary DM relay - about
|
|
6
|
+
5,200 arriving a day, 62 MB and climbing, on a box with 113 MB of memory free.
|
|
7
|
+
Nothing bounded it: NIP-40 expiry is enforced when an event is read and never on
|
|
8
|
+
disk, and only 68 of those 6,025 carried an expiration tag at all, so leaning on
|
|
9
|
+
other people's compliance would have reclaimed nothing.
|
|
10
|
+
|
|
11
|
+
So retention is the operator's, stated per kind, and **the default keeps
|
|
12
|
+
everything**. A relay that quietly began deleting what it was handed would be a
|
|
13
|
+
worse thing than one that fills up, and the specification prices retrieval
|
|
14
|
+
rather than retention precisely because a standing promise to hold something is
|
|
15
|
+
the one product this protocol declines to sell. Nothing here is protocol. It is
|
|
16
|
+
a disk, and whose disk it is decides.
|
|
17
|
+
|
|
18
|
+
The guard is the part that is not a preference. A kind 30803 coverage claim
|
|
19
|
+
names a shard as a filter and carries a negentropy fingerprint over the set;
|
|
20
|
+
evicting an event inside a claimed shard would leave this node advertising a
|
|
21
|
+
fingerprint it can no longer reproduce, and a peer reconciling against it would
|
|
22
|
+
find the difference and be right. So an event matching any shard this node
|
|
23
|
+
claims is never evicted, whatever the operator asked for, and the refusal is
|
|
24
|
+
recorded rather than performed silently.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import dataclasses
|
|
29
|
+
from typing import Any, Iterator, Mapping, Optional
|
|
30
|
+
from fleetmesh_core.storage import store as store_mod # noqa: E402
|
|
31
|
+
from node.compartment import stores as st
|
|
32
|
+
from node.channels import Channel, NOTE
|
|
33
|
+
from node.metrics import COUNTER, Metric
|
|
34
|
+
from node.compartment import Command, Compartment, Every, Need # noqa: E402
|
|
35
|
+
from node.config.parameter import Parameter, ParamBlock
|
|
36
|
+
from .types import (
|
|
37
|
+
RetentionStateReport,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# A sweep walks the store, so it does not run on every tick. An hour is far
|
|
41
|
+
# finer than any age worth setting and coarse enough to cost nothing.
|
|
42
|
+
SWEEP_SECONDS: int = 3600
|
|
43
|
+
# How many events one page of a sweep holds. The sweep reads events to decide
|
|
44
|
+
# whether a shard claim protects them, and the backlog it reads on the first
|
|
45
|
+
# sweep after a policy is set is unbounded by nature - days of an open face.
|
|
46
|
+
SWEEP_PAGE: int = 500
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclasses.dataclass(frozen=True)
|
|
50
|
+
class RetentionConfig(ParamBlock):
|
|
51
|
+
key = 'retention'
|
|
52
|
+
name = 'retention'
|
|
53
|
+
DERIVED: bool = False
|
|
54
|
+
PARAMS = (
|
|
55
|
+
Parameter(key='sweep_seconds', name='Sweep Cadence', description='Seconds between retention sweeps', type=int, default=SWEEP_SECONDS, tags=('retention', 'sweep')),
|
|
56
|
+
Parameter(key='sweep_page', name='Sweep Page Size', description='Number of events examined per sweep page', type=int, default=SWEEP_PAGE, tags=('retention', 'sweep')),
|
|
57
|
+
)
|
|
58
|
+
sweep_seconds: int = SWEEP_SECONDS
|
|
59
|
+
sweep_page: int = SWEEP_PAGE
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Retention(Compartment):
|
|
63
|
+
"""Per-kind ages, swept on a slow cadence, with the shards held back."""
|
|
64
|
+
|
|
65
|
+
name = 'retention'
|
|
66
|
+
reads = ('retention', 'coverage')
|
|
67
|
+
live = ('retention',)
|
|
68
|
+
config_cls = RetentionConfig
|
|
69
|
+
CHANNELS = (
|
|
70
|
+
Channel('events_evicted', 'Expired events evicted during sweep', level=NOTE, say='retention sweep evicted {count} expired events', tags=('retention', 'sweep')),
|
|
71
|
+
Channel('eviction_refused', 'Event eviction refused because it matches a claimed shard', level=NOTE, say='retention eviction refused for event {id}', tags=('retention', 'sweep')),
|
|
72
|
+
)
|
|
73
|
+
METRICS = (
|
|
74
|
+
Metric(name='retention_events_evicted', description='Times expired events were evicted.', kind=COUNTER, counts='events_evicted', tags=('retention', 'sweep')),
|
|
75
|
+
Metric(name='retention_evictions_refused', description='Times evictions were refused.', kind=COUNTER, counts='eviction_refused', tags=('retention', 'sweep')),
|
|
76
|
+
)
|
|
77
|
+
NEEDS = (
|
|
78
|
+
Need('store', 'stores.events'),
|
|
79
|
+
Need('cfg', 'cfg', getter=True),
|
|
80
|
+
)
|
|
81
|
+
EVERY = (Every(seconds=SWEEP_SECONDS, run='sweep'),)
|
|
82
|
+
COMMANDS = (
|
|
83
|
+
Command(
|
|
84
|
+
key='show',
|
|
85
|
+
name='Retention',
|
|
86
|
+
description='the ages this operator set, what they protect, and what has been evicted',
|
|
87
|
+
tags=('retention', 'status'),
|
|
88
|
+
permission='retention/show',
|
|
89
|
+
run='state',
|
|
90
|
+
reply='{ages, protected, swept_at, evicted, held_back}',
|
|
91
|
+
),
|
|
92
|
+
Command(
|
|
93
|
+
key='sweep',
|
|
94
|
+
name='Retention sweep',
|
|
95
|
+
description='run the sweep now rather than waiting for the cadence',
|
|
96
|
+
tags=('retention',),
|
|
97
|
+
permission='retention/sweep',
|
|
98
|
+
run='sweep',
|
|
99
|
+
reply='{evicted: {kind: n}, held_back: n, scanned: n}',
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
STORES = (
|
|
103
|
+
st.Store(key='evicted', description='total ever evicted, per event kind',
|
|
104
|
+
kind=st.TABLE, shape=int, bound=1024, keyed_by='kind', evict=st.LRU),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def __init__(self, store: Any, cfg: Any, clock: Any, node: Any = None) -> None:
|
|
108
|
+
Compartment.__init__(self, node, clock)
|
|
109
|
+
self.store = store
|
|
110
|
+
self._cfg = cfg # a callable returning the live config
|
|
111
|
+
self.swept_at: Optional[int] = None
|
|
112
|
+
self.evicted: st.Table[int, int] # kind -> total ever evicted here
|
|
113
|
+
self.held_back: int = 0 # events an age would have taken and a shard kept
|
|
114
|
+
|
|
115
|
+
# ---- what the operator asked for ----------------------------------------
|
|
116
|
+
def ages(self) -> dict[int, int]:
|
|
117
|
+
"""{kind: seconds}. Absent, empty or unreadable all mean keep everything."""
|
|
118
|
+
raw = (self._cfg() or {}).get('retention') or {}
|
|
119
|
+
if not isinstance(raw, dict):
|
|
120
|
+
return {}
|
|
121
|
+
out: dict[int, int] = {}
|
|
122
|
+
for k, v in raw.items():
|
|
123
|
+
try:
|
|
124
|
+
kind, age = int(k), int(v)
|
|
125
|
+
except (TypeError, ValueError):
|
|
126
|
+
self.refusals.refuse('retention', 'not a kind and a number of seconds',
|
|
127
|
+
at=self.clock(), detail={'kind': str(k)[:16], 'age': str(v)[:16]})
|
|
128
|
+
continue
|
|
129
|
+
if age > 0:
|
|
130
|
+
out[kind] = age
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
def protected(self) -> list[Mapping[str, Any]]:
|
|
134
|
+
"""The shard filters a coverage claim or ProjectedNodeStore covers. Never evicted."""
|
|
135
|
+
cov = (self._cfg() or {}).get('coverage') or []
|
|
136
|
+
out: list[Mapping[str, Any]] = [
|
|
137
|
+
c['filter'] for c in cov
|
|
138
|
+
if isinstance(c, dict) and isinstance(c.get('filter'), dict)
|
|
139
|
+
]
|
|
140
|
+
if hasattr(self.store, 'protected_filters'):
|
|
141
|
+
for f in self.store.protected_filters():
|
|
142
|
+
if f not in out:
|
|
143
|
+
out.append(f)
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
# ---- the sweep ----------------------------------------------------------
|
|
147
|
+
def sweep(self, now: Optional[int] = None) -> Mapping[str, Any]:
|
|
148
|
+
now = now if now is not None else self.clock()
|
|
149
|
+
ages, guard = self.ages(), self.protected()
|
|
150
|
+
evicted: dict[int, int] = {}
|
|
151
|
+
held_back = 0
|
|
152
|
+
scanned = 0
|
|
153
|
+
if not ages:
|
|
154
|
+
self.swept_at = now
|
|
155
|
+
return {'evicted': evicted, 'held_back': held_back, 'scanned': scanned}
|
|
156
|
+
for kind, age in sorted(ages.items()):
|
|
157
|
+
cutoff = now - age
|
|
158
|
+
for ev in self._older(kind, cutoff):
|
|
159
|
+
scanned += 1
|
|
160
|
+
if any(store_mod.matches(ev, f) for f in guard):
|
|
161
|
+
# Not a preference and not overridable: the fingerprint this
|
|
162
|
+
# node published over that shard has to stay reproducible.
|
|
163
|
+
held_back += 1
|
|
164
|
+
self.held_back += 1
|
|
165
|
+
self.refusals.refuse('retention', 'inside a shard this node claims coverage for',
|
|
166
|
+
at=now, detail={'id': ev['id'][:16], 'kind': kind})
|
|
167
|
+
continue
|
|
168
|
+
self.store.forget(ev['id'])
|
|
169
|
+
evicted[kind] = evicted.get(kind, 0) + 1
|
|
170
|
+
self.evicted[kind] = self.evicted.get(kind, 0) + 1
|
|
171
|
+
if sum(evicted.values()) > 0 and hasattr(self.store, 'vacuum'):
|
|
172
|
+
try:
|
|
173
|
+
self.store.vacuum(pages=50)
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
self.swept_at = now
|
|
177
|
+
return {'evicted': evicted, 'held_back': held_back, 'scanned': scanned}
|
|
178
|
+
|
|
179
|
+
def _older(self, kind: int, cutoff: int) -> Iterator[Mapping[str, Any]]:
|
|
180
|
+
"""One page at a time, oldest first, so the sweep costs a page and not
|
|
181
|
+
the backlog. The first sweep after a policy is set is the big one."""
|
|
182
|
+
after: Optional[tuple[int, str]] = None
|
|
183
|
+
while True:
|
|
184
|
+
page = self.store.by_kind_before(kind, cutoff, SWEEP_PAGE, after)
|
|
185
|
+
if not page:
|
|
186
|
+
return
|
|
187
|
+
for ev in page:
|
|
188
|
+
yield ev
|
|
189
|
+
if len(page) < SWEEP_PAGE:
|
|
190
|
+
return
|
|
191
|
+
after = (page[-1]['created_at'], page[-1]['id'])
|
|
192
|
+
|
|
193
|
+
# ---- the contract -------------------------------------------------------
|
|
194
|
+
def _report_sweep(self, r: Mapping[str, Any]) -> list[tuple[Any, ...]]:
|
|
195
|
+
did: list[tuple[Any, ...]] = []
|
|
196
|
+
if r.get('evicted'):
|
|
197
|
+
did.append(('evicted', sum(r['evicted'].values()), r['evicted']))
|
|
198
|
+
if r.get('held_back'):
|
|
199
|
+
did.append(('held back', r['held_back'], 'inside a claimed shard'))
|
|
200
|
+
return did
|
|
201
|
+
|
|
202
|
+
def state(self) -> RetentionStateReport:
|
|
203
|
+
return {'ages': {str(k): v for k, v in self.ages().items()},
|
|
204
|
+
'protected': len(self.protected()),
|
|
205
|
+
'swept_at': self.swept_at,
|
|
206
|
+
'evicted': {str(k): v for k, v in self.evicted.items()},
|
|
207
|
+
'held_back': self.held_back}
|
|
208
|
+
|
|
209
|
+
def trouble(self) -> list[str]:
|
|
210
|
+
out: list[str] = []
|
|
211
|
+
if self.held_back:
|
|
212
|
+
out.append('%d event(s) an age would have taken are inside a shard this node claims, and '
|
|
213
|
+
'were kept: a coverage fingerprint has to stay reproducible' % self.held_back)
|
|
214
|
+
return out
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Types and records for the Retention compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RetentionStateReport(TypedDict):
|
|
8
|
+
ages: dict[str, int]
|
|
9
|
+
protected: int
|
|
10
|
+
swept_at: Optional[int]
|
|
11
|
+
evicted: dict[str, int]
|
|
12
|
+
held_back: int
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""sync/1.0 over the wire.
|
|
2
|
+
|
|
3
|
+
fleetmesh_core/sync.py runs a session as two objects handing each other messages in one
|
|
4
|
+
process. Here the two objects are on two nodes and the messages travel as
|
|
5
|
+
DIDComm envelopes through the transport, on one thread per session. The
|
|
6
|
+
reference still decides everything: what to ask for, what to serve, what to
|
|
7
|
+
count, what to find.
|
|
8
|
+
|
|
9
|
+
Two things are added at the wire. Every delivered event travels inside a
|
|
10
|
+
delivery envelope with a hop signed by the node that sent it, carried as a
|
|
11
|
+
DIDComm attachment named delivery-envelope, and the receiver verifies every
|
|
12
|
+
hop before the reference sees the event; an event whose envelope does not
|
|
13
|
+
verify is not delivered and is a finding. And each side's session is bound
|
|
14
|
+
to the grant that governs it: the initiator pulls under the grant it holds
|
|
15
|
+
from the peer, the responder serves under the grant it issued.
|
|
16
|
+
|
|
17
|
+
One thing follows a session. A responder that asserts the shard first-hand,
|
|
18
|
+
over a range that has closed, and has just served a peer to equality over
|
|
19
|
+
that whole range, may hand the shard over: a signed statement the peer
|
|
20
|
+
carries in its own coverage claim so it can assert the same set. The offer
|
|
21
|
+
is the responder's to make (config `handover`), the statement copies its
|
|
22
|
+
terms from the claim this node has already published and nothing else, and
|
|
23
|
+
the receiver keeps it only if it fingerprints to what the receiver now holds.
|
|
24
|
+
|
|
25
|
+
A package by seam: envelope.py (the wire vocabulary), core.py (the
|
|
26
|
+
compartment), initiator.py (`pull`), responder.py (`handle`), handover.py.
|
|
27
|
+
Every name the module exported is re-exported here.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from .core import SyncPeer
|
|
32
|
+
from .envelope import ATTACHMENT, MAX_SESSIONS_PER_PEER, PIURI, SESSION_IDLE, SyncError, envelope_attachment, verified_events
|
|
33
|
+
|
|
34
|
+
__all__ = ['ATTACHMENT', 'MAX_SESSIONS_PER_PEER', 'PIURI', 'SESSION_IDLE', 'SyncError', 'envelope_attachment',
|
|
35
|
+
'verified_events', 'SyncPeer']
|