g1t/services/repos/src/coalesce.rs

125 lines4,656 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1//! Work done once for many askers.
2//!
3//! When a repository's default branch moves, work asks again whether each
4//! of its open pull requests (up to 100) still merges cleanly
5//! (`divergence`). Every one of those questions starts with the same
6//! answer about the target: its history from the new head, and what it
7//! changed since a pull request's merge base. That half is worked out once
8//! per target head and kept here, in the isolate, for the burst of
9//! questions that follows ([`Memo`]); the history itself is kept by commit
10//! hash in the object cache (store.rs), so other isolates read it without
11//! asking the store. Only each pull request's own side is read per pull
12//! request.
13
14use std::collections::{HashMap, HashSet};
15use std::hash::Hash;
16use std::rc::Rc;
17
18use g1t_contracts::repos::Commit;
19
20/// Answers kept for a short while, at most `cap` of them.
21pub struct Memo<K, V> {
22 entries: HashMap<K, (V, u64)>,
23 ttl_ms: u64,
24 cap: usize,
25}
26
27impl<K: Eq + Hash + Clone, V: Clone> Memo<K, V> {
28 pub fn new(ttl_ms: u64, cap: usize) -> Self {
29 Memo { entries: HashMap::new(), ttl_ms, cap }
30 }
31
32 pub fn get(&self, key: &K, now: u64) -> Option<V> {
33 self.entries
34 .get(key)
35 .filter(|(_, at)| now.saturating_sub(*at) < self.ttl_ms)
36 .map(|(value, _)| value.clone())
37 }
38
39 pub fn put(&mut self, key: K, value: V, now: u64) {
40 let ttl = self.ttl_ms;
41 self.entries.retain(|_, (_, at)| now.saturating_sub(*at) < ttl);
42 while self.entries.len() >= self.cap {
43 let Some(oldest) = self.entries.iter().min_by_key(|(_, (_, at))| *at).map(|(key, _)| key.clone()) else {
44 break;
45 };
46 self.entries.remove(&oldest);
47 }
48 self.entries.insert(key, (value, now));
49 }
50
51 #[cfg(test)]
52 pub fn len(&self) -> usize {
53 self.entries.len()
54 }
55}
56
57/// A target branch's side of the question: its history from its head
58/// (first parent, newest first), and the same as a set.
59pub struct TargetSide {
60 pub history: Vec<Commit>,
61 pub shared: HashSet<String>,
62}
63
64impl TargetSide {
65 pub fn new(history: Vec<Commit>) -> Rc<Self> {
66 let shared = history.iter().map(|commit| commit.hash.clone()).collect();
67 Rc::new(TargetSide { history, shared })
68 }
69}
70
71/// Where a target's side is kept: the repository, its default branch, and
72/// the version of its refs, which moves with the branch. Kept only while
73/// the version can be trusted (registry.rs, refs_cache.rs `usable`).
74pub type TargetKey = (String, String, u64);
75/// What the target changed between two trees: the repository and the trees.
76pub type TheirsKey = (String, String, String);
77
78/// How long a target's side is kept: long enough for one burst of
79/// questions after a push.
80pub const TARGET_TTL_MS: u64 = 60_000;
81/// What a target changed between two trees never changes; kept a while.
82pub const THEIRS_TTL_MS: u64 = 10 * 60_000;
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn an_answer_is_kept_for_a_while_and_the_oldest_goes_first() {
90 let mut memo: Memo<&str, u32> = Memo::new(1_000, 2);
91 memo.put("a", 1, 0);
92 assert_eq!(memo.get(&"a", 999), Some(1));
93 assert_eq!(memo.get(&"a", 1_000), None);
94 memo.put("b", 2, 10);
95 memo.put("c", 3, 20);
96 // Full: "a" was oldest.
97 assert_eq!(memo.get(&"a", 20), None);
98 assert_eq!(memo.get(&"b", 20), Some(2));
99 assert_eq!(memo.len(), 2);
100 // Expired ones go before anything is pushed out.
101 memo.put("d", 4, 1_015);
102 assert_eq!(memo.get(&"c", 1_015), Some(3));
103 assert_eq!(memo.get(&"d", 1_015), Some(4));
104 }
105
106 #[test]
107 fn a_target_side_is_keyed_by_its_refs_version() {
108 let commit = |hash: &str| Commit {
109 hash: hash.into(),
110 tree_hash: String::new(),
111 message: String::new(),
112 author: g1t_contracts::repos::Signature { name: String::new(), email: String::new() },
113 parents: Vec::new(),
114 authored_at: String::new(),
115 };
116 let side = TargetSide::new(vec![commit("b"), commit("a")]);
117 assert!(side.shared.contains("a") && side.shared.contains("b"));
118 let mut memo: Memo<TargetKey, Rc<TargetSide>> = Memo::new(TARGET_TTL_MS, 32);
119 let key = ("rep_1".to_owned(), "main".to_owned(), 7);
120 memo.put(key.clone(), side, 0);
121 assert!(memo.get(&key, 10).is_some());
122 // The branch moved: the version did too, and the old side is not used.
123 assert!(memo.get(&("rep_1".to_owned(), "main".to_owned(), 8), 10).is_none());
124 }
125}