Skip to content

Commit 30fc882

Browse files
protocols/gossipsub: Implement unsub backoff spec changes (#2403)
Implements the changes specified by libp2p/specs#383. Co-authored-by: Max Inden <[email protected]>
1 parent 96dbfcd commit 30fc882

File tree

4 files changed

+122
-5
lines changed

4 files changed

+122
-5
lines changed

protocols/gossipsub/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@
1313

1414
- Fix `GossipsubConfigBuilder::build()` requiring `&self` to live for `'static` (see [PR 2409])
1515

16+
- Implement Unsubscribe backoff as per [libp2p specs PR 383] (see [PR 2403]).
17+
1618
[PR 2346]: https://github.com/libp2p/rust-libp2p/pull/2346
1719
[PR 2339]: https://github.com/libp2p/rust-libp2p/pull/2339
1820
[PR 2327]: https://github.com/libp2p/rust-libp2p/pull/2327
1921
[PR 2408]: https://github.com/libp2p/rust-libp2p/pull/2408
2022
[PR 2409]: https://github.com/libp2p/rust-libp2p/pull/2409
23+
[PR 2403]: https://github.com/libp2p/rust-libp2p/pull/2403
24+
[libp2p specs PR 383]: https://github.com/libp2p/specs/pull/383
2125

2226
# 0.34.0 [2021-11-16]
2327

protocols/gossipsub/src/behaviour.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,7 @@ where
10491049
topic_hash: &TopicHash,
10501050
peer: &PeerId,
10511051
do_px: bool,
1052+
on_unsubscribe: bool,
10521053
) -> GossipsubControlAction {
10531054
if let Some((peer_score, ..)) = &mut self.peer_score {
10541055
peer_score.prune(peer, topic_hash.clone());
@@ -1088,14 +1089,19 @@ where
10881089
Vec::new()
10891090
};
10901091

1092+
let backoff = if on_unsubscribe {
1093+
self.config.unsubscribe_backoff()
1094+
} else {
1095+
self.config.prune_backoff()
1096+
};
1097+
10911098
// update backoff
1092-
self.backoffs
1093-
.update_backoff(topic_hash, peer, self.config.prune_backoff());
1099+
self.backoffs.update_backoff(topic_hash, peer, backoff);
10941100

10951101
GossipsubControlAction::Prune {
10961102
topic_hash: topic_hash.clone(),
10971103
peers,
1098-
backoff: Some(self.config.prune_backoff().as_secs()),
1104+
backoff: Some(backoff.as_secs()),
10991105
}
11001106
}
11011107

@@ -1111,7 +1117,9 @@ where
11111117
for peer in peers {
11121118
// Send a PRUNE control message
11131119
debug!("LEAVE: Sending PRUNE to peer: {:?}", peer);
1114-
let control = self.make_prune(topic_hash, &peer, self.config.do_px());
1120+
let on_unsubscribe = true;
1121+
let control =
1122+
self.make_prune(topic_hash, &peer, self.config.do_px(), on_unsubscribe);
11151123
Self::control_pool_add(&mut self.control_pool, peer, control);
11161124

11171125
// If the peer did not previously exist in any mesh, inform the handler
@@ -1487,9 +1495,10 @@ where
14871495

14881496
if !to_prune_topics.is_empty() {
14891497
// build the prune messages to send
1498+
let on_unsubscribe = false;
14901499
let prune_messages = to_prune_topics
14911500
.iter()
1492-
.map(|t| self.make_prune(t, peer_id, do_px))
1501+
.map(|t| self.make_prune(t, peer_id, do_px, on_unsubscribe))
14931502
.collect();
14941503
// Send the prune messages to the peer
14951504
debug!(
@@ -2598,6 +2607,9 @@ where
25982607
// NOTE: In this case a peer has been added to a topic mesh, and removed from another.
25992608
// It therefore must be in at least one mesh and we do not need to inform the handler
26002609
// of its removal from another.
2610+
2611+
// The following prunes are not due to unsubscribing.
2612+
let on_unsubscribe = false;
26012613
if let Some(topics) = to_prune.remove(&peer) {
26022614
let mut prunes = topics
26032615
.iter()
@@ -2606,6 +2618,7 @@ where
26062618
topic_hash,
26072619
&peer,
26082620
self.config.do_px() && !no_px.contains(&peer),
2621+
on_unsubscribe,
26092622
)
26102623
})
26112624
.collect::<Vec<_>>();
@@ -2630,13 +2643,16 @@ where
26302643
}
26312644

