1use antithesis_sdk::assert_always_or_unreachable;
5use arc_swap::ArcSwap;
6use camino::Utf8PathBuf;
7use indexmap::IndexMap;
8use klukai_types::tripwire::Tripwire;
9use metrics::counter;
10use parking_lot::RwLock;
11use rusqlite::{Connection, OptionalExtension};
12use serde_json::json;
13use std::{net::SocketAddr, ops::DerefMut, sync::Arc, time::Duration};
14use tokio::{
15 net::TcpListener,
16 sync::{
17 RwLock as TokioRwLock, Semaphore,
18 mpsc::{Receiver as TokioReceiver, channel as tokio_channel},
19 },
20};
21use tracing::{debug, error, info, trace, warn};
22
23use crate::{
25 api::{
26 peer::gossip_server_endpoint,
27 public::{
28 pubsub::{MatcherBroadcastCache, SharedMatcherBroadcastCache, process_sub_channel},
29 update::SharedUpdateBroadcastCache,
30 },
31 },
32 transport::Transport,
33};
34use klukai_types::{
35 actor::ActorId,
36 agent::{
37 Agent, AgentConfig, Booked, BookedVersions, LockRegistry, LockState, SplitPool, migrate,
38 },
39 base::{CrsqlDbVersion, CrsqlDbVersionRange},
40 broadcast::{BroadcastInput, ChangeSource, ChangeV1, FocaInput},
41 channel::{CorroReceiver, bounded},
42 config::Config,
43 members::Members,
44 pubsub::{Matcher, SubsManager},
45 schema::{Schema, init_schema},
46 sqlite::CrConn,
47 updates::UpdatesManager,
48};
49
50pub struct AgentOptions {
52 pub lock_registry: LockRegistry,
53 pub gossip_server_endpoint: quinn::Endpoint,
54 pub transport: Transport,
55 pub api_listeners: Vec<TcpListener>,
56 pub rx_bcast: CorroReceiver<BroadcastInput>,
57 pub rx_apply: CorroReceiver<(ActorId, CrsqlDbVersion)>,
58 pub rx_clear_buf: CorroReceiver<(ActorId, CrsqlDbVersionRange)>,
59 pub rx_changes: CorroReceiver<(ChangeV1, ChangeSource)>,
60 pub rx_foca: CorroReceiver<FocaInput>,
61 pub rtt_rx: TokioReceiver<(SocketAddr, Duration)>,
62 pub subs_manager: SubsManager,
63 pub subs_bcast_cache: SharedMatcherBroadcastCache,
64 pub updates_bcast_cache: SharedUpdateBroadcastCache,
65 pub tripwire: Tripwire,
66}
67
68pub async fn setup(conf: Config, tripwire: Tripwire) -> eyre::Result<(Agent, AgentOptions)> {
70 debug!("setting up corrosion @ {}", conf.db.path);
71
72 if let Some(parent) = conf.db.path.parent() {
73 tokio::fs::create_dir_all(parent).await?;
74 }
75
76 let members = Members::default();
78
79 let actor_id = {
80 let db_conn = Connection::open(&conf.db.path)?;
82 db_conn.execute_batch("PRAGMA auto_vacuum = INCREMENTAL")?;
83
84 let conn = CrConn::init(db_conn)?;
85 conn.query_row("SELECT crsql_site_id();", [], |row| {
86 row.get::<_, ActorId>(0)
87 })
88 }?;
89
90 info!("Actor ID: {actor_id}");
91
92 let write_sema = Arc::new(Semaphore::new(1));
93
94 let pool = SplitPool::create(&conf.db.path, write_sema.clone()).await?;
95
96 let clock = Arc::new(
97 uhlc::HLCBuilder::default()
98 .with_id(actor_id.try_into().unwrap())
99 .with_max_delta(Duration::from_millis(300))
100 .build(),
101 );
102
103 let schema = {
104 let mut conn = pool.write_priority().await?;
105 migrate(clock.clone(), &mut conn)?;
106 let mut schema = init_schema(&conn)?;
107 schema.constrain()?;
108
109 schema
110 };
111
112 let subs_manager = SubsManager::default();
113
114 let updates_manager = UpdatesManager::default();
115 let subs_bcast_cache = setup_spawn_subscriptions(
117 &subs_manager,
118 conf.db.subscriptions_path(),
119 &pool,
120 &schema,
121 &tripwire,
122 )
123 .await?;
124
125 let updates_bcast_cache = SharedUpdateBroadcastCache::default();
126
127 let cluster_id = {
128 let conn = pool.read().await?;
129 conn.query_row(
130 "SELECT value FROM __corro_state WHERE key = 'cluster_id'",
131 [],
132 |row| row.get(0),
133 )
134 .optional()?
135 .unwrap_or_default()
136 };
137
138 info!("Cluster ID: {cluster_id}");
139
140 let (tx_apply, rx_apply) = bounded(conf.perf.apply_channel_len, "apply");
141 let (tx_clear_buf, rx_clear_buf) = bounded(conf.perf.clearbuf_channel_len, "clear_buf");
142
143 let gossip_server_endpoint = gossip_server_endpoint(&conf.gossip).await?;
144 let gossip_addr = gossip_server_endpoint.local_addr()?;
145
146 let external_addr = conf.gossip.external_addr;
147
148 let (rtt_tx, rtt_rx) = tokio_channel(128);
151
152 let transport = Transport::new(&conf.gossip, rtt_tx).await?;
153
154 let mut api_listeners = Vec::with_capacity(conf.api.bind_addr.len());
155 for addr in conf.api.bind_addr.iter() {
156 api_listeners.push(TcpListener::bind(addr).await?);
157 }
158 let api_addr = api_listeners.first().unwrap().local_addr()?;
159
160 let (tx_bcast, rx_bcast) = bounded(conf.perf.bcast_channel_len, "bcast");
161 let (tx_changes, rx_changes) = bounded(conf.perf.changes_channel_len, "changes");
162 let (tx_foca, rx_foca) = bounded(conf.perf.foca_channel_len, "foca");
163
164 let lock_registry = LockRegistry::default();
165
166 let booked = Booked::new(BookedVersions::new(actor_id), lock_registry.clone());
168
169 tokio::spawn({
171 let pool = pool.clone();
172 let mut booked = booked.write_owned::<&str, _>("init", None).await;
174 async move {
175 let conn = pool.read().await?;
176 *booked.deref_mut().deref_mut() =
177 tokio::task::block_in_place(|| BookedVersions::from_conn(&conn, actor_id))
178 .expect("loading BookedVersions from db failed");
179 Ok::<_, eyre::Report>(())
180 }
181 });
182
183 tokio::spawn({
184 let registry = lock_registry.clone();
185 async move {
186 const WARNING_THRESHOLD: Duration = Duration::from_secs(10);
187 let mut interval = tokio::time::interval(Duration::from_secs(60));
188
189 loop {
190 interval.tick().await;
191 trace!("inspecting the lock registry...");
192
193 let top: IndexMap<_, _> = {
194 registry
195 .map
196 .read()
197 .iter()
198 .take(10) .map(|(k, v)| (*k, v.clone()))
200 .collect()
201 };
202
203 if top.values().any(|meta| {
204 let duration = meta.started_at.elapsed();
205 duration >= WARNING_THRESHOLD
206 }) {
207 warn!(
208 "lock registry shows locks held for a long time! top {} locks:",
209 top.len()
210 );
211
212 for (id, lock) in top {
213 let duration = lock.started_at.elapsed();
214 warn!(
215 "{} (id: {id}, type: {:?}, state: {:?}) locked for: {duration:?}",
216 lock.label, lock.kind, lock.state
217 );
218
219 if matches!(lock.state, LockState::Locked) {
220 let details = json!({
221 "duration": duration,
222 "label": lock.label,
223 "kind": lock.kind,
224 "state": lock.state,
225 });
226 assert_always_or_unreachable!(
227 duration < Duration::from_secs(60),
228 "bookie lock held for too long",
229 &details
230 );
231 }
232
233 if duration >= WARNING_THRESHOLD {
234 counter!("corro.agent.lock.slow.count", "name" => lock.label)
235 .increment(1);
236 }
237 }
238 }
239 }
240 }
241 });
242
243 let opts = AgentOptions {
244 gossip_server_endpoint,
245 transport: transport.clone(),
246 api_listeners,
247 lock_registry,
248 rx_bcast,
249 rx_apply,
250 rx_clear_buf,
251 rx_changes,
252 rx_foca,
253 rtt_rx,
254 subs_manager: subs_manager.clone(),
255 subs_bcast_cache,
256 updates_bcast_cache,
257 tripwire: tripwire.clone(),
258 };
259
260 let agent = Agent::new(AgentConfig {
261 actor_id,
262 pool,
263 gossip_addr,
264 external_addr,
265 api_addr,
266 members: RwLock::new(members),
267 config: ArcSwap::from_pointee(conf),
268 clock,
269 booked,
270 tx_bcast,
271 tx_apply,
272 tx_clear_buf,
273 tx_changes,
274 tx_foca,
275 write_sema,
276 schema: RwLock::new(schema),
277 cluster_id,
278 subs_manager,
279 updates_manager,
280 tripwire,
281 });
282
283 Ok((agent, opts))
284}
285
286async fn setup_spawn_subscriptions(
292 subs_manager: &SubsManager,
293 subs_path: Utf8PathBuf,
294 pool: &SplitPool,
295 schema: &Schema,
296 tripwire: &Tripwire,
297) -> eyre::Result<SharedMatcherBroadcastCache> {
298 let mut subs_bcast_cache = MatcherBroadcastCache::default();
299 let mut to_cleanup = vec![];
300
301 if let Ok(mut dir) = tokio::fs::read_dir(&subs_path).await {
302 while let Ok(Some(entry)) = dir.next_entry().await {
303 let path_str = entry.path().display().to_string();
304 if let Some(sub_id_str) = path_str.strip_prefix(subs_path.as_str())
305 && let Ok(sub_id) = sub_id_str.trim_matches('/').parse()
306 {
307 let (_, created) = match subs_manager.restore(
308 sub_id,
309 &subs_path,
310 schema,
311 pool,
312 tripwire.clone(),
313 ) {
314 Ok(res) => res,
315 Err(e) => {
316 error!(%sub_id, "could not restore subscription: {e}");
317 to_cleanup.push(sub_id);
318 continue;
319 }
320 };
321
322 info!(%sub_id, "Restored subscription");
323
324 let (sub_tx, _) = tokio::sync::broadcast::channel(10240);
325
326 tokio::spawn(process_sub_channel(
327 subs_manager.clone(),
328 sub_id,
329 sub_tx.clone(),
330 created.evt_rx,
331 ));
332
333 subs_bcast_cache.insert(sub_id, sub_tx);
334 }
335 }
336 }
337
338 for id in to_cleanup {
339 info!(sub_id = %id, "Cleaning up unclean subscription");
340 Matcher::cleanup(id, Matcher::sub_path(subs_path.as_path(), id))?;
341 }
342
343 Ok(Arc::new(TokioRwLock::new(subs_bcast_cache)))
344}