Line data Source code
1 : mod downloader;
2 : pub mod heatmap;
3 : mod heatmap_uploader;
4 : mod scheduler;
5 :
6 : use std::{sync::Arc, time::SystemTime};
7 :
8 : use crate::{
9 : context::RequestContext,
10 : disk_usage_eviction_task::DiskUsageEvictionInfo,
11 : task_mgr::{self, TaskKind, BACKGROUND_RUNTIME},
12 : };
13 :
14 : use self::{
15 : downloader::{downloader_task, SecondaryDetail},
16 : heatmap_uploader::heatmap_uploader_task,
17 : };
18 :
19 : use super::{
20 : config::{SecondaryLocationConfig, TenantConfOpt},
21 : mgr::TenantManager,
22 : span::debug_assert_current_span_has_tenant_id,
23 : storage_layer::LayerName,
24 : };
25 :
26 : use pageserver_api::{
27 : models,
28 : shard::{ShardIdentity, TenantShardId},
29 : };
30 : use remote_storage::GenericRemoteStorage;
31 :
32 : use tokio_util::sync::CancellationToken;
33 : use tracing::instrument;
34 : use utils::{completion::Barrier, id::TimelineId, sync::gate::Gate};
35 :
36 : enum DownloadCommand {
37 : Download(TenantShardId),
38 : }
39 : enum UploadCommand {
40 : Upload(TenantShardId),
41 : }
42 :
43 : impl UploadCommand {
44 0 : fn get_tenant_shard_id(&self) -> &TenantShardId {
45 0 : match self {
46 0 : Self::Upload(id) => id,
47 0 : }
48 0 : }
49 : }
50 :
51 : impl DownloadCommand {
52 0 : fn get_tenant_shard_id(&self) -> &TenantShardId {
53 0 : match self {
54 0 : Self::Download(id) => id,
55 0 : }
56 0 : }
57 : }
58 :
59 : struct CommandRequest<T> {
60 : payload: T,
61 : response_tx: tokio::sync::oneshot::Sender<CommandResponse>,
62 : }
63 :
64 : struct CommandResponse {
65 : result: anyhow::Result<()>,
66 : }
67 :
68 : // Whereas [`Tenant`] represents an attached tenant, this type represents the work
69 : // we do for secondary tenant locations: where we are not serving clients or
70 : // ingesting WAL, but we are maintaining a warm cache of layer files.
71 : //
72 : // This type is all about the _download_ path for secondary mode. The upload path
73 : // runs separately (see [`heatmap_uploader`]) while a regular attached `Tenant` exists.
74 : //
75 : // This structure coordinates TenantManager and SecondaryDownloader,
76 : // so that the downloader can indicate which tenants it is currently
77 : // operating on, and the manager can indicate when a particular
78 : // secondary tenant should cancel any work in flight.
79 : #[derive(Debug)]
80 : pub(crate) struct SecondaryTenant {
81 : /// Carrying a tenant shard ID simplifies callers such as the downloader
82 : /// which need to organize many of these objects by ID.
83 : tenant_shard_id: TenantShardId,
84 :
85 : /// Cancellation token indicates to SecondaryDownloader that it should stop doing
86 : /// any work for this tenant at the next opportunity.
87 : pub(crate) cancel: CancellationToken,
88 :
89 : pub(crate) gate: Gate,
90 :
91 : // Secondary mode does not need the full shard identity or the TenantConfOpt. However,
92 : // storing these enables us to report our full LocationConf, enabling convenient reconciliation
93 : // by the control plane (see [`Self::get_location_conf`])
94 : shard_identity: ShardIdentity,
95 : tenant_conf: std::sync::Mutex<TenantConfOpt>,
96 :
97 : // Internal state used by the Downloader.
98 : detail: std::sync::Mutex<SecondaryDetail>,
99 :
100 : // Public state indicating overall progress of downloads relative to the last heatmap seen
101 : pub(crate) progress: std::sync::Mutex<models::SecondaryProgress>,
102 : }
103 :
104 : impl SecondaryTenant {
105 0 : pub(crate) fn new(
106 0 : tenant_shard_id: TenantShardId,
107 0 : shard_identity: ShardIdentity,
108 0 : tenant_conf: TenantConfOpt,
109 0 : config: &SecondaryLocationConfig,
110 0 : ) -> Arc<Self> {
111 0 : Arc::new(Self {
112 0 : tenant_shard_id,
113 0 : // todo: shall we make this a descendent of the
114 0 : // main cancellation token, or is it sufficient that
115 0 : // on shutdown we walk the tenants and fire their
116 0 : // individual cancellations?
117 0 : cancel: CancellationToken::new(),
118 0 : gate: Gate::default(),
119 0 :
120 0 : shard_identity,
121 0 : tenant_conf: std::sync::Mutex::new(tenant_conf),
122 0 :
123 0 : detail: std::sync::Mutex::new(SecondaryDetail::new(config.clone())),
124 0 :
125 0 : progress: std::sync::Mutex::default(),
126 0 : })
127 0 : }
128 :
129 0 : pub(crate) fn tenant_shard_id(&self) -> TenantShardId {
130 0 : self.tenant_shard_id
131 0 : }
132 :
133 0 : pub(crate) async fn shutdown(&self) {
134 0 : self.cancel.cancel();
135 0 :
136 0 : // Wait for any secondary downloader work to complete
137 0 : self.gate.close().await;
138 0 : }
139 :
140 0 : pub(crate) fn set_config(&self, config: &SecondaryLocationConfig) {
141 0 : self.detail.lock().unwrap().config = config.clone();
142 0 : }
143 :
144 0 : pub(crate) fn set_tenant_conf(&self, config: &TenantConfOpt) {
145 0 : *(self.tenant_conf.lock().unwrap()) = config.clone();
146 0 : }
147 :
148 : /// For API access: generate a LocationConfig equivalent to the one that would be used to
149 : /// create a Tenant in the same state. Do not use this in hot paths: it's for relatively
150 : /// rare external API calls, like a reconciliation at startup.
151 0 : pub(crate) fn get_location_conf(&self) -> models::LocationConfig {
152 0 : let conf = self.detail.lock().unwrap().config.clone();
153 0 :
154 0 : let conf = models::LocationConfigSecondary { warm: conf.warm };
155 0 :
156 0 : let tenant_conf = self.tenant_conf.lock().unwrap().clone();
157 0 : models::LocationConfig {
158 0 : mode: models::LocationConfigMode::Secondary,
159 0 : generation: None,
160 0 : secondary_conf: Some(conf),
161 0 : shard_number: self.tenant_shard_id.shard_number.0,
162 0 : shard_count: self.tenant_shard_id.shard_count.literal(),
163 0 : shard_stripe_size: self.shard_identity.stripe_size.0,
164 0 : tenant_conf: tenant_conf.into(),
165 0 : }
166 0 : }
167 :
168 0 : pub(crate) fn get_tenant_shard_id(&self) -> &TenantShardId {
169 0 : &self.tenant_shard_id
170 0 : }
171 :
172 0 : pub(crate) fn get_layers_for_eviction(self: &Arc<Self>) -> (DiskUsageEvictionInfo, usize) {
173 0 : self.detail.lock().unwrap().get_layers_for_eviction(self)
174 0 : }
175 :
176 : /// Cancellation safe, but on cancellation the eviction will go through
177 0 : #[instrument(skip_all, fields(tenant_id=%self.tenant_shard_id.tenant_id, shard_id=%self.tenant_shard_id.shard_slug(), timeline_id=%timeline_id, name=%name))]
178 : pub(crate) async fn evict_layer(self: &Arc<Self>, timeline_id: TimelineId, name: LayerName) {
179 : debug_assert_current_span_has_tenant_id();
180 :
181 : let guard = match self.gate.enter() {
182 : Ok(g) => g,
183 : Err(_) => {
184 : tracing::debug!("Dropping layer evictions, secondary tenant shutting down",);
185 : return;
186 : }
187 : };
188 :
189 : let now = SystemTime::now();
190 : tracing::info!("Evicting secondary layer");
191 :
192 : let this = self.clone();
193 :
194 : // spawn it to be cancellation safe
195 0 : tokio::task::spawn_blocking(move || {
196 0 : let _guard = guard;
197 0 :
198 0 : // Update the timeline's state. This does not have to be synchronized with
199 0 : // the download process, because:
200 0 : // - If downloader is racing with us to remove a file (e.g. because it is
201 0 : // removed from heatmap), then our mutual .remove() operations will both
202 0 : // succeed.
203 0 : // - If downloader is racing with us to download the object (this would require
204 0 : // multiple eviction iterations to race with multiple download iterations), then
205 0 : // if we remove it from the state, the worst that happens is the downloader
206 0 : // downloads it again before re-inserting, or we delete the file but it remains
207 0 : // in the state map (in which case it will be downloaded if this secondary
208 0 : // tenant transitions to attached and tries to access it)
209 0 : //
210 0 : // The important assumption here is that the secondary timeline state does not
211 0 : // have to 100% match what is on disk, because it's a best-effort warming
212 0 : // of the cache.
213 0 : let mut detail = this.detail.lock().unwrap();
214 0 : if let Some(timeline_detail) = detail.timelines.get_mut(&timeline_id) {
215 0 : let removed = timeline_detail.on_disk_layers.remove(&name);
216 :
217 : // We might race with removal of the same layer during downloads, if it was removed
218 : // from the heatmap. If we see that the OnDiskState is gone, then no need to
219 : // do a physical deletion or store in evicted_at.
220 0 : if let Some(removed) = removed {
221 0 : removed.remove_blocking();
222 0 : timeline_detail.evicted_at.insert(name, now);
223 0 : }
224 0 : }
225 0 : })
226 : .await
227 : .expect("secondary eviction should not have panicked");
228 : }
229 : }
230 :
231 : /// The SecondaryController is a pseudo-rpc client for administrative control of secondary mode downloads,
232 : /// and heatmap uploads. This is not a hot data path: it's used for:
233 : /// - Live migrations, where we want to ensure a migration destination has the freshest possible
234 : /// content before trying to cut over.
235 : /// - Tests, where we want to immediately upload/download for a particular tenant.
236 : ///
237 : /// In normal operations, outside of migrations, uploads & downloads are autonomous and not driven by this interface.
238 : pub struct SecondaryController {
239 : upload_req_tx: tokio::sync::mpsc::Sender<CommandRequest<UploadCommand>>,
240 : download_req_tx: tokio::sync::mpsc::Sender<CommandRequest<DownloadCommand>>,
241 : }
242 :
243 : impl SecondaryController {
244 0 : async fn dispatch<T>(
245 0 : &self,
246 0 : queue: &tokio::sync::mpsc::Sender<CommandRequest<T>>,
247 0 : payload: T,
248 0 : ) -> anyhow::Result<()> {
249 0 : let (response_tx, response_rx) = tokio::sync::oneshot::channel();
250 0 :
251 0 : queue
252 0 : .send(CommandRequest {
253 0 : payload,
254 0 : response_tx,
255 0 : })
256 0 : .await
257 0 : .map_err(|_| anyhow::anyhow!("Receiver shut down"))?;
258 :
259 0 : let response = response_rx
260 0 : .await
261 0 : .map_err(|_| anyhow::anyhow!("Request dropped"))?;
262 :
263 0 : response.result
264 0 : }
265 :
266 0 : pub async fn upload_tenant(&self, tenant_shard_id: TenantShardId) -> anyhow::Result<()> {
267 0 : self.dispatch(&self.upload_req_tx, UploadCommand::Upload(tenant_shard_id))
268 0 : .await
269 0 : }
270 0 : pub async fn download_tenant(&self, tenant_shard_id: TenantShardId) -> anyhow::Result<()> {
271 0 : self.dispatch(
272 0 : &self.download_req_tx,
273 0 : DownloadCommand::Download(tenant_shard_id),
274 0 : )
275 0 : .await
276 0 : }
277 : }
278 :
279 0 : pub fn spawn_tasks(
280 0 : tenant_manager: Arc<TenantManager>,
281 0 : remote_storage: GenericRemoteStorage,
282 0 : background_jobs_can_start: Barrier,
283 0 : cancel: CancellationToken,
284 0 : ) -> SecondaryController {
285 0 : let mgr_clone = tenant_manager.clone();
286 0 : let storage_clone = remote_storage.clone();
287 0 : let cancel_clone = cancel.clone();
288 0 : let bg_jobs_clone = background_jobs_can_start.clone();
289 0 :
290 0 : let (download_req_tx, download_req_rx) =
291 0 : tokio::sync::mpsc::channel::<CommandRequest<DownloadCommand>>(16);
292 0 : let (upload_req_tx, upload_req_rx) =
293 0 : tokio::sync::mpsc::channel::<CommandRequest<UploadCommand>>(16);
294 0 :
295 0 : let downloader_task_ctx = RequestContext::new(
296 0 : TaskKind::SecondaryDownloads,
297 0 : crate::context::DownloadBehavior::Download,
298 0 : );
299 0 : task_mgr::spawn(
300 0 : BACKGROUND_RUNTIME.handle(),
301 0 : downloader_task_ctx.task_kind(),
302 0 : None,
303 0 : None,
304 0 : "secondary tenant downloads",
305 0 : false,
306 0 : async move {
307 0 : downloader_task(
308 0 : mgr_clone,
309 0 : storage_clone,
310 0 : download_req_rx,
311 0 : bg_jobs_clone,
312 0 : cancel_clone,
313 0 : downloader_task_ctx,
314 0 : )
315 0 : .await;
316 :
317 0 : Ok(())
318 0 : },
319 0 : );
320 0 :
321 0 : task_mgr::spawn(
322 0 : BACKGROUND_RUNTIME.handle(),
323 0 : TaskKind::SecondaryUploads,
324 0 : None,
325 0 : None,
326 0 : "heatmap uploads",
327 0 : false,
328 0 : async move {
329 0 : heatmap_uploader_task(
330 0 : tenant_manager,
331 0 : remote_storage,
332 0 : upload_req_rx,
333 0 : background_jobs_can_start,
334 0 : cancel,
335 0 : )
336 0 : .await;
337 :
338 0 : Ok(())
339 0 : },
340 0 : );
341 0 :
342 0 : SecondaryController {
343 0 : download_req_tx,
344 0 : upload_req_tx,
345 0 : }
346 0 : }
347 :
348 : /// For running with remote storage disabled: a SecondaryController that is connected to nothing.
349 0 : pub fn null_controller() -> SecondaryController {
350 0 : let (download_req_tx, _download_req_rx) =
351 0 : tokio::sync::mpsc::channel::<CommandRequest<DownloadCommand>>(16);
352 0 : let (upload_req_tx, _upload_req_rx) =
353 0 : tokio::sync::mpsc::channel::<CommandRequest<UploadCommand>>(16);
354 0 : SecondaryController {
355 0 : upload_req_tx,
356 0 : download_req_tx,
357 0 : }
358 0 : }
|