26322645
// handle the remaining prunes
2646+
// The following prunes are not due to unsubscribing.
2647+
let on_unsubscribe = false;
26332648
for (peer, topics) in to_prune.iter() {
26342649
let mut remaining_prunes = Vec::new();
26352650
for topic_hash in topics {
26362651
let prune = self.make_prune(
26372652
topic_hash,
26382653
peer,
26392654
self.config.do_px() && !no_px.contains(peer),
2655+
on_unsubscribe,
26402656
);
26412657
remaining_prunes.push(prune);
26422658
// inform the handler

protocols/gossipsub/src/behaviour/tests.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,77 @@ mod tests {
20372037
);
20382038
}
20392039

2040+
#[test]
2041+
fn test_unsubscribe_backoff() {
2042+
const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(100);
2043+
let config = GossipsubConfigBuilder::default()
2044+
.backoff_slack(1)
2045+
// ensure a prune_backoff > unsubscribe_backoff
2046+
.prune_backoff(Duration::from_secs(5))
2047+
.unsubscribe_backoff(1)
2048+
.heartbeat_interval(HEARTBEAT_INTERVAL)
2049+
.build()
2050+
.unwrap();
2051+
2052+
let topic = String::from("test");
2053+
// only one peer => mesh too small and will try to regraft as early as possible
2054+
let (mut gs, _, topics) = inject_nodes1()
2055+
.peer_no(1)
2056+
.topics(vec![topic.clone()])
2057+
.to_subscribe(true)
2058+
.gs_config(config)
2059+
.create_network();
2060+
2061+
let _ = gs.unsubscribe(&Topic::new(topic.clone()));
2062+
2063+
assert_eq!(
2064+
count_control_msgs(&gs, |_, m| match m {
2065+
GossipsubControlAction::Prune { backoff, .. } => backoff == &Some(1),
2066+
_ => false,
2067+
}),
2068+
1,
2069+
"Peer should be pruned with `unsubscribe_backoff`."
2070+
);
2071+
2072+
let _ = gs.subscribe(&Topic::new(topics[0].to_string()));
2073+
2074+
// forget all events until now
2075+
flush_events(&mut gs);
2076+
2077+
// call heartbeat
2078+
gs.heartbeat();
2079+
2080+
// Sleep for one second and apply 10 regular heartbeats (interval = 100ms).
2081+
for _ in 0..10 {
2082+
sleep(HEARTBEAT_INTERVAL);
2083+
gs.heartbeat();
2084+
}
2085+
2086+
// Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat
2087+
// is needed).
2088+
assert_eq!(
2089+
count_control_msgs(&gs, |_, m| match m {
2090+
GossipsubControlAction::Graft { .. } => true,
2091+
_ => false,
2092+
}),
2093+
0,
2094+
"Graft message created too early within backoff period"
2095+
);
2096+
2097+
// Heartbeat one more time this should graft now
2098+
sleep(HEARTBEAT_INTERVAL);
2099+
gs.heartbeat();
2100+
2101+
// check that graft got created
2102+
assert!(
2103+
count_control_msgs(&gs, |_, m| match m {
2104+
GossipsubControlAction::Graft { .. } => true,
2105+
_ => false,
2106+
}) > 0,
2107+
"No graft message was created after backoff period"
2108+
);
2109+
}
2110+
20402111
#[test]
20412112
fn test_flood_publish() {
20422113
let config: GossipsubConfig = GossipsubConfig::default();

protocols/gossipsub/src/config.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub struct GossipsubConfig {
7777
do_px: bool,
7878
prune_peers: usize,
7979
prune_backoff: Duration,
80+
unsubscribe_backoff: Duration,
8081
backoff_slack: u32,
8182
flood_publish: bool,
8283
graft_flood_threshold: Duration,
@@ -276,6 +277,15 @@ impl GossipsubConfig {
276277
self.prune_backoff
277278
}
278279

280+
/// Controls the backoff time when unsubscribing from a topic.
281+
///
282+
/// This is how long to wait before resubscribing to the topic. A short backoff period in case
283+
/// of an unsubscribe event allows reaching a healthy mesh in a more timely manner. The default
284+
/// is 10 seconds.
285+
pub fn unsubscribe_backoff(&self) -> Duration {
286+
self.unsubscribe_backoff
287+
}
288+
279289
/// Number of heartbeat slots considered as slack for backoffs. This gurantees that we wait
280290
/// at least backoff_slack heartbeats after a backoff is over before we try to graft. This
281291
/// solves problems occuring through high latencies. In particular if
@@ -421,6 +431,7 @@ impl Default for GossipsubConfigBuilder {
421431
do_px: false,
422432
prune_peers: 0, // NOTE: Increasing this currently has little effect until Signed records are implemented.
423433
prune_backoff: Duration::from_secs(60),
434+
unsubscribe_backoff: Duration::from_secs(10),
424435
backoff_slack: 1,
425436
flood_publish: true,
426437
graft_flood_threshold: Duration::from_secs(10),
@@ -636,6 +647,16 @@ impl GossipsubConfigBuilder {
636647
self
637648
}
638649

650+
/// Controls the backoff time when unsubscribing from a topic.
651+
///
652+
/// This is how long to wait before resubscribing to the topic. A short backoff period in case
653+
/// of an unsubscribe event allows reaching a healthy mesh in a more timely manner. The default
654+
/// is 10 seconds.
655+
pub fn unsubscribe_backoff(&mut self, unsubscribe_backoff: u64) -> &mut Self {
656+
self.config.unsubscribe_backoff = Duration::from_secs(unsubscribe_backoff);
657+
self
658+
}
659+
639660
/// Number of heartbeat slots considered as slack for backoffs. This gurantees that we wait
640661
/// at least backoff_slack heartbeats after a backoff is over before we try to graft. This
641662
/// solves problems occuring through high latencies. In particular if
@@ -777,6 +798,11 @@ impl GossipsubConfigBuilder {
777798
"The following inequality doesn't hold mesh_outbound_min <= self.config.mesh_n / 2",
778799
);
779800
}
801+
802+
if self.config.unsubscribe_backoff.as_millis() == 0 {
803+
return Err("The unsubscribe_backoff parameter should be positive.");
804+
}
805+
780806
Ok(self.config.clone())
781807
}
782808
}

0 commit comments

Comments
 (0)