Implement subscription tree branch pruning
Problem
The subscription tree for contract updates doesn't properly prune branch nodes when their downstream subscribers disconnect.
Current Behavior
When a peer receives an Unsubscribed message, it:
- Removes the peer from its
subscriberslist - Does nothing else
Expected Behavior
When a downstream subscriber unsubscribes, a branch peer should:
- Remove the peer from its downstream subscribers
- Check if it has any remaining downstream subscribers
- If no downstream subscribers AND not seeding locally (
is_seeding_contract), sendUnsubscribedto its upstream peer - This cascades up the tree, pruning unnecessary branches
Root Cause
The current SeedingManager::subscribers stores a flat Vec<PeerKeyLocation> (soon to be Vec<SocketAddr>) that doesn't distinguish between:
- Upstream: The peer we subscribed through (toward the root/contract location)
- Downstream: Peers that subscribed through us (leaves or other branches)
Suggested Data Model
```rust struct ContractSubscription { upstream: Option<SocketAddr>, // who we're subscribed to (toward root) downstream: Vec<SocketAddr>, // who subscribed through us is_local: bool, // local user subscribed (leaf node) } ```
Impact
Without branch pruning:
- Intermediate nodes stay subscribed longer than needed
- Updates are forwarded to branches with no leaves
- Network resources wasted on unnecessary update propagation
Context
- Discovered during #2164 Phase 4 planning (connection-based routing refactor)
- The routing refactor changes subscribers to
Vec<SocketAddr>but keeps the flat structure - This issue can be addressed independently after the routing refactor lands
Related
- Part of #2164 peer identity simplification effort
- Subscription tree design: https://freenet.org/news/summary-delta-sync/
[AI-assisted - Claude]
🤖 Auto-labeled
Applied labels:
-
T-bug(96% confidence) -
P-medium(90% confidence) -
E-medium(75% confidence) -
A-networking(90% confidence) -
A-contracts(80% confidence)
Reasoning: The report describes incorrect behavior where branch nodes are not pruned when downstream subscribers disconnect, causing unnecessary subscriptions and wasted network resources — this is a defect (T-bug). It is not immediately release-blocking but impacts resource usage and correctness across peers, so P-medium is appropriate. The fix requires changing the subscription data model and cascading Unsubscribed messages, involving moderate complexity (E-medium). The problem concerns peer routing/topology and subscription propagation, so A-networking applies; it also directly relates to contract subscription handling, so A-contracts is relevant.
Previous labels: none
If these labels are incorrect, please update them. This helps improve the auto-labeling system.
Subscription Tree Pruning - Implementation
Problem
When a peer unsubscribes from a contract, the current implementation only removes that peer from the local subscribers list. It doesn't check if the unsubscription should propagate upstream, causing "dead branches" - nodes that receive updates they no longer need to forward.
Gateway ──► Peer-A ──► Peer-B ──► ✗ (Peer-C gone)
│
(dead branch - wasted bandwidth)
Solution: Role-Based Subscription Tracking
Each node tracks its position in the subscription tree:
- Upstream: The peer we receive updates FROM
- Downstream: Peers that receive updates FROM us
- Has client: Whether a WebSocket client (app like River) is connected
Pruning Rule
When a downstream peer unsubscribes:
if downstream == [] AND has_client == false:
→ Send Unsubscribed to upstream
This cascades up the tree, cleaning the entire branch.
Example: Full Cascade
Initial:
Gateway ──► Peer-A ──► Peer-B ──► Peer-C (has client)
Peer-C's client disconnects:
1. Peer-C: downstream=[], has_client=false → Unsubscribed to Peer-B
2. Peer-B: downstream=[], has_client=false → Unsubscribed to Peer-A
3. Peer-A: downstream=[], has_client=false → Unsubscribed to Gateway
4. Gateway: downstream=[], has_client=true (source) → STOP
Final:
Gateway (still seeding, no forwarding)
Example: Partial Prune (Multiple Downstream)
Initial:
Peer-A
├──► Peer-B ──► Peer-C (has client)
└──► Peer-D (has client)
Peer-C disconnects:
1. Peer-B: downstream=[], has_client=false → Unsubscribed to Peer-A
2. Peer-A: downstream=[Peer-D], has_client=false → STOP (still has downstream)
Final:
Peer-A
└──► Peer-D (has client)
Example: Client Prevents Cascade
Initial:
Gateway ──► Peer-A ──► Peer-B (has client) ──► Peer-C (has client)
Peer-C disconnects:
1. Peer-B: downstream=[], has_client=true → STOP
Final:
Gateway ──► Peer-A ──► Peer-B (has client)
Peer-B keeps receiving updates for its local client.
Implementation
Data Model
enum SubscriptionRole {
Upstream, // We receive updates from this peer
Downstream, // This peer receives updates from us
}
struct SubscriptionEntry {
peer: PeerKeyLocation,
role: SubscriptionRole,
}
Subscriptions stored as: Map<ContractKey, Vec<SubscriptionEntry>>
Client subscriptions tracked separately: Map<ContractKey, Set<ClientId>>
Key Methods
| Method | Description |
|---|---|
set_upstream() |
Set the peer we receive updates from |
add_downstream() |
Add a peer that wants updates from us |
remove_subscriber() |
Remove peer, returns upstream to notify if pruning needed |
add_client_subscription() |
Register WebSocket client interest |
has_client_subscriptions() |
Check if any clients connected |
Upstream: The peer we receive updates FROM Downstream: Peers that receive updates FROM us
This might be pedantic, but updates can propagate both up and down a tree, see the visualization at the bottom of this page. Updates can start at a leaf and then propagate first up and then down the tree.
That's not to say there isn't a concept of up and down, generally a peer subscribed to a contract will itself try to subscribe to the contract via whichever connected peer is closest to the contract location, so the "upstream" peer will be that peer's closest neighbor to the contract.
There is an open question of whether a peer should resubscribe if it acquires a new connection closer to a contract than the one it's currently subscribed through but that's probably icing on the cake. There is also a question of whether a peer should ever accept a subscription request from a peer that is closer to it than the subscribed contract.