LCOV - code coverage report
Current view: top level - storage_controller/src/service - safekeeper_reconciler.rs (source / functions) Coverage Total Hit
Test: aca806cab4756d7eb6a304846130f4a73a5d5393.info Lines: 0.0 % 326 0
Test Date: 2025-04-24 20:31:15 Functions: 0.0 % 49 0

            Line data    Source code
       1              : use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
       2              : 
       3              : use clashmap::{ClashMap, Entry};
       4              : use safekeeper_api::models::PullTimelineRequest;
       5              : use safekeeper_client::mgmt_api;
       6              : use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
       7              : use tokio_util::sync::CancellationToken;
       8              : use tracing::Instrument;
       9              : use utils::{
      10              :     id::{NodeId, TenantId, TimelineId},
      11              :     logging::SecretString,
      12              : };
      13              : 
      14              : use crate::{
      15              :     persistence::SafekeeperTimelineOpKind, safekeeper::Safekeeper,
      16              :     safekeeper_client::SafekeeperClient,
      17              : };
      18              : 
      19              : use super::Service;
      20              : 
      21              : pub(crate) struct SafekeeperReconcilers {
      22              :     cancel: CancellationToken,
      23              :     reconcilers: HashMap<NodeId, ReconcilerHandle>,
      24              : }
      25              : 
      26              : impl SafekeeperReconcilers {
      27            0 :     pub fn new(cancel: CancellationToken) -> Self {
      28            0 :         SafekeeperReconcilers {
      29            0 :             cancel,
      30            0 :             reconcilers: HashMap::new(),
      31            0 :         }
      32            0 :     }
      33              :     /// Adds a safekeeper-specific reconciler.
      34              :     /// Can be called multiple times, but it needs to be called at least once
      35              :     /// for every new safekeeper added.
      36            0 :     pub(crate) fn start_reconciler(&mut self, node_id: NodeId, service: &Arc<Service>) {
      37            0 :         self.reconcilers.entry(node_id).or_insert_with(|| {
      38            0 :             SafekeeperReconciler::spawn(self.cancel.child_token(), service.clone())
      39            0 :         });
      40            0 :     }
      41              :     /// Stop a safekeeper-specific reconciler.
      42              :     /// Stops the reconciler, cancelling all ongoing tasks.
      43            0 :     pub(crate) fn stop_reconciler(&mut self, node_id: NodeId) {
      44            0 :         if let Some(handle) = self.reconcilers.remove(&node_id) {
      45            0 :             handle.cancel.cancel();
      46            0 :         }
      47            0 :     }
      48            0 :     pub(crate) fn schedule_request_vec(&self, reqs: Vec<ScheduleRequest>) {
      49            0 :         tracing::info!(
      50            0 :             "Scheduling {} pending safekeeper ops loaded from db",
      51            0 :             reqs.len()
      52              :         );
      53            0 :         for req in reqs {
      54            0 :             self.schedule_request(req);
      55            0 :         }
      56            0 :     }
      57            0 :     pub(crate) fn schedule_request(&self, req: ScheduleRequest) {
      58            0 :         let node_id = req.safekeeper.get_id();
      59            0 :         let reconciler_handle = self.reconcilers.get(&node_id).unwrap();
      60            0 :         reconciler_handle.schedule_reconcile(req);
      61            0 :     }
      62              :     /// Cancel ongoing reconciles for the given timeline
      63              :     ///
      64              :     /// Specifying `None` here only removes reconciles for the tenant-global reconciliation,
      65              :     /// instead of doing this for all timelines of the tenant.
      66              :     ///
      67              :     /// Callers must remove the reconciles from the db manually
      68            0 :     pub(crate) fn cancel_reconciles_for_timeline(
      69            0 :         &mut self,
      70            0 :         node_id: NodeId,
      71            0 :         tenant_id: TenantId,
      72            0 :         timeline_id: Option<TimelineId>,
      73            0 :     ) {
      74            0 :         if let Some(handle) = self.reconcilers.get(&node_id) {
      75            0 :             handle.cancel_reconciliation(tenant_id, timeline_id);
      76            0 :         }
      77            0 :     }
      78              : }
      79              : 
      80              : /// Initial load of the pending operations from the db
      81            0 : pub(crate) async fn load_schedule_requests(
      82            0 :     service: &Arc<Service>,
      83            0 :     safekeepers: &HashMap<NodeId, Safekeeper>,
      84            0 : ) -> anyhow::Result<Vec<ScheduleRequest>> {
      85            0 :     let pending_ops_timelines = service
      86            0 :         .persistence
      87            0 :         .list_pending_ops_with_timelines()
      88            0 :         .await?;
      89            0 :     let mut res = Vec::with_capacity(pending_ops_timelines.len());
      90            0 :     for (op_persist, timeline_persist) in pending_ops_timelines {
      91            0 :         let node_id = NodeId(op_persist.sk_id as u64);
      92            0 :         let Some(sk) = safekeepers.get(&node_id) else {
      93              :             // This shouldn't happen, at least the safekeeper should exist as decomissioned.
      94            0 :             tracing::warn!(
      95              :                 tenant_id = op_persist.tenant_id,
      96              :                 timeline_id = op_persist.timeline_id,
      97            0 :                 "couldn't find safekeeper with pending op id {node_id} in list of stored safekeepers"
      98              :             );
      99            0 :             continue;
     100              :         };
     101            0 :         let sk = Box::new(sk.clone());
     102            0 :         let tenant_id = TenantId::from_str(&op_persist.tenant_id)?;
     103            0 :         let timeline_id = if !op_persist.timeline_id.is_empty() {
     104            0 :             Some(TimelineId::from_str(&op_persist.timeline_id)?)
     105              :         } else {
     106            0 :             None
     107              :         };
     108            0 :         let host_list = match op_persist.op_kind {
     109            0 :             SafekeeperTimelineOpKind::Delete => Vec::new(),
     110            0 :             SafekeeperTimelineOpKind::Exclude => Vec::new(),
     111              :             SafekeeperTimelineOpKind::Pull => {
     112            0 :                 if timeline_id.is_none() {
     113              :                     // We only do this extra check (outside of timeline_persist check) to give better error msgs
     114            0 :                     anyhow::bail!(
     115            0 :                         "timeline_id is empty for `pull` schedule request for {tenant_id}"
     116            0 :                     );
     117            0 :                 };
     118            0 :                 let Some(timeline_persist) = timeline_persist else {
     119              :                     // This shouldn't happen, the timeline should still exist
     120            0 :                     tracing::warn!(
     121              :                         tenant_id = op_persist.tenant_id,
     122              :                         timeline_id = op_persist.timeline_id,
     123            0 :                         "couldn't find timeline for corresponding pull op"
     124              :                     );
     125            0 :                     continue;
     126              :                 };
     127            0 :                 timeline_persist
     128            0 :                     .sk_set
     129            0 :                     .iter()
     130            0 :                     .filter_map(|sk_id| {
     131            0 :                         let other_node_id = NodeId(*sk_id as u64);
     132            0 :                         if node_id == other_node_id {
     133              :                             // We obviously don't want to pull from ourselves
     134            0 :                             return None;
     135            0 :                         }
     136            0 :                         let Some(sk) = safekeepers.get(&other_node_id) else {
     137            0 :                             tracing::warn!(
     138            0 :                                 "couldnt find safekeeper with pending op id {other_node_id}, not pulling from it"
     139              :                             );
     140            0 :                             return None;
     141              :                         };
     142            0 :                         Some((other_node_id, sk.base_url()))
     143            0 :                     })
     144            0 :                     .collect::<Vec<_>>()
     145              :             }
     146              :         };
     147            0 :         let req = ScheduleRequest {
     148            0 :             safekeeper: sk,
     149            0 :             host_list,
     150            0 :             tenant_id,
     151            0 :             timeline_id,
     152            0 :             generation: op_persist.generation as u32,
     153            0 :             kind: op_persist.op_kind,
     154            0 :         };
     155            0 :         res.push(req);
     156              :     }
     157            0 :     Ok(res)
     158            0 : }
     159              : 
     160              : pub(crate) struct ScheduleRequest {
     161              :     pub(crate) safekeeper: Box<Safekeeper>,
     162              :     pub(crate) host_list: Vec<(NodeId, String)>,
     163              :     pub(crate) tenant_id: TenantId,
     164              :     pub(crate) timeline_id: Option<TimelineId>,
     165              :     pub(crate) generation: u32,
     166              :     pub(crate) kind: SafekeeperTimelineOpKind,
     167              : }
     168              : 
     169              : /// Handle to per safekeeper reconciler.
     170              : struct ReconcilerHandle {
     171              :     tx: UnboundedSender<(ScheduleRequest, CancellationToken)>,
     172              :     ongoing_tokens: Arc<ClashMap<(TenantId, Option<TimelineId>), CancellationToken>>,
     173              :     cancel: CancellationToken,
     174              : }
     175              : 
     176              : impl ReconcilerHandle {
     177              :     /// Obtain a new token slot, cancelling any existing reconciliations for
     178              :     /// that timeline. It is not useful to have >1 operation per <tenant_id,
     179              :     /// timeline_id, safekeeper>, hence scheduling op cancels current one if it
     180              :     /// exists.
     181            0 :     fn new_token_slot(
     182            0 :         &self,
     183            0 :         tenant_id: TenantId,
     184            0 :         timeline_id: Option<TimelineId>,
     185            0 :     ) -> CancellationToken {
     186            0 :         let entry = self.ongoing_tokens.entry((tenant_id, timeline_id));
     187            0 :         if let Entry::Occupied(entry) = &entry {
     188            0 :             let cancel: &CancellationToken = entry.get();
     189            0 :             cancel.cancel();
     190            0 :         }
     191            0 :         entry.insert(self.cancel.child_token()).clone()
     192            0 :     }
     193              :     /// Cancel an ongoing reconciliation
     194            0 :     fn cancel_reconciliation(&self, tenant_id: TenantId, timeline_id: Option<TimelineId>) {
     195            0 :         if let Some((_, cancel)) = self.ongoing_tokens.remove(&(tenant_id, timeline_id)) {
     196            0 :             cancel.cancel();
     197            0 :         }
     198            0 :     }
     199            0 :     fn schedule_reconcile(&self, req: ScheduleRequest) {
     200            0 :         let cancel = self.new_token_slot(req.tenant_id, req.timeline_id);
     201            0 :         let hostname = req.safekeeper.skp.host.clone();
     202            0 :         if let Err(err) = self.tx.send((req, cancel)) {
     203            0 :             tracing::info!("scheduling request onto {hostname} returned error: {err}");
     204            0 :         }
     205            0 :     }
     206              : }
     207              : 
     208              : pub(crate) struct SafekeeperReconciler {
     209              :     service: Arc<Service>,
     210              :     rx: UnboundedReceiver<(ScheduleRequest, CancellationToken)>,
     211              :     cancel: CancellationToken,
     212              : }
     213              : 
     214              : impl SafekeeperReconciler {
     215            0 :     fn spawn(cancel: CancellationToken, service: Arc<Service>) -> ReconcilerHandle {
     216            0 :         // We hold the ServiceInner lock so we don't want to make sending to the reconciler channel to be blocking.
     217            0 :         let (tx, rx) = mpsc::unbounded_channel();
     218            0 :         let mut reconciler = SafekeeperReconciler {
     219            0 :             service,
     220            0 :             rx,
     221            0 :             cancel: cancel.clone(),
     222            0 :         };
     223            0 :         let handle = ReconcilerHandle {
     224            0 :             tx,
     225            0 :             ongoing_tokens: Arc::new(ClashMap::new()),
     226            0 :             cancel,
     227            0 :         };
     228            0 :         tokio::spawn(async move { reconciler.run().await });
     229            0 :         handle
     230            0 :     }
     231            0 :     async fn run(&mut self) {
     232              :         loop {
     233              :             // TODO add parallelism with semaphore here
     234            0 :             let req = tokio::select! {
     235            0 :                 req = self.rx.recv() => req,
     236            0 :                 _ = self.cancel.cancelled() => break,
     237              :             };
     238            0 :             let Some((req, req_cancel)) = req else { break };
     239            0 :             if req_cancel.is_cancelled() {
     240            0 :                 continue;
     241            0 :             }
     242            0 : 
     243            0 :             let kind = req.kind;
     244            0 :             let tenant_id = req.tenant_id;
     245            0 :             let timeline_id = req.timeline_id;
     246            0 :             let node_id = req.safekeeper.skp.id;
     247            0 :             self.reconcile_one(req, req_cancel)
     248            0 :                 .instrument(tracing::info_span!(
     249            0 :                     "reconcile_one",
     250            0 :                     ?kind,
     251            0 :                     %tenant_id,
     252            0 :                     ?timeline_id,
     253            0 :                     %node_id,
     254            0 :                 ))
     255            0 :                 .await;
     256              :         }
     257            0 :     }
     258            0 :     async fn reconcile_one(&self, req: ScheduleRequest, req_cancel: CancellationToken) {
     259            0 :         let req_host = req.safekeeper.skp.host.clone();
     260            0 :         match req.kind {
     261              :             SafekeeperTimelineOpKind::Pull => {
     262            0 :                 let Some(timeline_id) = req.timeline_id else {
     263            0 :                     tracing::warn!(
     264            0 :                         "ignoring invalid schedule request: timeline_id is empty for `pull`"
     265              :                     );
     266            0 :                     return;
     267              :                 };
     268            0 :                 let our_id = req.safekeeper.get_id();
     269            0 :                 let http_hosts = req
     270            0 :                     .host_list
     271            0 :                     .iter()
     272            0 :                     .filter(|(node_id, _hostname)| *node_id != our_id)
     273            0 :                     .map(|(_, hostname)| hostname.clone())
     274            0 :                     .collect::<Vec<_>>();
     275            0 :                 let pull_req = PullTimelineRequest {
     276            0 :                     http_hosts,
     277            0 :                     tenant_id: req.tenant_id,
     278            0 :                     timeline_id,
     279            0 :                 };
     280            0 :                 self.reconcile_inner(
     281            0 :                     req,
     282            0 :                     async |client| client.pull_timeline(&pull_req).await,
     283            0 :                     |resp| {
     284            0 :                         tracing::info!(
     285            0 :                             "pulled timeline from {} onto {req_host}",
     286              :                             resp.safekeeper_host,
     287              :                         );
     288            0 :                     },
     289            0 :                     req_cancel,
     290            0 :                 )
     291            0 :                 .await;
     292              :             }
     293              :             SafekeeperTimelineOpKind::Exclude => {
     294              :                 // TODO actually exclude instead of delete here
     295            0 :                 let tenant_id = req.tenant_id;
     296            0 :                 let Some(timeline_id) = req.timeline_id else {
     297            0 :                     tracing::warn!(
     298            0 :                         "ignoring invalid schedule request: timeline_id is empty for `exclude`"
     299              :                     );
     300            0 :                     return;
     301              :                 };
     302            0 :                 self.reconcile_inner(
     303            0 :                     req,
     304            0 :                     async |client| client.delete_timeline(tenant_id, timeline_id).await,
     305            0 :                     |_resp| {
     306            0 :                         tracing::info!("deleted timeline from {req_host}");
     307            0 :                     },
     308            0 :                     req_cancel,
     309            0 :                 )
     310            0 :                 .await;
     311              :             }
     312              :             SafekeeperTimelineOpKind::Delete => {
     313            0 :                 let tenant_id = req.tenant_id;
     314            0 :                 if let Some(timeline_id) = req.timeline_id {
     315            0 :                     let deleted = self
     316            0 :                         .reconcile_inner(
     317            0 :                             req,
     318            0 :                             async |client| client.delete_timeline(tenant_id, timeline_id).await,
     319            0 :                             |_resp| {
     320            0 :                                 tracing::info!("deleted timeline from {req_host}");
     321            0 :                             },
     322            0 :                             req_cancel,
     323            0 :                         )
     324            0 :                         .await;
     325            0 :                     if deleted {
     326            0 :                         self.delete_timeline_from_db(tenant_id, timeline_id).await;
     327            0 :                     }
     328              :                 } else {
     329            0 :                     let deleted = self
     330            0 :                         .reconcile_inner(
     331            0 :                             req,
     332            0 :                             async |client| client.delete_tenant(tenant_id).await,
     333            0 :                             |_resp| {
     334            0 :                                 tracing::info!(%tenant_id, "deleted tenant from {req_host}");
     335            0 :                             },
     336            0 :                             req_cancel,
     337            0 :                         )
     338            0 :                         .await;
     339            0 :                     if deleted {
     340            0 :                         self.delete_tenant_timelines_from_db(tenant_id).await;
     341            0 :                     }
     342              :                 }
     343              :             }
     344              :         }
     345            0 :     }
     346            0 :     async fn delete_timeline_from_db(&self, tenant_id: TenantId, timeline_id: TimelineId) {
     347            0 :         match self
     348            0 :             .service
     349            0 :             .persistence
     350            0 :             .list_pending_ops_for_timeline(tenant_id, timeline_id)
     351            0 :             .await
     352              :         {
     353            0 :             Ok(list) => {
     354            0 :                 if !list.is_empty() {
     355              :                     // duplicate the timeline_id here because it might be None in the reconcile context
     356            0 :                     tracing::info!(%timeline_id, "not deleting timeline from db as there is {} open reconciles", list.len());
     357            0 :                     return;
     358            0 :                 }
     359              :             }
     360            0 :             Err(e) => {
     361            0 :                 tracing::warn!(%timeline_id, "couldn't query pending ops: {e}");
     362            0 :                 return;
     363              :             }
     364              :         }
     365            0 :         tracing::info!(%tenant_id, %timeline_id, "deleting timeline from db after all reconciles succeeded");
     366              :         // In theory we could crash right after deleting the op from the db and right before reaching this,
     367              :         // but then we'll boot up with a timeline that has deleted_at set, so hopefully we'll issue deletion ops for it again.
     368            0 :         if let Err(err) = self
     369            0 :             .service
     370            0 :             .persistence
     371            0 :             .delete_timeline(tenant_id, timeline_id)
     372            0 :             .await
     373              :         {
     374            0 :             tracing::warn!(%tenant_id, %timeline_id, "couldn't delete timeline from db: {err}");
     375            0 :         }
     376            0 :     }
     377            0 :     async fn delete_tenant_timelines_from_db(&self, tenant_id: TenantId) {
     378            0 :         let timeline_list = match self
     379            0 :             .service
     380            0 :             .persistence
     381            0 :             .list_timelines_for_tenant(tenant_id)
     382            0 :             .await
     383              :         {
     384            0 :             Ok(timeline_list) => timeline_list,
     385            0 :             Err(e) => {
     386            0 :                 tracing::warn!(%tenant_id, "couldn't query timelines: {e}");
     387            0 :                 return;
     388              :             }
     389              :         };
     390            0 :         for timeline in timeline_list {
     391            0 :             let Ok(timeline_id) = TimelineId::from_str(&timeline.timeline_id) else {
     392            0 :                 tracing::warn!("Invalid timeline ID in database {}", timeline.timeline_id);
     393            0 :                 continue;
     394              :             };
     395            0 :             self.delete_timeline_from_db(tenant_id, timeline_id).await;
     396              :         }
     397            0 :     }
     398              :     /// Returns whether the reconciliation happened successfully
     399            0 :     async fn reconcile_inner<T, F, U>(
     400            0 :         &self,
     401            0 :         req: ScheduleRequest,
     402            0 :         closure: impl Fn(SafekeeperClient) -> F,
     403            0 :         log_success: impl FnOnce(T) -> U,
     404            0 :         req_cancel: CancellationToken,
     405            0 :     ) -> bool
     406            0 :     where
     407            0 :         F: Future<Output = Result<T, safekeeper_client::mgmt_api::Error>>,
     408            0 :     {
     409            0 :         let jwt = self
     410            0 :             .service
     411            0 :             .config
     412            0 :             .safekeeper_jwt_token
     413            0 :             .clone()
     414            0 :             .map(SecretString::from);
     415              :         loop {
     416            0 :             let res = req
     417            0 :                 .safekeeper
     418            0 :                 .with_client_retries(
     419            0 :                     |client| {
     420            0 :                         let closure = &closure;
     421            0 :                         async move { closure(client).await }
     422            0 :                     },
     423            0 :                     self.service.get_http_client(),
     424            0 :                     &jwt,
     425            0 :                     3,
     426            0 :                     10,
     427            0 :                     Duration::from_secs(10),
     428            0 :                     &req_cancel,
     429            0 :                 )
     430            0 :                 .await;
     431            0 :             match res {
     432            0 :                 Ok(resp) => {
     433            0 :                     log_success(resp);
     434            0 :                     let res = self
     435            0 :                         .service
     436            0 :                         .persistence
     437            0 :                         .remove_pending_op(
     438            0 :                             req.tenant_id,
     439            0 :                             req.timeline_id,
     440            0 :                             req.safekeeper.get_id(),
     441            0 :                             req.generation,
     442            0 :                         )
     443            0 :                         .await;
     444            0 :                     if let Err(err) = res {
     445            0 :                         tracing::info!(
     446            0 :                             "couldn't remove reconciliation request onto {} from persistence: {err:?}",
     447              :                             req.safekeeper.skp.host
     448              :                         );
     449            0 :                     }
     450            0 :                     return true;
     451              :                 }
     452              :                 Err(mgmt_api::Error::Cancelled) => {
     453              :                     // On cancellation, the code that issued it will take care of removing db entries (if needed)
     454            0 :                     return false;
     455              :                 }
     456            0 :                 Err(e) => {
     457            0 :                     tracing::info!(
     458            0 :                         "Reconcile attempt for safekeeper {} failed, retrying after sleep: {e:?}",
     459              :                         req.safekeeper.skp.host
     460              :                     );
     461              :                     const SLEEP_TIME: Duration = Duration::from_secs(1);
     462            0 :                     tokio::time::sleep(SLEEP_TIME).await;
     463              :                 }
     464              :             }
     465              :         }
     466            0 :     }
     467              : }
        

Generated by: LCOV version 2.1-beta