Skip to main content

klukai_agent/agent/
setup.rs

1//! Setup main agent state
2
3// External crates
4use 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
23// Internals
24use 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
50/// Runtime state for the Corrosion agent
51pub 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
68/// Setup an agent runtime and state with a configuration
69pub 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    // do this early to error earlier
77    let members = Members::default();
78
79    let actor_id = {
80        // we need to set auto_vacuum before any tables are created
81        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    // Setup subscription handlers, this is before we start processing changes.
116    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    // RTT handling interacts with the tokio ReceiverStream and as
149    // such needs a raw tokio channel
150    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    // make an empty booked!
167    let booked = Booked::new(BookedVersions::new(actor_id), lock_registry.clone());
168
169    // asynchronously load it up!
170    tokio::spawn({
171        let pool = pool.clone();
172        // acquiring the lock here means everything will have to wait for it to be ready
173        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) // this is an ordered map, so taking the first few is gonna be the highest values
199                        .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
286/// Initialise subscription state and tasks
287///
288/// 1. Get subscriptions state directory from config
289/// 2. Load existing subscriptions and restore them in SubsManager
290/// 3. Spawn subscription processor task
291async 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}