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,662 @@
|
|
|
1
|
+
"""What a compartment holds: its in-memory collections, declared and bounded.
|
|
2
|
+
|
|
3
|
+
The sixth thing a compartment declares, beside PARAMS (what it can be told),
|
|
4
|
+
CHANNELS (what it says), METRICS (what it measures), COMMANDS (what it can be
|
|
5
|
+
asked to do) and LIFECYCLE (how it fails): STORES, what it keeps.
|
|
6
|
+
|
|
7
|
+
Before this a compartment's memory was whatever attributes its __init__
|
|
8
|
+
happened to assign. Nothing outside it knew they existed, so nobody could ask
|
|
9
|
+
how much announce was holding, which collections grow with the number of
|
|
10
|
+
peers, or what a command reads. Some were trimmed by hand (`del self.log
|
|
11
|
+
[:-200]` in capacity) and some were not trimmed at all, and the difference was
|
|
12
|
+
visible only to someone reading the code.
|
|
13
|
+
|
|
14
|
+
A Store declares one collection:
|
|
15
|
+
|
|
16
|
+
key the attribute it is reached under, and its name everywhere else
|
|
17
|
+
kind RING a log, newest last, oldest dropped at `bound`
|
|
18
|
+
TABLE a mapping, keyed by `keyed_by`
|
|
19
|
+
SET members, keyed by `keyed_by`
|
|
20
|
+
COUNTERS a fixed set of named integers, the fields of `shape`
|
|
21
|
+
shape the type of one entry (one row, one member, one record). A
|
|
22
|
+
TypedDict is checked on every write: required keys present, no
|
|
23
|
+
key it does not name. A class is checked with isinstance. A
|
|
24
|
+
write that does not fit raises TypeError - a programming error,
|
|
25
|
+
and the lifecycle degrades the compartment that made it.
|
|
26
|
+
bound how many entries it may hold. Required: a collection that grows
|
|
27
|
+
with the outside world and says nothing about how far is the
|
|
28
|
+
failure this exists to prevent. COUNTERS are bounded by their
|
|
29
|
+
shape and take no bound.
|
|
30
|
+
evict what happens at the bound. OLDEST drops the earliest written,
|
|
31
|
+
LRU the least recently read or written, REFUSE refuses the
|
|
32
|
+
write (StoreFull, or False from `put`) - for state that must
|
|
33
|
+
not be silently lost, where the caller has to decide.
|
|
34
|
+
keyed_by what makes it grow: 'peer', 'event', 'url'. Required on a
|
|
35
|
+
TABLE and a SET, because that is the question an operator asks.
|
|
36
|
+
persist MEMORY (gone with the process) or SNAPSHOT (carried across a
|
|
37
|
+
process restart by the lifecycle, node/lifecycle phase 4)
|
|
38
|
+
sensitive its entries are never shown in a report, only its size
|
|
39
|
+
|
|
40
|
+
and the compartment base class builds a container for each one - Ring, Table,
|
|
41
|
+
Members, Counters - that enforces the bound and the shape, and counts itself:
|
|
42
|
+
inserts, evictions, refusals and its high-water mark. So the declaration and
|
|
43
|
+
the behaviour cannot drift apart, and the node meters every compartment's
|
|
44
|
+
memory without the compartment writing a metric.
|
|
45
|
+
|
|
46
|
+
What is NOT a store: a list of callbacks (wiring), a config cache, a small
|
|
47
|
+
fixed lookup table, a per-packet buffer on a hot path. The rule is whether
|
|
48
|
+
the collection grows with the outside world.
|
|
49
|
+
|
|
50
|
+
Durable data (the event store, grants, the change log) lives in sqlite and is
|
|
51
|
+
not declared here yet. A persist value for it arrives with the code that
|
|
52
|
+
honours it.
|
|
53
|
+
|
|
54
|
+
A package rather than a root module for the reason node/channels is one:
|
|
55
|
+
node/*.py is held to a monotone baseline (fleetmesh.org/scripts/check-root.py).
|
|
56
|
+
Standard library only.
|
|
57
|
+
"""
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import collections
|
|
61
|
+
import dataclasses
|
|
62
|
+
import re
|
|
63
|
+
import threading
|
|
64
|
+
from typing import (
|
|
65
|
+
Any,
|
|
66
|
+
Generic,
|
|
67
|
+
Hashable,
|
|
68
|
+
Iterable,
|
|
69
|
+
Iterator,
|
|
70
|
+
Mapping,
|
|
71
|
+
MutableMapping,
|
|
72
|
+
Optional,
|
|
73
|
+
Sequence,
|
|
74
|
+
TypedDict,
|
|
75
|
+
TypeVar,
|
|
76
|
+
Union,
|
|
77
|
+
is_typeddict,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# ---- the vocabulary -------------------------------------------------------------
|
|
81
|
+
RING = 'ring'
|
|
82
|
+
TABLE = 'table'
|
|
83
|
+
SET = 'set'
|
|
84
|
+
COUNTERS = 'counters'
|
|
85
|
+
KINDS: tuple[str, ...] = (RING, TABLE, SET, COUNTERS)
|
|
86
|
+
|
|
87
|
+
OLDEST = 'oldest'
|
|
88
|
+
LRU = 'lru'
|
|
89
|
+
REFUSE = 'refuse'
|
|
90
|
+
EVICTIONS: tuple[str, ...] = (OLDEST, LRU, REFUSE)
|
|
91
|
+
|
|
92
|
+
MEMORY = 'memory'
|
|
93
|
+
SNAPSHOT = 'snapshot'
|
|
94
|
+
PERSISTS: tuple[str, ...] = (MEMORY, SNAPSHOT)
|
|
95
|
+
|
|
96
|
+
# A REFUSE store this full is trouble before it is refusing: the operator
|
|
97
|
+
# hears about it while there is still room to act.
|
|
98
|
+
NEARLY_FULL = 0.9
|
|
99
|
+
|
|
100
|
+
_KEY = re.compile(r'[a-z_][a-z0-9_]*$')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class StoreFull(Exception):
|
|
104
|
+
"""A REFUSE store at its bound was asked to take a new entry."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def shape_problem(shape: Optional[type], value: object) -> str:
|
|
108
|
+
"""Why `value` is not one `shape`, or '' when it is (or nothing is declared).
|
|
109
|
+
|
|
110
|
+
Shallow by design: a TypedDict's keys, not its values' types. The keys are
|
|
111
|
+
where drift happens - a field renamed in one writer and not another - and
|
|
112
|
+
checking them costs a set comparison per write.
|
|
113
|
+
"""
|
|
114
|
+
if shape is None:
|
|
115
|
+
return ''
|
|
116
|
+
if is_typeddict(shape):
|
|
117
|
+
if not isinstance(value, dict):
|
|
118
|
+
return 'a %s is a dict, not %s' % (shape.__name__, type(value).__name__)
|
|
119
|
+
required: frozenset[str] = getattr(shape, '__required_keys__', frozenset())
|
|
120
|
+
known = required | getattr(shape, '__optional_keys__', frozenset())
|
|
121
|
+
missing = sorted(required - value.keys())
|
|
122
|
+
extra = sorted(k for k in value if k not in known)
|
|
123
|
+
if missing or extra:
|
|
124
|
+
return 'a %s %s' % (shape.__name__, '; '.join(
|
|
125
|
+
(['is missing %s' % ', '.join(missing)] if missing else [])
|
|
126
|
+
+ (['has no field %s' % ', '.join(extra)] if extra else [])))
|
|
127
|
+
return ''
|
|
128
|
+
if isinstance(shape, type) and not isinstance(value, shape):
|
|
129
|
+
return 'expected %s, got %s' % (shape.__name__, type(value).__name__)
|
|
130
|
+
return ''
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class StoreRecord(TypedDict, total=False):
|
|
134
|
+
"""A store as the census, `meshnode subsystems` and the docs read it."""
|
|
135
|
+
key: str
|
|
136
|
+
description: str
|
|
137
|
+
kind: str
|
|
138
|
+
shape: str
|
|
139
|
+
bound: int
|
|
140
|
+
evict: str
|
|
141
|
+
keyed_by: str
|
|
142
|
+
persist: str
|
|
143
|
+
sensitive: bool
|
|
144
|
+
tags: list[str]
|
|
145
|
+
compartment: str
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class StoreLive(TypedDict):
|
|
149
|
+
"""What a store is holding now, for a compartment's report."""
|
|
150
|
+
kind: str
|
|
151
|
+
size: int
|
|
152
|
+
bound: int
|
|
153
|
+
high_water: int
|
|
154
|
+
inserts: int
|
|
155
|
+
evictions: int
|
|
156
|
+
refused: int
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclasses.dataclass(frozen=True)
|
|
160
|
+
class Store:
|
|
161
|
+
"""One collection a compartment keeps. See the module docstring."""
|
|
162
|
+
key: str
|
|
163
|
+
description: str
|
|
164
|
+
kind: str
|
|
165
|
+
shape: Optional[type] = None
|
|
166
|
+
bound: int = 0
|
|
167
|
+
evict: str = OLDEST
|
|
168
|
+
keyed_by: str = ''
|
|
169
|
+
persist: str = MEMORY
|
|
170
|
+
sensitive: bool = False
|
|
171
|
+
tags: tuple[str, ...] = ()
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def limit(self) -> int:
|
|
175
|
+
"""The bound in force: declared, or a COUNTERS store's field count."""
|
|
176
|
+
if self.kind == COUNTERS:
|
|
177
|
+
return len(_fields(self.shape))
|
|
178
|
+
return self.bound
|
|
179
|
+
|
|
180
|
+
def problems(self) -> list[str]:
|
|
181
|
+
"""Everything wrong with this declaration, in sentences, collected."""
|
|
182
|
+
out: list[str] = []
|
|
183
|
+
if not _KEY.match(self.key or ''):
|
|
184
|
+
out.append('store %r: a key is lowercase letters, digits and underscores' % self.key)
|
|
185
|
+
if not self.description:
|
|
186
|
+
out.append('store %s: say what it holds (description)' % self.key)
|
|
187
|
+
if self.kind not in KINDS:
|
|
188
|
+
out.append('store %s: kind must be one of %s' % (self.key, ', '.join(KINDS)))
|
|
189
|
+
if self.persist not in PERSISTS:
|
|
190
|
+
out.append('store %s: persist must be one of %s' % (self.key, ', '.join(PERSISTS)))
|
|
191
|
+
if self.kind == COUNTERS:
|
|
192
|
+
if not is_typeddict(self.shape):
|
|
193
|
+
out.append('store %s: COUNTERS name their fields with a TypedDict shape' % self.key)
|
|
194
|
+
if self.bound:
|
|
195
|
+
out.append('store %s: COUNTERS are bounded by their shape; drop bound' % self.key)
|
|
196
|
+
return out
|
|
197
|
+
if self.bound <= 0:
|
|
198
|
+
out.append('store %s: a %s must declare its bound - how far it may grow' % (self.key, self.kind))
|
|
199
|
+
if self.evict not in EVICTIONS:
|
|
200
|
+
out.append('store %s: evict must be one of %s' % (self.key, ', '.join(EVICTIONS)))
|
|
201
|
+
if self.kind == RING and self.evict != OLDEST:
|
|
202
|
+
out.append('store %s: a ring drops its oldest entry; evict is OLDEST' % self.key)
|
|
203
|
+
if self.kind in (TABLE, SET) and not self.keyed_by:
|
|
204
|
+
out.append('store %s: a %s says what it grows with (keyed_by: peer, event, url ...)'
|
|
205
|
+
% (self.key, self.kind))
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
def describe(self) -> StoreRecord:
|
|
209
|
+
out: StoreRecord = {'key': self.key, 'description': self.description, 'kind': self.kind,
|
|
210
|
+
'bound': self.limit, 'persist': self.persist, 'tags': list(self.tags)}
|
|
211
|
+
if self.shape is not None:
|
|
212
|
+
out['shape'] = getattr(self.shape, '__name__', str(self.shape))
|
|
213
|
+
if self.kind != COUNTERS:
|
|
214
|
+
out['evict'] = self.evict
|
|
215
|
+
if self.keyed_by:
|
|
216
|
+
out['keyed_by'] = self.keyed_by
|
|
217
|
+
if self.sensitive:
|
|
218
|
+
out['sensitive'] = True
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
def build(self) -> 'Container':
|
|
222
|
+
"""The container that keeps this declaration's promises."""
|
|
223
|
+
match self.kind:
|
|
224
|
+
case 'ring':
|
|
225
|
+
return Ring(self)
|
|
226
|
+
case 'table':
|
|
227
|
+
return Table(self)
|
|
228
|
+
case 'set':
|
|
229
|
+
return Members(self)
|
|
230
|
+
case _:
|
|
231
|
+
return Counters(self)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _fields(shape: Optional[type]) -> tuple[str, ...]:
|
|
235
|
+
if shape is None or not is_typeddict(shape):
|
|
236
|
+
return ()
|
|
237
|
+
return tuple(getattr(shape, '__annotations__', {}))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def declared(obj: Any) -> tuple[Store, ...]:
|
|
241
|
+
"""The stores a compartment declares, or () for one not yet converted."""
|
|
242
|
+
return tuple(getattr(obj, 'STORES', ()) or ())
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def every_store(compartments: Sequence[Any]) -> list[StoreRecord]:
|
|
246
|
+
"""Every declared store: what the node keeps, beside what it says
|
|
247
|
+
(every_channel), measures (every_metric) and can be told (every_parameter)."""
|
|
248
|
+
out: list[StoreRecord] = []
|
|
249
|
+
for c in compartments:
|
|
250
|
+
for s in declared(c):
|
|
251
|
+
row = s.describe()
|
|
252
|
+
row['compartment'] = getattr(c, 'name', '') or getattr(c, '__name__', str(c))
|
|
253
|
+
out.append(row)
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---- the containers ---------------------------------------------------------------
|
|
258
|
+
class _Container:
|
|
259
|
+
"""What every container shares: the declaration, a lock, and its counts.
|
|
260
|
+
|
|
261
|
+
Locked because the tick writes on one thread while the control socket
|
|
262
|
+
reads `state` on another; iterating a dict while it is resized raises.
|
|
263
|
+
Every read that iterates takes a copy under the lock.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, store: Store) -> None:
|
|
267
|
+
self.store: Store = store
|
|
268
|
+
self._lock: threading.RLock = threading.RLock()
|
|
269
|
+
self.inserts: int = 0
|
|
270
|
+
self.evictions: int = 0
|
|
271
|
+
self.refused: int = 0
|
|
272
|
+
self.high_water: int = 0
|
|
273
|
+
|
|
274
|
+
# Each container says how big it is; the counts are shared.
|
|
275
|
+
def __len__(self) -> int:
|
|
276
|
+
raise NotImplementedError
|
|
277
|
+
|
|
278
|
+
def _check(self, value: object) -> None:
|
|
279
|
+
why = shape_problem(self.store.shape, value)
|
|
280
|
+
if why:
|
|
281
|
+
raise TypeError('store %s: %s' % (self.store.key, why))
|
|
282
|
+
|
|
283
|
+
def _grew(self) -> None:
|
|
284
|
+
self.inserts += 1
|
|
285
|
+
n = len(self)
|
|
286
|
+
if n > self.high_water:
|
|
287
|
+
self.high_water = n
|
|
288
|
+
|
|
289
|
+
def live(self) -> StoreLive:
|
|
290
|
+
return {'kind': self.store.kind, 'size': len(self), 'bound': self.store.limit,
|
|
291
|
+
'high_water': self.high_water, 'inserts': self.inserts,
|
|
292
|
+
'evictions': self.evictions, 'refused': self.refused}
|
|
293
|
+
|
|
294
|
+
def nearly_full(self) -> bool:
|
|
295
|
+
lim = self.store.limit
|
|
296
|
+
return bool(lim) and len(self) >= NEARLY_FULL * lim
|
|
297
|
+
|
|
298
|
+
# ---- carried across a process restart (persist=SNAPSHOT) -----------------
|
|
299
|
+
def dump(self) -> object:
|
|
300
|
+
"""The contents as JSON-able data."""
|
|
301
|
+
raise NotImplementedError
|
|
302
|
+
|
|
303
|
+
def check(self, data: object) -> None:
|
|
304
|
+
"""Raise ValueError unless `data` is a dump this store can load whole.
|
|
305
|
+
Separate from load() so a compartment's restore is all or nothing."""
|
|
306
|
+
raise NotImplementedError
|
|
307
|
+
|
|
308
|
+
def load(self, data: object) -> None:
|
|
309
|
+
"""Replace the contents with a checked dump. Not counted as inserts."""
|
|
310
|
+
raise NotImplementedError
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
V = TypeVar('V')
|
|
314
|
+
K = TypeVar('K', bound=Hashable)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Ring(_Container, Generic[V]):
|
|
318
|
+
"""A log: newest last, the oldest dropped at the bound."""
|
|
319
|
+
|
|
320
|
+
def __init__(self, store: Store) -> None:
|
|
321
|
+
super().__init__(store)
|
|
322
|
+
self._items: collections.deque[V] = collections.deque()
|
|
323
|
+
|
|
324
|
+
def __len__(self) -> int:
|
|
325
|
+
return len(self._items)
|
|
326
|
+
|
|
327
|
+
def append(self, item: V) -> None:
|
|
328
|
+
self._check(item)
|
|
329
|
+
with self._lock:
|
|
330
|
+
self._items.append(item)
|
|
331
|
+
while len(self._items) > self.store.bound:
|
|
332
|
+
self._items.popleft()
|
|
333
|
+
self.evictions += 1
|
|
334
|
+
self._grew()
|
|
335
|
+
|
|
336
|
+
def extend(self, items: Iterable[V]) -> None:
|
|
337
|
+
for item in items:
|
|
338
|
+
self.append(item)
|
|
339
|
+
|
|
340
|
+
def last(self, n: int) -> list[V]:
|
|
341
|
+
with self._lock:
|
|
342
|
+
return list(self._items)[-n:] if n > 0 else []
|
|
343
|
+
|
|
344
|
+
def discard(self, item: V) -> bool:
|
|
345
|
+
"""Take back the newest entry equal to `item`; False if none is kept.
|
|
346
|
+
|
|
347
|
+
For a log written before the thing it records, and undone when that
|
|
348
|
+
thing failed (sync records a handover, then sends it). The newest,
|
|
349
|
+
because the entry being undone is the one just written. Not an
|
|
350
|
+
eviction: nothing was dropped for room, so the counts stay as they are.
|
|
351
|
+
"""
|
|
352
|
+
with self._lock:
|
|
353
|
+
for i in range(len(self._items) - 1, -1, -1):
|
|
354
|
+
if self._items[i] == item:
|
|
355
|
+
del self._items[i]
|
|
356
|
+
return True
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
def __getitem__(self, index: Union[int, slice]) -> Any:
|
|
360
|
+
with self._lock:
|
|
361
|
+
if isinstance(index, slice):
|
|
362
|
+
return list(self._items)[index]
|
|
363
|
+
return self._items[index]
|
|
364
|
+
|
|
365
|
+
def __iter__(self) -> Iterator[V]:
|
|
366
|
+
with self._lock:
|
|
367
|
+
return iter(list(self._items))
|
|
368
|
+
|
|
369
|
+
def clear(self) -> None:
|
|
370
|
+
with self._lock:
|
|
371
|
+
self._items.clear()
|
|
372
|
+
|
|
373
|
+
def dump(self) -> object:
|
|
374
|
+
with self._lock:
|
|
375
|
+
return list(self._items)
|
|
376
|
+
|
|
377
|
+
def check(self, data: object) -> None:
|
|
378
|
+
if not isinstance(data, list):
|
|
379
|
+
raise ValueError('store %s: a ring dump is a list' % self.store.key)
|
|
380
|
+
for item in data:
|
|
381
|
+
why = shape_problem(self.store.shape, item)
|
|
382
|
+
if why:
|
|
383
|
+
raise ValueError('store %s: %s' % (self.store.key, why))
|
|
384
|
+
|
|
385
|
+
def load(self, data: object) -> None:
|
|
386
|
+
self.check(data)
|
|
387
|
+
assert isinstance(data, list)
|
|
388
|
+
with self._lock:
|
|
389
|
+
self._items = collections.deque(data[-self.store.bound:])
|
|
390
|
+
self.high_water = max(self.high_water, len(self._items))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class Table(_Container, MutableMapping[K, V]):
|
|
394
|
+
"""A bounded mapping. Reads refresh an entry's place when evict is LRU."""
|
|
395
|
+
|
|
396
|
+
def __init__(self, store: Store) -> None:
|
|
397
|
+
super().__init__(store)
|
|
398
|
+
self._rows: collections.OrderedDict[K, V] = collections.OrderedDict()
|
|
399
|
+
|
|
400
|
+
def __len__(self) -> int:
|
|
401
|
+
return len(self._rows)
|
|
402
|
+
|
|
403
|
+
def __getitem__(self, key: K) -> V:
|
|
404
|
+
with self._lock:
|
|
405
|
+
v = self._rows[key]
|
|
406
|
+
if self.store.evict == LRU:
|
|
407
|
+
self._rows.move_to_end(key)
|
|
408
|
+
return v
|
|
409
|
+
|
|
410
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
411
|
+
if not self.put(key, value):
|
|
412
|
+
raise StoreFull('store %s is full (%d %s); it refuses new entries rather than drop one'
|
|
413
|
+
% (self.store.key, self.store.bound, self.store.keyed_by or 'entries'))
|
|
414
|
+
|
|
415
|
+
def put(self, key: K, value: V) -> bool:
|
|
416
|
+
"""Write one entry. False - and counted - when a REFUSE store is full."""
|
|
417
|
+
self._check(value)
|
|
418
|
+
with self._lock:
|
|
419
|
+
if key in self._rows:
|
|
420
|
+
self._rows[key] = value
|
|
421
|
+
if self.store.evict == LRU:
|
|
422
|
+
self._rows.move_to_end(key)
|
|
423
|
+
return True
|
|
424
|
+
if len(self._rows) >= self.store.bound:
|
|
425
|
+
if self.store.evict == REFUSE:
|
|
426
|
+
self.refused += 1
|
|
427
|
+
return False
|
|
428
|
+
self._rows.popitem(last=False)
|
|
429
|
+
self.evictions += 1
|
|
430
|
+
self._rows[key] = value
|
|
431
|
+
self._grew()
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
def __delitem__(self, key: K) -> None:
|
|
435
|
+
with self._lock:
|
|
436
|
+
del self._rows[key]
|
|
437
|
+
|
|
438
|
+
def __iter__(self) -> Iterator[K]:
|
|
439
|
+
with self._lock:
|
|
440
|
+
return iter(list(self._rows))
|
|
441
|
+
|
|
442
|
+
def __contains__(self, key: object) -> bool:
|
|
443
|
+
with self._lock:
|
|
444
|
+
return key in self._rows
|
|
445
|
+
|
|
446
|
+
def __repr__(self) -> str:
|
|
447
|
+
return 'Table(%s, %d/%d)' % (self.store.key, len(self), self.store.bound)
|
|
448
|
+
|
|
449
|
+
def dump(self) -> object:
|
|
450
|
+
# Pairs, not an object: a JSON object's keys are strings, and order
|
|
451
|
+
# is what LRU and OLDEST evict by.
|
|
452
|
+
with self._lock:
|
|
453
|
+
return [[k, v] for k, v in self._rows.items()]
|
|
454
|
+
|
|
455
|
+
def check(self, data: object) -> None:
|
|
456
|
+
if not isinstance(data, list) or not all(isinstance(p, list) and len(p) == 2 for p in data):
|
|
457
|
+
raise ValueError('store %s: a table dump is a list of [key, value] pairs' % self.store.key)
|
|
458
|
+
if len(data) > self.store.bound:
|
|
459
|
+
raise ValueError('store %s: %d entries, more than its bound of %d'
|
|
460
|
+
% (self.store.key, len(data), self.store.bound))
|
|
461
|
+
for k, v in data:
|
|
462
|
+
if not isinstance(k, (str, int)):
|
|
463
|
+
raise ValueError('store %s: key %r is not a string or an integer' % (self.store.key, k))
|
|
464
|
+
why = shape_problem(self.store.shape, v)
|
|
465
|
+
if why:
|
|
466
|
+
raise ValueError('store %s: %s' % (self.store.key, why))
|
|
467
|
+
|
|
468
|
+
def load(self, data: object) -> None:
|
|
469
|
+
self.check(data)
|
|
470
|
+
assert isinstance(data, list)
|
|
471
|
+
with self._lock:
|
|
472
|
+
self._rows = collections.OrderedDict((k, v) for k, v in data)
|
|
473
|
+
self.high_water = max(self.high_water, len(self._rows))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class Members(_Container, Generic[K]):
|
|
477
|
+
"""A bounded set. Evicts in the order members were added (or touched, LRU)."""
|
|
478
|
+
|
|
479
|
+
def __init__(self, store: Store) -> None:
|
|
480
|
+
super().__init__(store)
|
|
481
|
+
self._m: collections.OrderedDict[K, None] = collections.OrderedDict()
|
|
482
|
+
|
|
483
|
+
def __len__(self) -> int:
|
|
484
|
+
return len(self._m)
|
|
485
|
+
|
|
486
|
+
def add(self, member: K) -> bool:
|
|
487
|
+
"""Add one. False - and counted - when a REFUSE set is full."""
|
|
488
|
+
self._check(member)
|
|
489
|
+
with self._lock:
|
|
490
|
+
if member in self._m:
|
|
491
|
+
if self.store.evict == LRU:
|
|
492
|
+
self._m.move_to_end(member)
|
|
493
|
+
return True
|
|
494
|
+
if len(self._m) >= self.store.bound:
|
|
495
|
+
if self.store.evict == REFUSE:
|
|
496
|
+
self.refused += 1
|
|
497
|
+
return False
|
|
498
|
+
self._m.popitem(last=False)
|
|
499
|
+
self.evictions += 1
|
|
500
|
+
self._m[member] = None
|
|
501
|
+
self._grew()
|
|
502
|
+
return True
|
|
503
|
+
|
|
504
|
+
def discard(self, member: K) -> None:
|
|
505
|
+
with self._lock:
|
|
506
|
+
self._m.pop(member, None)
|
|
507
|
+
|
|
508
|
+
def remove(self, member: K) -> None:
|
|
509
|
+
with self._lock:
|
|
510
|
+
if member not in self._m:
|
|
511
|
+
raise KeyError(member)
|
|
512
|
+
del self._m[member]
|
|
513
|
+
|
|
514
|
+
def clear(self) -> None:
|
|
515
|
+
with self._lock:
|
|
516
|
+
self._m.clear()
|
|
517
|
+
|
|
518
|
+
def __contains__(self, member: object) -> bool:
|
|
519
|
+
with self._lock:
|
|
520
|
+
return member in self._m
|
|
521
|
+
|
|
522
|
+
def __iter__(self) -> Iterator[K]:
|
|
523
|
+
with self._lock:
|
|
524
|
+
return iter(list(self._m))
|
|
525
|
+
|
|
526
|
+
def dump(self) -> object:
|
|
527
|
+
with self._lock:
|
|
528
|
+
return list(self._m)
|
|
529
|
+
|
|
530
|
+
def check(self, data: object) -> None:
|
|
531
|
+
if not isinstance(data, list) or len(data) > self.store.bound:
|
|
532
|
+
raise ValueError('store %s: a set dump is a list within its bound' % self.store.key)
|
|
533
|
+
for m in data:
|
|
534
|
+
why = shape_problem(self.store.shape, m)
|
|
535
|
+
if why:
|
|
536
|
+
raise ValueError('store %s: %s' % (self.store.key, why))
|
|
537
|
+
|
|
538
|
+
def load(self, data: object) -> None:
|
|
539
|
+
self.check(data)
|
|
540
|
+
assert isinstance(data, list)
|
|
541
|
+
with self._lock:
|
|
542
|
+
self._m = collections.OrderedDict((m, None) for m in data)
|
|
543
|
+
self.high_water = max(self.high_water, len(self._m))
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class Counters(_Container):
|
|
547
|
+
"""A fixed set of named integers - the fields of the store's TypedDict."""
|
|
548
|
+
|
|
549
|
+
def __init__(self, store: Store) -> None:
|
|
550
|
+
super().__init__(store)
|
|
551
|
+
self._n: dict[str, int] = {f: 0 for f in _fields(store.shape)}
|
|
552
|
+
self.high_water = len(self._n)
|
|
553
|
+
|
|
554
|
+
def __len__(self) -> int:
|
|
555
|
+
return len(self._n)
|
|
556
|
+
|
|
557
|
+
def __getitem__(self, field: str) -> int:
|
|
558
|
+
return self._n[field]
|
|
559
|
+
|
|
560
|
+
def __setitem__(self, field: str, value: int) -> None:
|
|
561
|
+
if field not in self._n:
|
|
562
|
+
raise KeyError('store %s has no counter %r; its fields are %s'
|
|
563
|
+
% (self.store.key, field, ', '.join(self._n)))
|
|
564
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
565
|
+
raise TypeError('store %s: %s is a count, not %s' % (self.store.key, field, type(value).__name__))
|
|
566
|
+
with self._lock:
|
|
567
|
+
self._n[field] = value
|
|
568
|
+
|
|
569
|
+
def incr(self, field: str, n: int = 1) -> int:
|
|
570
|
+
with self._lock:
|
|
571
|
+
self[field] = self._n.get(field, 0) + n
|
|
572
|
+
self.inserts += 1
|
|
573
|
+
return self._n[field]
|
|
574
|
+
|
|
575
|
+
def as_dict(self) -> dict[str, int]:
|
|
576
|
+
with self._lock:
|
|
577
|
+
return dict(self._n)
|
|
578
|
+
|
|
579
|
+
def __iter__(self) -> Iterator[str]:
|
|
580
|
+
with self._lock:
|
|
581
|
+
return iter(list(self._n))
|
|
582
|
+
|
|
583
|
+
def items(self) -> list[tuple[str, int]]:
|
|
584
|
+
with self._lock:
|
|
585
|
+
return list(self._n.items())
|
|
586
|
+
|
|
587
|
+
def keys(self) -> list[str]:
|
|
588
|
+
with self._lock:
|
|
589
|
+
return list(self._n.keys())
|
|
590
|
+
|
|
591
|
+
def values(self) -> list[int]:
|
|
592
|
+
with self._lock:
|
|
593
|
+
return list(self._n.values())
|
|
594
|
+
|
|
595
|
+
def get(self, field: str, default: Optional[int] = None) -> Optional[int]:
|
|
596
|
+
with self._lock:
|
|
597
|
+
return self._n.get(field, default)
|
|
598
|
+
|
|
599
|
+
def dump(self) -> object:
|
|
600
|
+
return self.as_dict()
|
|
601
|
+
|
|
602
|
+
def check(self, data: object) -> None:
|
|
603
|
+
if not isinstance(data, dict):
|
|
604
|
+
raise ValueError('store %s: a counters dump is an object' % self.store.key)
|
|
605
|
+
for f in self._n:
|
|
606
|
+
v = data.get(f)
|
|
607
|
+
if not isinstance(v, int) or isinstance(v, bool) or v < 0:
|
|
608
|
+
raise ValueError('store %s: %s is %r, not a count' % (self.store.key, f, v))
|
|
609
|
+
|
|
610
|
+
def load(self, data: object) -> None:
|
|
611
|
+
self.check(data)
|
|
612
|
+
assert isinstance(data, dict)
|
|
613
|
+
with self._lock:
|
|
614
|
+
self._n = {f: int(data[f]) for f in self._n}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
Container = Union[Ring[Any], Table[Any, Any], Members[Any], Counters]
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class Holdings:
|
|
621
|
+
"""One compartment's containers, built from its STORES. Owned by the base."""
|
|
622
|
+
|
|
623
|
+
def __init__(self, stores: Sequence[Store]) -> None:
|
|
624
|
+
self._by_key: dict[str, Container] = {s.key: s.build() for s in stores}
|
|
625
|
+
|
|
626
|
+
def __getitem__(self, key: str) -> Container:
|
|
627
|
+
return self._by_key[key]
|
|
628
|
+
|
|
629
|
+
def __iter__(self) -> Iterator[str]:
|
|
630
|
+
return iter(self._by_key)
|
|
631
|
+
|
|
632
|
+
def __len__(self) -> int:
|
|
633
|
+
return len(self._by_key)
|
|
634
|
+
|
|
635
|
+
def items(self) -> list[tuple[str, Container]]:
|
|
636
|
+
return list(self._by_key.items())
|
|
637
|
+
|
|
638
|
+
def live(self) -> dict[str, StoreLive]:
|
|
639
|
+
return {k: c.live() for k, c in self._by_key.items()}
|
|
640
|
+
|
|
641
|
+
def persisted(self) -> dict[str, Container]:
|
|
642
|
+
return {k: c for k, c in self._by_key.items() if c.store.persist == SNAPSHOT}
|
|
643
|
+
|
|
644
|
+
def snapshot(self) -> Mapping[str, Any]:
|
|
645
|
+
return {k: c.dump() for k, c in self.persisted().items()}
|
|
646
|
+
|
|
647
|
+
def restore(self, state: Mapping[str, Any]) -> None:
|
|
648
|
+
"""All or nothing: every persisted store checks its dump, then all load."""
|
|
649
|
+
mine = self.persisted()
|
|
650
|
+
for k, c in mine.items():
|
|
651
|
+
if k not in state:
|
|
652
|
+
raise ValueError('the snapshot has no store %s' % k)
|
|
653
|
+
c.check(state[k])
|
|
654
|
+
for k, c in mine.items():
|
|
655
|
+
c.load(state[k])
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
__all__ = [
|
|
659
|
+
'COUNTERS', 'Container', 'Counters', 'EVICTIONS', 'Holdings', 'KINDS', 'LRU', 'MEMORY', 'Members',
|
|
660
|
+
'NEARLY_FULL', 'OLDEST', 'PERSISTS', 'REFUSE', 'RING', 'Ring', 'SET', 'SNAPSHOT', 'Store', 'StoreFull',
|
|
661
|
+
'StoreLive', 'StoreRecord', 'TABLE', 'Table', 'declared', 'every_store', 'shape_problem',
|
|
662
|
+
]
|