Skip to main content

gossip_relay_picker/
lib.rs

1//! The main type here is `RelayPicker`. You will need to implement `RelayPickerHooks`
2//! and then create a `RelayPicker::new(hooks)` with those hooks.
3//!
4//! If you instantiate `RelayPicker` via `Default::default()`, for example in a lazy_static type
5//! setup, and then make changes which cause the Hooks to return something different than they
6//! did when they were created with `Default::default()` (e.g. global variable changes), then you
7//! might need to run `RelayPicker::init()` to reinitialize it with actual data.
8//!
9//!
10//!
11//!
12
13use async_trait::async_trait;
14use dashmap::DashMap;
15pub use nostr_types::{PublicKeyHex, RelayUrl, Unixtime};
16use thiserror::Error;
17
18/// This is how a person uses a relay: to write (outbox) or to read (inbox)
19#[derive(Debug, Copy, Clone)]
20pub enum Direction {
21    Read,
22    Write,
23}
24
25/// Errors the RelayPicker functions can return
26#[derive(Error, Debug, Clone, PartialEq, Eq)]
27pub enum Error {
28    /// No relays to pick from
29    #[error("No relays to pick from")]
30    NoRelays,
31
32    /// No people left to assign. A good result.
33    #[error("All people accounted for.")]
34    NoPeopleLeft,
35
36    /// No progress was made. A stuck result.
37    #[error("Unable to make further progress.")]
38    NoProgress,
39
40    /// General error
41    #[error("Error: {0}")]
42    General(String),
43}
44
45/// A RelayAssignment is a record of a relay which is serving (or will serve) the general
46/// feed for a set of public keys.
47#[derive(Debug, Clone)]
48pub struct RelayAssignment {
49    /// The URL of the relay
50    pub relay_url: RelayUrl,
51
52    /// The public keys assigned to the relay
53    pub pubkeys: Vec<PublicKeyHex>,
54}
55
56impl RelayAssignment {
57    pub fn merge_in(&mut self, other: RelayAssignment) -> Result<(), Error> {
58        if self.relay_url != other.relay_url {
59            return Err(Error::General(
60                "Attempted to merge relay assignments on different relays".to_owned(),
61            ));
62        }
63        self.pubkeys.extend(other.pubkeys);
64        Ok(())
65    }
66}
67
68/// These are functions that need to be provided to the Relay Picker
69#[async_trait]
70pub trait RelayPickerHooks: Send + Sync {
71    type Error: std::fmt::Display;
72
73    /// Returns all relays available to be connected to
74    fn get_all_relays(&self) -> Vec<RelayUrl>;
75
76    /// Returns the best relays that this public key uses in the given Direction,
77    /// in order of score from highest to lowest, along with the score.
78    ///
79    /// This is async and returns `Result<Vec<(RelayUrl, u64)>, Self::Error>`
80    /// Documentation may show the raw type
81    async fn get_relays_for_pubkey(
82        &self,
83        pubkey: PublicKeyHex,
84        direction: Direction,
85    ) -> Result<Vec<(RelayUrl, u64)>, Self::Error>;
86
87    /// Is the relay currently connected?
88    fn is_relay_connected(&self, relay: &RelayUrl) -> bool;
89
90    /// Returns the maximum number of relays that should be connected to at one time
91    fn get_max_relays(&self) -> usize;
92
93    /// Returns the number of relays each followed person's events should be pulled from
94    /// Many people use 2 or 3 for redundancy.
95    fn get_num_relays_per_person(&self) -> usize;
96
97    /// Returns the public keys of all the people followed
98    fn get_followed_pubkeys(&self) -> Vec<PublicKeyHex>;
99
100    /// Adjusts the score for a given relay, perhaps based on relay-specific metrics
101    fn adjust_score(&self, relay: RelayUrl, score: u64) -> u64;
102}
103
104/// The RelayPicker is a structure that helps assign people we follow to relays we watch.
105/// It remembers which publickeys are assigned to which relays, which pubkeys need more
106/// relays and how many, which relays need a time out, and person-relay scores for making
107/// good assignments dynamically.
108#[derive(Debug, Default)]
109pub struct RelayPicker<H: RelayPickerHooks + Default> {
110    /// Hooks you provide to the Relay Picker
111    hooks: H,
112
113    /// A ranking of relays per person.
114    person_relay_scores: DashMap<PublicKeyHex, Vec<(RelayUrl, u64)>>,
115
116    /// All of the relays currently connected, with optional assignments.
117    /// (Sometimes a relay is connected for a different kind of subscription.)
118    relay_assignments: DashMap<RelayUrl, RelayAssignment>,
119
120    /// Relays which recently failed and which require a timeout before
121    /// they can be chosen again.  The value is the time when it can be removed
122    /// from this list.
123    excluded_relays: DashMap<RelayUrl, i64>,
124
125    /// For each followed pubkey that still needs assignments, the number of relay
126    /// assignments it is seeking.  These start out at settings.num_relays_per_person
127    /// (if the person doesn't have that many relays, it will do the best it can)
128    pubkey_counts: DashMap<PublicKeyHex, usize>,
129}
130
131impl<H: RelayPickerHooks + Default> RelayPicker<H> {
132    /// Create a new Relay Picker
133    pub async fn new(hooks: H) -> Result<RelayPicker<H>, Error> {
134        let rp = RelayPicker {
135            hooks,
136            ..Default::default()
137        };
138
139        rp.refresh_person_relay_scores_inner(true).await?;
140
141        Ok(rp)
142    }
143
144    /// Re-initialize an existing Relay Picker
145    /// This is useful if you created the RelayPicker from Default (e.g. in lazy static)
146    pub async fn init(&self) -> Result<(), Error> {
147        self.relay_assignments.clear();
148        self.excluded_relays.clear();
149        self.pubkey_counts.clear();
150        self.person_relay_scores.clear();
151
152        self.refresh_person_relay_scores_inner(true).await?;
153
154        Ok(())
155    }
156
157    /// Add a public key
158    pub fn add_someone(&self, pubkey: PublicKeyHex) -> Result<(), Error> {
159        // Check if we already have them
160        if self.pubkey_counts.get(&pubkey).is_some() {
161            return Ok(());
162        }
163        for elem in self.relay_assignments.iter() {
164            let assignment = elem.value();
165            if assignment.pubkeys.contains(&pubkey) {
166                return Ok(());
167            }
168        }
169
170        self.pubkey_counts
171            .insert(pubkey, self.hooks.get_num_relays_per_person());
172        Ok(())
173    }
174
175    /// Remove a public key
176    pub fn remove_someone(&self, pubkey: PublicKeyHex) {
177        // Remove from pubkey counts
178        self.pubkey_counts.remove(&pubkey);
179
180        // Remove from relay assignments
181        for mut elem in self.relay_assignments.iter_mut() {
182            let assignment = elem.value_mut();
183            if let Some(pos) = assignment.pubkeys.iter().position(|x| x == &pubkey) {
184                assignment.pubkeys.remove(pos);
185            }
186        }
187    }
188
189    /// Refresh the person relay scores from the hook function
190    pub async fn refresh_person_relay_scores(&self) -> Result<(), Error> {
191        self.refresh_person_relay_scores_inner(false).await
192    }
193
194    // Refresh person relay scores.
195    async fn refresh_person_relay_scores_inner(
196        &self,
197        initialize_counts: bool,
198    ) -> Result<(), Error> {
199        self.person_relay_scores.clear();
200
201        if initialize_counts {
202            self.pubkey_counts.clear();
203        }
204
205        // Get all the people we follow
206        let pubkeys: Vec<PublicKeyHex> = self
207            .hooks
208            .get_followed_pubkeys()
209            .iter()
210            .map(|p| p.to_owned())
211            .collect();
212
213        // Compute scores for each person_relay pairing
214        for pubkey in &pubkeys {
215            let best_relays: Vec<(RelayUrl, u64)> = self
216                .hooks
217                .get_relays_for_pubkey(pubkey.to_owned(), Direction::Write)
218                .await
219                .map_err(|e| Error::General(format!("{e}")))?;
220            self.person_relay_scores.insert(pubkey.clone(), best_relays);
221
222            if initialize_counts {
223                self.pubkey_counts
224                    .insert(pubkey.clone(), self.hooks.get_num_relays_per_person());
225            }
226        }
227
228        Ok(())
229    }
230
231    /// When a relay disconnects, call this so that whatever assignments it might have
232    /// had can be reassigned.  Then call `pick_relays()` again.
233    pub fn relay_disconnected(&self, url: &RelayUrl) {
234        // Remove from connected relays list
235        if let Some((_key, assignment)) = self.relay_assignments.remove(url) {
236            // Exclude the relay for the next 30 seconds
237            let hence = Unixtime::now().unwrap().0 + 30;
238            self.excluded_relays.insert(url.to_owned(), hence);
239            tracing::debug!("{} goes into the penalty box until {}", url, hence,);
240
241            // Put the public keys back into pubkey_counts
242            for pubkey in assignment.pubkeys.iter() {
243                self.pubkey_counts
244                    .entry(pubkey.to_owned())
245                    .and_modify(|e| *e += 1)
246                    .or_insert(1);
247            }
248        }
249    }
250
251    /// Create the next assignment, and return the `RelayUrl` that has it.
252    /// You should probably immediately call `get_relay_assignment()` with that `RelayUrl`
253    /// to get the newly created assignment. The caller is responsible for making that
254    /// assignment actually happen.
255    pub async fn pick(&self) -> Result<RelayUrl, Error> {
256        // If we are at max relays, only consider relays we are already
257        // connected to
258        let at_max_relays = self.relay_assignments.len() >= self.hooks.get_max_relays();
259
260        // Maybe include excluded relays
261        let now = Unixtime::now().unwrap().0;
262        self.excluded_relays.retain(|_, v| *v > now);
263
264        if self.pubkey_counts.is_empty() {
265            return Err(Error::NoPeopleLeft);
266        }
267
268        let all_relays = self.hooks.get_all_relays();
269
270        if all_relays.is_empty() {
271            return Err(Error::NoRelays);
272        }
273
274        // Keep score for each relay
275        let scoreboard: DashMap<RelayUrl, u64> =
276            all_relays.iter().map(|x| (x.to_owned(), 0)).collect();
277
278        // Assign scores to relays from each pubkey
279        for elem in self.person_relay_scores.iter() {
280            let pubkeyhex = elem.key();
281            let relay_scores = elem.value();
282
283            // Skip if this pubkey doesn't need any more assignments
284            if let Some(pkc) = self.pubkey_counts.get(pubkeyhex) {
285                if *pkc == 0 {
286                    // person doesn't need anymore
287                    continue;
288                }
289            } else {
290                continue; // person doesn't need any
291            }
292
293            // Add scores of their relays
294            for (relay, score) in relay_scores.iter() {
295                // Skip relays that are excluded
296                if self.excluded_relays.contains_key(relay) {
297                    continue;
298                }
299
300                // If at max, skip relays not already connected
301                if at_max_relays && !self.hooks.is_relay_connected(relay) {
302                    continue;
303                }
304
305                // Skip if relay is already assigned this pubkey
306                if let Some(assignment) = self.relay_assignments.get(relay) {
307                    if assignment.pubkeys.contains(pubkeyhex) {
308                        continue;
309                    }
310                }
311
312                // Add the score
313                if let Some(mut entry) = scoreboard.get_mut(relay) {
314                    *entry += score;
315                }
316            }
317        }
318
319        // Adjust all scores based on relay rank and relay success rate
320        // (gossip code elided due to complex data tracking required)
321        // TBD to add this kind of feature back.
322        for mut score_entry in scoreboard.iter_mut() {
323            let url = score_entry.key().to_owned();
324            let score = score_entry.value_mut();
325            *score = self.hooks.adjust_score(url, *score);
326        }
327
328        let winner = scoreboard
329            .iter()
330            .max_by(|x, y| x.value().cmp(y.value()))
331            .unwrap();
332        let winning_url: RelayUrl = winner.key().to_owned();
333        let winning_score: u64 = *winner.value();
334
335        if winning_score == 0 {
336            return Err(Error::NoProgress);
337        }
338
339        // Now sort out which public keys go with that relay (we did this already
340        // above when assigning scores, but in a way which would require a lot of
341        // storage to keep, so we just do it again)
342        let covered_public_keys = {
343            let pubkeys_seeking_relays: Vec<PublicKeyHex> = self
344                .pubkey_counts
345                .iter()
346                .filter(|e| *e.value() > 0)
347                .map(|e| e.key().to_owned())
348                .collect();
349
350            let mut covered_pubkeys: Vec<PublicKeyHex> = Vec::new();
351
352            for pubkey in pubkeys_seeking_relays.iter() {
353                // Skip if relay is already assigned this pubkey
354                if let Some(assignment) = self.relay_assignments.get(&winning_url) {
355                    if assignment.pubkeys.contains(pubkey) {
356                        continue;
357                    }
358                }
359
360                if let Some(elem) = self.person_relay_scores.get(pubkey) {
361                    let relay_scores = elem.value();
362
363                    if relay_scores.iter().any(|e| e.0 == winning_url) {
364                        covered_pubkeys.push(pubkey.to_owned());
365
366                        if let Some(mut count) = self.pubkey_counts.get_mut(pubkey) {
367                            if *count > 0 {
368                                *count -= 1;
369                            }
370                        }
371                    }
372                }
373            }
374
375            covered_pubkeys
376        };
377
378        if covered_public_keys.is_empty() {
379            return Err(Error::NoProgress);
380        }
381
382        // Only keep pubkey_counts that are still > 0
383        self.pubkey_counts.retain(|_, count| *count > 0);
384
385        let assignment = RelayAssignment {
386            relay_url: winning_url.clone(),
387            pubkeys: covered_public_keys,
388        };
389
390        // Put assignment into relay_assignments
391        if let Some(mut maybe_elem) = self.relay_assignments.get_mut(&winning_url) {
392            // FIXME this could cause a panic, but it would mean we have bad code.
393            maybe_elem.value_mut().merge_in(assignment).unwrap();
394        } else {
395            self.relay_assignments
396                .insert(winning_url.clone(), assignment);
397        }
398
399        Ok(winning_url)
400    }
401
402    /// Get the `RelayAssignment` for a given `RelayUrl`
403    pub fn get_relay_assignment(&self, relay_url: &RelayUrl) -> Option<RelayAssignment> {
404        self.relay_assignments
405            .get(relay_url)
406            .map(|elem| elem.value().to_owned())
407    }
408
409    /// Iterate over all `RelayAssignment`s
410    pub fn relay_assignments_iter(&self) -> dashmap::iter::Iter<'_, RelayUrl, RelayAssignment> {
411        self.relay_assignments.iter()
412    }
413
414    /// Get an iterator over all `RelayUrl`s that are excluded, and the `Unixtime` when they
415    /// will be candidates again
416    pub fn excluded_relays_iter(&self) -> dashmap::iter::Iter<'_, RelayUrl, i64> {
417        self.excluded_relays.iter()
418    }
419
420    /// Get an iterator over all the `PublicKeyHex`s that are not fully assigned, as well as
421    /// the number of relays they still need.
422    pub fn pubkey_counts_iter(&self) -> dashmap::iter::Iter<'_, PublicKeyHex, usize> {
423        self.pubkey_counts.iter()
424    }
425}