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,110 @@
|
|
|
1
|
+
"""The scheduler's tick: what is due for every peer, and the retention
|
|
2
|
+
horizon. Separate because it is the one place the receipts and the windows
|
|
3
|
+
are driven from, and the one place that forgets.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from fleetmesh_core.accounting import accounting
|
|
10
|
+
from node.storage import grants as grants_mod
|
|
11
|
+
from .meter import C, KEEP_FINDINGS
|
|
12
|
+
from .shape import BudgetShape
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TickMixin:
|
|
16
|
+
# ---- the scheduler's tick ---------------------------------------------------
|
|
17
|
+
def tick(self: BudgetShape, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
18
|
+
"""Send due receipts; evaluate closed windows. Returns what it did."""
|
|
19
|
+
now_val = now if now is not None else self.clock()
|
|
20
|
+
did: list[tuple[Any, ...]] = []
|
|
21
|
+
self._save_meters()
|
|
22
|
+
self._forget_old(now_val)
|
|
23
|
+
for lst in (self.findings, self.clock_drift, self.receipt_gaps, self.unpublished, self.evaluations):
|
|
24
|
+
if isinstance(lst, list) and len(lst) > KEEP_FINDINGS:
|
|
25
|
+
del lst[:len(lst) - KEEP_FINDINGS]
|
|
26
|
+
for peer in self._peers():
|
|
27
|
+
try:
|
|
28
|
+
did += self._tick_peer(peer, now_val)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
# One peer must not end the tick for the others. A peer whose
|
|
31
|
+
# descriptor no longer resolves - it expired, or it declares a
|
|
32
|
+
# protocol digest this node refuses - raised TransportError out
|
|
33
|
+
# of `send_receipt`, and every peer after it in `grants.peers()`
|
|
34
|
+
# order silently missed its receipts for that window. On the
|
|
35
|
+
# 2026-09-26 roll that was three of four nodes, once a window,
|
|
36
|
+
# for as long as one peer stayed behind the digest.
|
|
37
|
+
#
|
|
38
|
+
# `scheduler.py` already learned this and says so at its own two
|
|
39
|
+
# call sites. This is the same fault in the compartment that
|
|
40
|
+
# owes the receipts.
|
|
41
|
+
self.refusals.refuse('accounting', 'the peer raised', subject=peer,
|
|
42
|
+
detail={'error': '%s: %s' % (type(e).__name__, e)}, at=now_val)
|
|
43
|
+
did.append(('accounting failed', peer, '%s: %s' % (type(e).__name__, e)))
|
|
44
|
+
return did
|
|
45
|
+
|
|
46
|
+
def _tick_peer(self: BudgetShape, peer: str, now_val: int) -> list[tuple[Any, ...]]:
|
|
47
|
+
"""One peer's share of the tick: its grant, its receipts, its windows."""
|
|
48
|
+
did: list[tuple[Any, ...]] = []
|
|
49
|
+
out = self.publish_pending(peer, now_val)
|
|
50
|
+
if out is not None:
|
|
51
|
+
did.append(('grant published', peer, out['payload']['tier'], out['envelope']['id']))
|
|
52
|
+
prev = self.final_due(peer, now_val)
|
|
53
|
+
if prev is not None:
|
|
54
|
+
body, head = self.send_receipt(peer, now_val, final=True, epoch=prev)
|
|
55
|
+
self.chain_out[peer][prev].final = True
|
|
56
|
+
did.append(('final receipt', peer, prev, body['seq']))
|
|
57
|
+
if self.receipt_due(peer, now_val):
|
|
58
|
+
body, head = self.send_receipt(peer, now_val)
|
|
59
|
+
did.append(('receipt', peer, body['seq']))
|
|
60
|
+
for e in self.reopen_due(peer):
|
|
61
|
+
rp = self.reprice(peer, e, now_val)
|
|
62
|
+
if rp:
|
|
63
|
+
did.append(('repriced', peer, e, rp.was, rp.severity))
|
|
64
|
+
for e in self.closeable(peer, now_val):
|
|
65
|
+
ev = self.evaluate(peer, e, now_val)
|
|
66
|
+
if ev:
|
|
67
|
+
did.append(('evaluated', peer, e, ev.severity))
|
|
68
|
+
return did
|
|
69
|
+
|
|
70
|
+
def _forget_old(self: BudgetShape, now: int) -> None:
|
|
71
|
+
"""Drop accounting for windows nothing can still be said about.
|
|
72
|
+
|
|
73
|
+
Both halves of it. This forgot the disk and not the memory the disk is
|
|
74
|
+
written back from: `_save_meters` walks usage_by and usage_of_me and
|
|
75
|
+
writes every epoch in them, so meters deleted on one tick were re-
|
|
76
|
+
inserted on the next, while the receipts and the evaluated marker
|
|
77
|
+
beside them stayed gone.
|
|
78
|
+
|
|
79
|
+
A window in that state - metered, no receipt, no record of having been
|
|
80
|
+
priced - is exactly the shape of active silence, and it was priced as
|
|
81
|
+
such. On the live mesh nyc3 held meters from epoch 496806 against
|
|
82
|
+
receipts that began at 496818, and re-priced the twelve windows between
|
|
83
|
+
as S2 on every restart, a batch at a time, against a peer that had done
|
|
84
|
+
nothing. It was pricing its own retention policy as the peer's fault.
|
|
85
|
+
"""
|
|
86
|
+
if self.accounts is None:
|
|
87
|
+
return
|
|
88
|
+
oldest = min((self._window_of(p, grants_mod.ISSUED) for p in self._peers()),
|
|
89
|
+
default=C['window_seconds_default'])
|
|
90
|
+
keep = self.KEEP_WINDOWS
|
|
91
|
+
if self.strain is not None:
|
|
92
|
+
# A reserve, not a constant. A node that has watched peers vanish
|
|
93
|
+
# and come back keeps their accounting longer, so a chain still
|
|
94
|
+
# links when one does. Counted by direction, so one peer
|
|
95
|
+
# disappearing over and over buys a single direction's patience.
|
|
96
|
+
keep = int(self.strain.reserve(self.KEEP_WINDOWS, 'partition'))
|
|
97
|
+
horizon = accounting.epoch(now, oldest) - keep
|
|
98
|
+
if horizon > getattr(self, '_forgotten_to', -1):
|
|
99
|
+
self.accounts.forget_before(horizon)
|
|
100
|
+
self._forget_in_memory(horizon)
|
|
101
|
+
self._forgotten_to = horizon
|
|
102
|
+
|
|
103
|
+
def _forget_in_memory(self: BudgetShape, horizon: int) -> None:
|
|
104
|
+
"""The same cutoff, applied to what the tick writes back."""
|
|
105
|
+
with self._lock:
|
|
106
|
+
for table in (self.usage_by, self.usage_of_me, self.receipts_in):
|
|
107
|
+
for by_epoch in table.values():
|
|
108
|
+
for e in [e for e in by_epoch if e < horizon]:
|
|
109
|
+
by_epoch.pop(e, None)
|
|
110
|
+
self.reopen = {(pk, e) for pk, e in self.reopen if e >= horizon} # not a store: pruned horizon
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Types and records for the Budget compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Mapping, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
from fleetmesh_core.wire.interfaces import ReceiptBody, SilenceEvaluation, WindowEvaluationDetail
|
|
7
|
+
from node.interfaces import ReceiptRefusedRow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OutChainDict(TypedDict, total=False):
|
|
11
|
+
"""The JSON serialization shape of an OutChain."""
|
|
12
|
+
grant: str
|
|
13
|
+
seq: int
|
|
14
|
+
prev: str
|
|
15
|
+
sent_at: Optional[int]
|
|
16
|
+
last: Optional[dict[str, int]]
|
|
17
|
+
heads: list[str]
|
|
18
|
+
final: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EvaluationDict(TypedDict, total=False):
|
|
22
|
+
"""The JSON serialization shape of an Evaluation."""
|
|
23
|
+
peer: str
|
|
24
|
+
epoch: int
|
|
25
|
+
state: str
|
|
26
|
+
measured: dict[str, int]
|
|
27
|
+
claimed: Optional[dict[str, int]]
|
|
28
|
+
tier_before: str
|
|
29
|
+
grant: str
|
|
30
|
+
limits: Mapping[str, int | float]
|
|
31
|
+
severity: Optional[str]
|
|
32
|
+
note: str
|
|
33
|
+
detail: dict[str, WindowEvaluationDetail | SilenceEvaluation]
|
|
34
|
+
tier_after: Optional[str]
|
|
35
|
+
limits_after: Optional[Mapping[str, int | float]]
|
|
36
|
+
effective_from: Optional[int]
|
|
37
|
+
receipt_heads: list[str]
|
|
38
|
+
receipts: list[ReceiptBody]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RepricingDict(TypedDict):
|
|
42
|
+
"""The JSON serialization shape of a Repricing."""
|
|
43
|
+
peer: str
|
|
44
|
+
epoch: int
|
|
45
|
+
was: str
|
|
46
|
+
severity: str
|
|
47
|
+
state: str
|
|
48
|
+
measured: dict[str, int]
|
|
49
|
+
claimed: Optional[dict[str, int]]
|
|
50
|
+
receipts: int
|
|
51
|
+
remedy_cancelled: bool
|
|
52
|
+
at: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BudgetStateReport(TypedDict):
|
|
56
|
+
peers: int
|
|
57
|
+
windows_evaluated: int
|
|
58
|
+
findings: int
|
|
59
|
+
grants_waiting_for_a_boundary: int
|
|
60
|
+
clock_drift: int
|
|
61
|
+
receipt_gaps: int
|
|
62
|
+
receipts_behind: dict[str, int]
|
|
63
|
+
receipts_unpublished: int
|
|
64
|
+
receipt_chains_open: int
|
|
65
|
+
idle_streaks: dict[str, int]
|
|
66
|
+
rejected: dict[str, int]
|
|
67
|
+
receipts_refused: list[ReceiptRefusedRow]
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""The window, priced: evaluate a closed one, publish the grant it decided
|
|
2
|
+
on, enforce the grant in force, and reprice on a late receipt. Separate
|
|
3
|
+
because this is where fleetmesh_core/window.py's ladder is applied and a grant is
|
|
4
|
+
changed; nothing here reads the wire.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import secrets
|
|
9
|
+
from typing import Any, Mapping, Optional
|
|
10
|
+
|
|
11
|
+
from fleetmesh_core.accounting import accounting, window
|
|
12
|
+
from fleetmesh_core.storage import sync
|
|
13
|
+
from node.storage import grants as grants_mod
|
|
14
|
+
from .evaluation import Evaluation, Repricing
|
|
15
|
+
from .meter import C, KEEP_FINDINGS, Meter
|
|
16
|
+
from .shape import BudgetShape
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WindowsMixin:
|
|
20
|
+
# ---- issuer: closing windows ---------------------------------------------
|
|
21
|
+
def evaluate(self: BudgetShape, peer: str, epoch: int, now: Optional[int] = None) -> Optional[Evaluation]:
|
|
22
|
+
"""One closed window against one subject. Returns the evaluation record."""
|
|
23
|
+
now_val = now if now is not None else self.clock()
|
|
24
|
+
row = self.grants.get(peer, grants_mod.ISSUED)
|
|
25
|
+
if row is None or row['status'] != 'live':
|
|
26
|
+
return None
|
|
27
|
+
# Priced under the grant that governed the window, not the one that is
|
|
28
|
+
# current when the grace period ends: a window that ran under one set
|
|
29
|
+
# of limits and was judged under another was a finding about nothing.
|
|
30
|
+
governed = self.governing(peer, grants_mod.ISSUED, epoch) or row
|
|
31
|
+
limits, tier = governed['payload']['limits'], governed['payload']['tier']
|
|
32
|
+
# The remedy applies to the grant that will be in force, which is the
|
|
33
|
+
# one already waiting for the boundary if a window was closed before
|
|
34
|
+
# this one in the same grace period, and otherwise the current one.
|
|
35
|
+
pending = self.grants.pending(peer)
|
|
36
|
+
base = pending['payload'] if pending else row['payload']
|
|
37
|
+
measured = self.usage_by.get(peer, {}).get(epoch, Meter()).snapshot()
|
|
38
|
+
receipts = self.receipts_in.get(peer, {}).get(epoch, [])
|
|
39
|
+
state, claimed = accounting.window_claim(receipts, measured)
|
|
40
|
+
rec = Evaluation(peer=peer, epoch=epoch, state=state, measured=measured, claimed=claimed,
|
|
41
|
+
tier_before=tier, grant=governed['envelope_id'], limits=limits)
|
|
42
|
+
if state == 'not-evaluated':
|
|
43
|
+
rec.note = 'nothing moved and nothing was claimed'
|
|
44
|
+
self.idle[peer] = self.idle.get(peer, 0) + 1
|
|
45
|
+
# Capacity earned in one busy week used to sit there a year later as
|
|
46
|
+
# surface nobody was watching: a limit moved up on a clean window
|
|
47
|
+
# and down on a fault and never decayed. A grant that has measured
|
|
48
|
+
# nothing for long enough gives some of itself back, down to where a
|
|
49
|
+
# peering opens and never below. Nothing happened, which is the whole
|
|
50
|
+
# of the reason, so it is amended with cause disuse and is not a
|
|
51
|
+
# finding about anyone.
|
|
52
|
+
if self.idle[peer] >= C['decay_idle_windows'] and base.get('limits'):
|
|
53
|
+
decayed = window.decay(base['limits'], base['tier'])
|
|
54
|
+
decayed = {k: (int(v) if float(v).is_integer() else v) for k, v in decayed.items()}
|
|
55
|
+
if decayed != base['limits']:
|
|
56
|
+
# Disuse is not a finding: the pending row carries no
|
|
57
|
+
# severity and `_apply_pending` reads that absence as the
|
|
58
|
+
# cause. GrantStore.put_pending annotates severity as str
|
|
59
|
+
# although it stores NULL here (node/grants.py, not owned
|
|
60
|
+
# by this change).
|
|
61
|
+
self.grants.put_pending(
|
|
62
|
+
peer, dict(base, limits=decayed, blinding=secrets.token_bytes(32).hex()),
|
|
63
|
+
'decayed after %d windows that measured nothing' % self.idle[peer],
|
|
64
|
+
None,
|
|
65
|
+
accounting.epoch(now_val, self._window_of(peer, grants_mod.ISSUED)))
|
|
66
|
+
rec.note += '; grant decayed for disuse'
|
|
67
|
+
self.idle[peer] = 0
|
|
68
|
+
self.evaluations.append(rec)
|
|
69
|
+
self.evaluated.add((peer, epoch))
|
|
70
|
+
if self.accounts is not None:
|
|
71
|
+
self.accounts.put_evaluated(peer, epoch, None)
|
|
72
|
+
return rec
|
|
73
|
+
# A window with no receipt is still a window that was measured. Silence
|
|
74
|
+
# is a floor of S2 and not a ceiling: skipping the arithmetic here - which
|
|
75
|
+
# is what this did - priced a peer that flooded and said nothing BELOW the
|
|
76
|
+
# same peer reporting it honestly, because honest excess reaches S3 on
|
|
77
|
+
# overrun alone. fleetmesh_core/badpeer.py plugged into a node is what found it, and
|
|
78
|
+
# it is the same inversion the severity ladder itself had. An absent claim
|
|
79
|
+
# is the empty claim, which is how vectors/window.json has always priced
|
|
80
|
+
# "no receipt at all".
|
|
81
|
+
self.idle[peer] = 0
|
|
82
|
+
ev = window.evaluate_window(limits, measured, claimed or {})
|
|
83
|
+
severity, detail = ev['severity'], ev['per_limit']
|
|
84
|
+
if state == 'active-silence':
|
|
85
|
+
severity = window.worse(severity, 'S2')
|
|
86
|
+
detail = dict(detail, silence=accounting.silence(measured))
|
|
87
|
+
self.findings.append((peer, epoch, severity, 'traffic measured and no receipt: active silence'))
|
|
88
|
+
for name, d in ev['per_limit'].items():
|
|
89
|
+
if d['severity'] != 'S0':
|
|
90
|
+
self.findings.append((peer, epoch, d['severity'], '%s: %s' % (name, d)))
|
|
91
|
+
# The clean branch grows each limit by what this window actually carried.
|
|
92
|
+
new_limits, new_tier, note = window.adjust(base['limits'], base['tier'], severity, measured)
|
|
93
|
+
if severity == 'S3':
|
|
94
|
+
self.zero_until[peer] = now_val + C['s3_zero_hours'] * 3600
|
|
95
|
+
rounded = {k: (int(v) if float(v).is_integer() else v) for k, v in new_limits.items()}
|
|
96
|
+
new_payload = dict(base, limits=rounded, tier=new_tier, blinding=secrets.token_bytes(32).hex())
|
|
97
|
+
decided = accounting.epoch(now_val, self._window_of(peer, grants_mod.ISSUED))
|
|
98
|
+
self.grants.put_pending(peer, new_payload, '%s after %s' % (note, severity), severity, decided)
|
|
99
|
+
self.ledger.record(peer, epoch, severity, new_tier)
|
|
100
|
+
rec.severity, rec.detail, rec.note = severity, detail, note
|
|
101
|
+
rec.tier_after, rec.limits_after, rec.effective_from = new_tier, rounded, decided + 1
|
|
102
|
+
self.evaluations.append(rec)
|
|
103
|
+
self.evaluated.add((peer, epoch))
|
|
104
|
+
if self.accounts is not None:
|
|
105
|
+
self.accounts.put_evaluated(peer, epoch, severity, new_tier)
|
|
106
|
+
rec.receipt_heads = [accounting.link(r) for r in receipts]
|
|
107
|
+
rec.receipts = list(receipts) # the bodies, which are the evidence
|
|
108
|
+
for cb in self.on_evaluated:
|
|
109
|
+
cb(rec)
|
|
110
|
+
return rec
|
|
111
|
+
|
|
112
|
+
def publish_pending(self: BudgetShape, peer: str, now: Optional[int] = None) -> Optional[Mapping[str, Any]]:
|
|
113
|
+
"""At a window boundary, the grant the last evaluation decided on goes
|
|
114
|
+
out: written, published as a new kind 30801 at the same coordinate, and
|
|
115
|
+
the subject told with peering/1.0/amend. Returns the envelope or None."""
|
|
116
|
+
now_val = now if now is not None else self.clock()
|
|
117
|
+
pend = self.grants.pending(peer)
|
|
118
|
+
if pend is None:
|
|
119
|
+
return None
|
|
120
|
+
w = self._window_of(peer, grants_mod.ISSUED)
|
|
121
|
+
current = accounting.epoch(now_val, w)
|
|
122
|
+
if current <= pend['decided_epoch']:
|
|
123
|
+
return None
|
|
124
|
+
row = self.grants.get(peer, grants_mod.ISSUED)
|
|
125
|
+
if row is None or row['status'] != 'live':
|
|
126
|
+
self.grants.clear_pending(peer)
|
|
127
|
+
return None
|
|
128
|
+
payload = pend['payload']
|
|
129
|
+
# The tier ceiling is enforced where a grant is written and not only
|
|
130
|
+
# where one is computed. window.adjust holds its own result inside the
|
|
131
|
+
# tier it returns, but the thing the specification constrains is the
|
|
132
|
+
# grant that reaches the wire, and a pending payload can have been
|
|
133
|
+
# decided by an older build and slept through a restart. If this ever
|
|
134
|
+
# fires the defect is upstream, so it is recorded against this node.
|
|
135
|
+
# A pending grant with no severity was not decided by the ladder. The only
|
|
136
|
+
# path that writes one is decay, so this is where disuse reaches the wire.
|
|
137
|
+
cause = 'remedy' if pend.get('severity') else 'disuse'
|
|
138
|
+
# The clamp, the envelope, the store, the amend: peering owns all of it
|
|
139
|
+
# now. This used to be its own copy of that procedure, and it was the
|
|
140
|
+
# only copy that clamped the payload to its tier ceiling.
|
|
141
|
+
out = self.peering.issue(peer, payload, cause, pend['note'], effective_epoch=current, now=now_val,
|
|
142
|
+
on_clamp=lambda pk, tier, over: self.findings.append(
|
|
143
|
+
('self', None, None,
|
|
144
|
+
'pending grant for %s carried limits above the %s ceiling: %s'
|
|
145
|
+
% (pk[:8], tier, over))))
|
|
146
|
+
# What standing tells a third party about this peer is the tier of the
|
|
147
|
+
# grant in force, so it moves when the grant does. The operator path
|
|
148
|
+
# used to set this itself the moment a person decided; now that every
|
|
149
|
+
# decision lands at the boundary, the boundary is where it belongs, and
|
|
150
|
+
# both paths get it from the same line.
|
|
151
|
+
if self.ledger is not None:
|
|
152
|
+
self.ledger.tier[peer] = out['payload'].get('tier')
|
|
153
|
+
return {'envelope': out['envelope'], 'payload': out['payload'],
|
|
154
|
+
'published': out['published'], 'severity': pend['severity']}
|
|
155
|
+
|
|
156
|
+
# ---- the grant, enforced ----------------------------------------------------
|
|
157
|
+
def _over(self: BudgetShape, limits: Mapping[str, Any], counters: Mapping[str, Any], opening: bool = False) -> Optional[str]:
|
|
158
|
+
"""The first limit a session would go past, or None. `opening` adds
|
|
159
|
+
the connection and request the open itself costs. The reference's own
|
|
160
|
+
`Grant.exceeded` decides, so what is refused here is exactly what the
|
|
161
|
+
ladder would price, under the same grant.counter_of mapping."""
|
|
162
|
+
c = dict(counters)
|
|
163
|
+
if opening:
|
|
164
|
+
c['conn_open'] = c.get('conn_open', 0) + 1
|
|
165
|
+
c['req'] = c.get('req', 0) + 1
|
|
166
|
+
over = sync.Grant('', '', limits).exceeded(c)
|
|
167
|
+
if not over:
|
|
168
|
+
return None
|
|
169
|
+
name = sorted(over)[0]
|
|
170
|
+
return '%s %s over the grant\'s %s' % (name, c.get(window.counter_of(name), 0), limits[name])
|
|
171
|
+
|
|
172
|
+
def throttled(self: BudgetShape, peer: str, now: Optional[int] = None) -> Optional[str]:
|
|
173
|
+
"""Why this node must not open a session with the peer right now, or
|
|
174
|
+
None. Read from the grant it holds: revoked, zero, or a limit the
|
|
175
|
+
window's usage so far has reached."""
|
|
176
|
+
now_val = now if now is not None else self.clock()
|
|
177
|
+
row = self.grants.get(peer, grants_mod.RECEIVED)
|
|
178
|
+
if row is None or row['status'] != 'live':
|
|
179
|
+
return 'no live grant from the peer'
|
|
180
|
+
limits = row['payload'].get('limits', {})
|
|
181
|
+
if row['payload'].get('tier') == 'revoked' or not any(limits.values()):
|
|
182
|
+
return 'the grant is zero'
|
|
183
|
+
epoch = self.epoch(peer, grants_mod.RECEIVED, now_val)
|
|
184
|
+
used = self._meter(self.usage_of_me, peer, epoch).snapshot()
|
|
185
|
+
return self._over(limits, used, opening=True)
|
|
186
|
+
|
|
187
|
+
def granted_rate(self: BudgetShape, peer: str) -> tuple[int, int]:
|
|
188
|
+
"""(sessions the grant allows per window, the window in seconds).
|
|
189
|
+
|
|
190
|
+
What a cadence is derived from when none is configured: the rate the
|
|
191
|
+
peer actually granted, so a tier promotion changes how often this node
|
|
192
|
+
pulls without anybody editing a number beside it.
|
|
193
|
+
"""
|
|
194
|
+
row = self.grants.get(peer, grants_mod.RECEIVED)
|
|
195
|
+
w = self._window_of(peer, grants_mod.RECEIVED)
|
|
196
|
+
if row is None or row['status'] != 'live':
|
|
197
|
+
return (0, w)
|
|
198
|
+
limits = row['payload'].get('limits', {})
|
|
199
|
+
return (int(limits.get('conn_rate', 0) or 0), w)
|
|
200
|
+
|
|
201
|
+
def sessions_left(self: BudgetShape, peer: str, now: Optional[int] = None) -> tuple[int, int]:
|
|
202
|
+
"""How many more sessions this node may open with the peer in the
|
|
203
|
+
window that is running, and when that window closes.
|
|
204
|
+
|
|
205
|
+
The pair the scheduler needs to pace itself from the contract rather
|
|
206
|
+
than from a configured number. `conn_rate` is what the peer granted
|
|
207
|
+
per window; `conn_open` is what this node has already spent of it, and
|
|
208
|
+
it is read from the meter, which is the record the peer also prices -
|
|
209
|
+
not a private clock the scheduler keeps on the side.
|
|
210
|
+
|
|
211
|
+
Returns (0, close) when the budget is spent, which is a wait rather
|
|
212
|
+
than a refusal: the allowance returns when the window turns.
|
|
213
|
+
"""
|
|
214
|
+
now_val = now if now is not None else self.clock()
|
|
215
|
+
row = self.grants.get(peer, grants_mod.RECEIVED)
|
|
216
|
+
if row is None or row['status'] != 'live':
|
|
217
|
+
return (0, now_val)
|
|
218
|
+
limits = row['payload'].get('limits', {})
|
|
219
|
+
allowed = int(limits.get('conn_rate', 0) or 0)
|
|
220
|
+
w = self._window_of(peer, grants_mod.RECEIVED)
|
|
221
|
+
epoch = self.epoch(peer, grants_mod.RECEIVED, now_val)
|
|
222
|
+
closes = accounting.window_bounds(epoch, w)[1]
|
|
223
|
+
if row['payload'].get('tier') == 'revoked' or allowed <= 0:
|
|
224
|
+
return (0, closes)
|
|
225
|
+
used = self._meter(self.usage_of_me, peer, epoch).snapshot()
|
|
226
|
+
spent = int(used.get(window.counter_of('conn_rate'), 0) or 0)
|
|
227
|
+
return (max(0, allowed - spent), closes)
|
|
228
|
+
|
|
229
|
+
def refuses(self: BudgetShape, peer: str, now: Optional[int] = None) -> Optional[str]:
|
|
230
|
+
"""Why this node must not serve the peer's session, or None. Read from
|
|
231
|
+
the grant it issued and governing now, plus an S3 zero still running."""
|
|
232
|
+
now_val = now if now is not None else self.clock()
|
|
233
|
+
if self.zero_until.get(peer, 0) > now_val:
|
|
234
|
+
return 'zeroed after S3 until %d' % self.zero_until[peer]
|
|
235
|
+
row = self.grants.get(peer, grants_mod.ISSUED)
|
|
236
|
+
if row is None or row['status'] != 'live':
|
|
237
|
+
return 'no live grant to the peer'
|
|
238
|
+
epoch = self.epoch(peer, grants_mod.ISSUED, now_val)
|
|
239
|
+
limits = (self.governing(peer, grants_mod.ISSUED, epoch) or row)['payload'].get('limits', {})
|
|
240
|
+
if row['payload'].get('tier') == 'revoked' or not any(limits.values()):
|
|
241
|
+
return 'the grant is zero'
|
|
242
|
+
used = self._meter(self.usage_by, peer, epoch).snapshot()
|
|
243
|
+
return self._over(limits, used)
|
|
244
|
+
|
|
245
|
+
def closeable(self: BudgetShape, peer: str, now: int) -> list[int]:
|
|
246
|
+
"""Epochs of my grant to this peer that have closed and passed grace."""
|
|
247
|
+
w = self._window_of(peer, grants_mod.ISSUED)
|
|
248
|
+
current = accounting.epoch(now, w)
|
|
249
|
+
# The window a receipt may not name is a window that may not be priced.
|
|
250
|
+
# on_receipt refuses one outside this bound as "answerable by nothing",
|
|
251
|
+
# and a verdict reached where no answer is admissible is not a verdict:
|
|
252
|
+
# the subject cannot refute it, and the evidence to support it is what
|
|
253
|
+
# this node deliberately threw away. Same bound, both directions.
|
|
254
|
+
oldest = current - self.KEEP_WINDOWS
|
|
255
|
+
with self._lock: # the sync thread grows these; see _save_meters
|
|
256
|
+
seen = set(self.usage_by.get(peer, {})) | set(self.receipts_in.get(peer, {}))
|
|
257
|
+
out: list[int] = []
|
|
258
|
+
for e in sorted(seen):
|
|
259
|
+
if e < oldest:
|
|
260
|
+
continue
|
|
261
|
+
if e < current and (peer, e) not in self.evaluated and now >= accounting.window_bounds(e, w)[1] + accounting.window_grace(w):
|
|
262
|
+
out.append(e)
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
def reopen_due(self: BudgetShape, peer: str) -> list[int]:
|
|
266
|
+
"""Windows already priced that a receipt has since landed on. Draining
|
|
267
|
+
the queue is what makes a reprice happen once and not on every tick."""
|
|
268
|
+
with self._lock:
|
|
269
|
+
mine = sorted(e for pk, e in self.reopen if pk == peer)
|
|
270
|
+
self.reopen -= {(peer, e) for e in mine}
|
|
271
|
+
return mine
|
|
272
|
+
|
|
273
|
+
def reprice(self: BudgetShape, peer: str, epoch: int, now: Optional[int] = None) -> Optional[Repricing]:
|
|
274
|
+
"""Price one window again, because a receipt covering it arrived late.
|
|
275
|
+
|
|
276
|
+
A window closes on the clock and is judged once its grace has run, and
|
|
277
|
+
the subject's last receipt for it can arrive after that: the grace is
|
|
278
|
+
window_grace_fraction of the window - 360 seconds at the default - and
|
|
279
|
+
a control-plane gap of 882 seconds has been measured on a running mesh.
|
|
280
|
+
What the issuer holds then is a claim missing its tail, which is
|
|
281
|
+
arithmetically indistinguishable from a claim that was understated. So
|
|
282
|
+
an honest peer earns the S2 misreport floor for a delivery gap, and
|
|
283
|
+
cannot refute it: the published classifier run over the receipt it did
|
|
284
|
+
sign returns S2, correctly. That is the fourth layer at which something
|
|
285
|
+
other than dishonesty has been priced as dishonesty, after the session
|
|
286
|
+
abort, the standing streak, and the node that vanishes mid-window which
|
|
287
|
+
`farewell` answers. The first three were fixed by saying something
|
|
288
|
+
rather than nothing; this one cannot be, because the subject did say it
|
|
289
|
+
and the wire lost it. So the issuer reads again instead.
|
|
290
|
+
|
|
291
|
+
**A reprice may only lower a severity, and that is not a courtesy.**
|
|
292
|
+
The receipt chain appends and does nothing else - seq, prev and every
|
|
293
|
+
counter are checked before a receipt is stored - so a late receipt is
|
|
294
|
+
the tail of a claim the subject had already committed to, and can only
|
|
295
|
+
move that claim toward what was measured. A second reading that came
|
|
296
|
+
out worse would therefore not be a later truth but a different one, and
|
|
297
|
+
a node that could be made to price harder by a late receipt would hand
|
|
298
|
+
every peer a way to worsen a verdict it had already earned. If the new
|
|
299
|
+
reading is not strictly better, the first one stands and nothing here
|
|
300
|
+
happens.
|
|
301
|
+
"""
|
|
302
|
+
now_val = now if now is not None else self.clock()
|
|
303
|
+
was = self.ledger.severity_of(peer, epoch)
|
|
304
|
+
if was is None:
|
|
305
|
+
return None
|
|
306
|
+
row = self.grants.get(peer, grants_mod.ISSUED)
|
|
307
|
+
if row is None or row['status'] != 'live':
|
|
308
|
+
return None
|
|
309
|
+
governed = self.governing(peer, grants_mod.ISSUED, epoch) or row
|
|
310
|
+
limits = governed['payload']['limits']
|
|
311
|
+
measured = self.usage_by.get(peer, {}).get(epoch, Meter()).snapshot()
|
|
312
|
+
receipts = self.receipts_in.get(peer, {}).get(epoch, [])
|
|
313
|
+
state, claimed = accounting.window_claim(receipts, measured)
|
|
314
|
+
if state == 'not-evaluated':
|
|
315
|
+
return None
|
|
316
|
+
severity = window.evaluate_window(limits, measured, claimed or {})['severity']
|
|
317
|
+
if state == 'active-silence':
|
|
318
|
+
severity = window.worse(severity, 'S2')
|
|
319
|
+
if severity == was or window.worse(severity, was) != was:
|
|
320
|
+
return None
|
|
321
|
+
self.ledger.amend(peer, epoch, severity)
|
|
322
|
+
if self.accounts is not None:
|
|
323
|
+
self.accounts.put_evaluated(peer, epoch, severity, self.ledger.tier.get(peer))
|
|
324
|
+
# The remedy is cancelled only where cancelling it is the whole answer:
|
|
325
|
+
# the window was clean and the grant that would have punished it has
|
|
326
|
+
# not been issued yet. A reading that is milder but still adverse
|
|
327
|
+
# leaves the stricter remedy standing rather than deriving a new one
|
|
328
|
+
# from a base that is already gone, and a remedy that has reached the
|
|
329
|
+
# wire is a signed artefact this must not quietly unwind. The record is
|
|
330
|
+
# corrected in every case; only the unissued grant is withdrawn, and
|
|
331
|
+
# the direction of the error is the strict one either way. A tier the
|
|
332
|
+
# ladder dropped stays dropped, because raising one is a person's
|
|
333
|
+
# decision and nothing here is a person.
|
|
334
|
+
pend = self.grants.pending(peer)
|
|
335
|
+
cancelled = False
|
|
336
|
+
if severity == 'S0' and pend is not None and pend.get('severity') == was:
|
|
337
|
+
self.grants.clear_pending(peer)
|
|
338
|
+
cancelled = True
|
|
339
|
+
rec = Repricing(peer=peer, epoch=epoch, was=was, severity=severity, state=state,
|
|
340
|
+
measured=measured, claimed=claimed, receipts=len(receipts),
|
|
341
|
+
remedy_cancelled=cancelled, at=now_val)
|
|
342
|
+
self.repriced.append(rec)
|
|
343
|
+
del self.repriced[:-KEEP_FINDINGS]
|
|
344
|
+
for cb in self.on_repriced:
|
|
345
|
+
cb(peer, epoch, was, severity)
|
|
346
|
+
return rec
|