Line data Source code
1 : //! Functions for handling per-tenant configuration options
2 : //!
3 : //! If tenant is created with --config option,
4 : //! the tenant-specific config will be stored in tenant's directory.
5 : //! Otherwise, global pageserver's config is used.
6 : //!
7 : //! If the tenant config file is corrupted, the tenant will be disabled.
8 : //! We cannot use global or default config instead, because wrong settings
9 : //! may lead to a data loss.
10 : //!
11 : pub(crate) use pageserver_api::config::TenantConfigToml as TenantConf;
12 : use pageserver_api::models::CompactionAlgorithmSettings;
13 : use pageserver_api::models::EvictionPolicy;
14 : use pageserver_api::models::{self, ThrottleConfig};
15 : use pageserver_api::shard::{ShardCount, ShardIdentity, ShardNumber, ShardStripeSize};
16 : use serde::de::IntoDeserializer;
17 : use serde::{Deserialize, Serialize};
18 : use serde_json::Value;
19 : use std::num::NonZeroU64;
20 : use std::time::Duration;
21 : use utils::generation::Generation;
22 :
23 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
24 : pub(crate) enum AttachmentMode {
25 : /// Our generation is current as far as we know, and as far as we know we are the only attached
26 : /// pageserver. This is the "normal" attachment mode.
27 : Single,
28 : /// Our generation number is current as far as we know, but we are advised that another
29 : /// pageserver is still attached, and therefore to avoid executing deletions. This is
30 : /// the attachment mode of a pagesever that is the destination of a migration.
31 : Multi,
32 : /// Our generation number is superseded, or about to be superseded. We are advised
33 : /// to avoid remote storage writes if possible, and to avoid sending billing data. This
34 : /// is the attachment mode of a pageserver that is the origin of a migration.
35 : Stale,
36 : }
37 :
38 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
39 : pub(crate) struct AttachedLocationConfig {
40 : pub(crate) generation: Generation,
41 : pub(crate) attach_mode: AttachmentMode,
42 : // TODO: add a flag to override AttachmentMode's policies under
43 : // disk pressure (i.e. unblock uploads under disk pressure in Stale
44 : // state, unblock deletions after timeout in Multi state)
45 : }
46 :
47 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48 : pub(crate) struct SecondaryLocationConfig {
49 : /// If true, keep the local cache warm by polling remote storage
50 : pub(crate) warm: bool,
51 : }
52 :
53 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54 : pub(crate) enum LocationMode {
55 : Attached(AttachedLocationConfig),
56 : Secondary(SecondaryLocationConfig),
57 : }
58 :
59 : /// Per-tenant, per-pageserver configuration. All pageservers use the same TenantConf,
60 : /// but have distinct LocationConf.
61 0 : #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
62 : pub(crate) struct LocationConf {
63 : /// The location-specific part of the configuration, describes the operating
64 : /// mode of this pageserver for this tenant.
65 : pub(crate) mode: LocationMode,
66 :
67 : /// The detailed shard identity. This structure is already scoped within
68 : /// a TenantShardId, but we need the full ShardIdentity to enable calculating
69 : /// key->shard mappings.
70 : #[serde(default = "ShardIdentity::unsharded")]
71 : #[serde(skip_serializing_if = "ShardIdentity::is_unsharded")]
72 : pub(crate) shard: ShardIdentity,
73 :
74 : /// The pan-cluster tenant configuration, the same on all locations
75 : pub(crate) tenant_conf: TenantConfOpt,
76 : }
77 :
78 : impl std::fmt::Debug for LocationConf {
79 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 0 : match &self.mode {
81 0 : LocationMode::Attached(conf) => {
82 0 : write!(
83 0 : f,
84 0 : "Attached {:?}, gen={:?}",
85 0 : conf.attach_mode, conf.generation
86 0 : )
87 : }
88 0 : LocationMode::Secondary(conf) => {
89 0 : write!(f, "Secondary, warm={}", conf.warm)
90 : }
91 : }
92 0 : }
93 : }
94 :
95 : impl AttachedLocationConfig {
96 : /// Consult attachment mode to determine whether we are currently permitted
97 : /// to delete layers. This is only advisory, not required for data safety.
98 : /// See [`AttachmentMode`] for more context.
99 754 : pub(crate) fn may_delete_layers_hint(&self) -> bool {
100 754 : // TODO: add an override for disk pressure in AttachedLocationConfig,
101 754 : // and respect it here.
102 754 : match &self.attach_mode {
103 754 : AttachmentMode::Single => true,
104 : AttachmentMode::Multi | AttachmentMode::Stale => {
105 : // In Multi mode we avoid doing deletions because some other
106 : // attached pageserver might get 404 while trying to read
107 : // a layer we delete which is still referenced in their metadata.
108 : //
109 : // In Stale mode, we avoid doing deletions because we expect
110 : // that they would ultimately fail validation in the deletion
111 : // queue due to our stale generation.
112 0 : false
113 : }
114 : }
115 754 : }
116 :
117 : /// Whether we are currently hinted that it is worthwhile to upload layers.
118 : /// This is only advisory, not required for data safety.
119 : /// See [`AttachmentMode`] for more context.
120 0 : pub(crate) fn may_upload_layers_hint(&self) -> bool {
121 0 : // TODO: add an override for disk pressure in AttachedLocationConfig,
122 0 : // and respect it here.
123 0 : match &self.attach_mode {
124 0 : AttachmentMode::Single | AttachmentMode::Multi => true,
125 : AttachmentMode::Stale => {
126 : // In Stale mode, we avoid doing uploads because we expect that
127 : // our replacement pageserver will already have started its own
128 : // IndexPart that will never reference layers we upload: it is
129 : // wasteful.
130 0 : false
131 : }
132 : }
133 0 : }
134 : }
135 :
136 : impl LocationConf {
137 : /// For use when loading from a legacy configuration: presence of a tenant
138 : /// implies it is in AttachmentMode::Single, which used to be the only
139 : /// possible state. This function should eventually be removed.
140 190 : pub(crate) fn attached_single(
141 190 : tenant_conf: TenantConfOpt,
142 190 : generation: Generation,
143 190 : shard_params: &models::ShardParameters,
144 190 : ) -> Self {
145 190 : Self {
146 190 : mode: LocationMode::Attached(AttachedLocationConfig {
147 190 : generation,
148 190 : attach_mode: AttachmentMode::Single,
149 190 : }),
150 190 : shard: ShardIdentity::from_params(ShardNumber(0), shard_params),
151 190 : tenant_conf,
152 190 : }
153 190 : }
154 :
155 : /// For use when attaching/re-attaching: update the generation stored in this
156 : /// structure. If we were in a secondary state, promote to attached (posession
157 : /// of a fresh generation implies this).
158 0 : pub(crate) fn attach_in_generation(&mut self, mode: AttachmentMode, generation: Generation) {
159 0 : match &mut self.mode {
160 0 : LocationMode::Attached(attach_conf) => {
161 0 : attach_conf.generation = generation;
162 0 : attach_conf.attach_mode = mode;
163 0 : }
164 : LocationMode::Secondary(_) => {
165 : // We are promoted to attached by the control plane's re-attach response
166 0 : self.mode = LocationMode::Attached(AttachedLocationConfig {
167 0 : generation,
168 0 : attach_mode: mode,
169 0 : })
170 : }
171 : }
172 0 : }
173 :
174 0 : pub(crate) fn try_from(conf: &'_ models::LocationConfig) -> anyhow::Result<Self> {
175 0 : let tenant_conf = TenantConfOpt::try_from(&conf.tenant_conf)?;
176 :
177 0 : fn get_generation(conf: &'_ models::LocationConfig) -> Result<Generation, anyhow::Error> {
178 0 : conf.generation
179 0 : .map(Generation::new)
180 0 : .ok_or_else(|| anyhow::anyhow!("Generation must be set when attaching"))
181 0 : }
182 :
183 0 : let mode = match &conf.mode {
184 : models::LocationConfigMode::AttachedMulti => {
185 : LocationMode::Attached(AttachedLocationConfig {
186 0 : generation: get_generation(conf)?,
187 0 : attach_mode: AttachmentMode::Multi,
188 : })
189 : }
190 : models::LocationConfigMode::AttachedSingle => {
191 : LocationMode::Attached(AttachedLocationConfig {
192 0 : generation: get_generation(conf)?,
193 0 : attach_mode: AttachmentMode::Single,
194 : })
195 : }
196 : models::LocationConfigMode::AttachedStale => {
197 : LocationMode::Attached(AttachedLocationConfig {
198 0 : generation: get_generation(conf)?,
199 0 : attach_mode: AttachmentMode::Stale,
200 : })
201 : }
202 : models::LocationConfigMode::Secondary => {
203 0 : anyhow::ensure!(conf.generation.is_none());
204 :
205 0 : let warm = conf
206 0 : .secondary_conf
207 0 : .as_ref()
208 0 : .map(|c| c.warm)
209 0 : .unwrap_or(false);
210 0 : LocationMode::Secondary(SecondaryLocationConfig { warm })
211 : }
212 : models::LocationConfigMode::Detached => {
213 : // Should not have been called: API code should translate this mode
214 : // into a detach rather than trying to decode it as a LocationConf
215 0 : return Err(anyhow::anyhow!("Cannot decode a Detached configuration"));
216 : }
217 : };
218 :
219 0 : let shard = if conf.shard_count == 0 {
220 0 : ShardIdentity::unsharded()
221 : } else {
222 0 : ShardIdentity::new(
223 0 : ShardNumber(conf.shard_number),
224 0 : ShardCount::new(conf.shard_count),
225 0 : ShardStripeSize(conf.shard_stripe_size),
226 0 : )?
227 : };
228 :
229 0 : Ok(Self {
230 0 : shard,
231 0 : mode,
232 0 : tenant_conf,
233 0 : })
234 0 : }
235 : }
236 :
237 : impl Default for LocationConf {
238 : // TODO: this should be removed once tenant loading can guarantee that we are never
239 : // loading from a directory without a configuration.
240 : // => tech debt since https://github.com/neondatabase/neon/issues/1555
241 0 : fn default() -> Self {
242 0 : Self {
243 0 : mode: LocationMode::Attached(AttachedLocationConfig {
244 0 : generation: Generation::none(),
245 0 : attach_mode: AttachmentMode::Single,
246 0 : }),
247 0 : tenant_conf: TenantConfOpt::default(),
248 0 : shard: ShardIdentity::unsharded(),
249 0 : }
250 0 : }
251 : }
252 :
253 : /// Same as TenantConf, but this struct preserves the information about
254 : /// which parameters are set and which are not.
255 120 : #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
256 : pub struct TenantConfOpt {
257 : #[serde(skip_serializing_if = "Option::is_none")]
258 : #[serde(default)]
259 : pub checkpoint_distance: Option<u64>,
260 :
261 : #[serde(skip_serializing_if = "Option::is_none")]
262 : #[serde(with = "humantime_serde")]
263 : #[serde(default)]
264 : pub checkpoint_timeout: Option<Duration>,
265 :
266 : #[serde(skip_serializing_if = "Option::is_none")]
267 : #[serde(default)]
268 : pub compaction_target_size: Option<u64>,
269 :
270 : #[serde(skip_serializing_if = "Option::is_none")]
271 : #[serde(with = "humantime_serde")]
272 : #[serde(default)]
273 : pub compaction_period: Option<Duration>,
274 :
275 : #[serde(skip_serializing_if = "Option::is_none")]
276 : #[serde(default)]
277 : pub compaction_threshold: Option<usize>,
278 :
279 : #[serde(skip_serializing_if = "Option::is_none")]
280 : #[serde(default)]
281 : pub compaction_algorithm: Option<CompactionAlgorithmSettings>,
282 :
283 : #[serde(skip_serializing_if = "Option::is_none")]
284 : #[serde(default)]
285 : pub gc_horizon: Option<u64>,
286 :
287 : #[serde(skip_serializing_if = "Option::is_none")]
288 : #[serde(with = "humantime_serde")]
289 : #[serde(default)]
290 : pub gc_period: Option<Duration>,
291 :
292 : #[serde(skip_serializing_if = "Option::is_none")]
293 : #[serde(default)]
294 : pub image_creation_threshold: Option<usize>,
295 :
296 : #[serde(skip_serializing_if = "Option::is_none")]
297 : #[serde(with = "humantime_serde")]
298 : #[serde(default)]
299 : pub pitr_interval: Option<Duration>,
300 :
301 : #[serde(skip_serializing_if = "Option::is_none")]
302 : #[serde(with = "humantime_serde")]
303 : #[serde(default)]
304 : pub walreceiver_connect_timeout: Option<Duration>,
305 :
306 : #[serde(skip_serializing_if = "Option::is_none")]
307 : #[serde(with = "humantime_serde")]
308 : #[serde(default)]
309 : pub lagging_wal_timeout: Option<Duration>,
310 :
311 : #[serde(skip_serializing_if = "Option::is_none")]
312 : #[serde(default)]
313 : pub max_lsn_wal_lag: Option<NonZeroU64>,
314 :
315 : #[serde(skip_serializing_if = "Option::is_none")]
316 : #[serde(default)]
317 : pub eviction_policy: Option<EvictionPolicy>,
318 :
319 : #[serde(skip_serializing_if = "Option::is_none")]
320 : #[serde(default)]
321 : pub min_resident_size_override: Option<u64>,
322 :
323 : #[serde(skip_serializing_if = "Option::is_none")]
324 : #[serde(with = "humantime_serde")]
325 : #[serde(default)]
326 : pub evictions_low_residence_duration_metric_threshold: Option<Duration>,
327 :
328 : #[serde(skip_serializing_if = "Option::is_none")]
329 : #[serde(with = "humantime_serde")]
330 : #[serde(default)]
331 : pub heatmap_period: Option<Duration>,
332 :
333 : #[serde(skip_serializing_if = "Option::is_none")]
334 : #[serde(default)]
335 : pub lazy_slru_download: Option<bool>,
336 :
337 : #[serde(skip_serializing_if = "Option::is_none")]
338 : pub timeline_get_throttle: Option<pageserver_api::models::ThrottleConfig>,
339 :
340 : #[serde(skip_serializing_if = "Option::is_none")]
341 : pub image_layer_creation_check_threshold: Option<u8>,
342 :
343 : #[serde(skip_serializing_if = "Option::is_none")]
344 : #[serde(with = "humantime_serde")]
345 : #[serde(default)]
346 : pub lsn_lease_length: Option<Duration>,
347 :
348 : #[serde(skip_serializing_if = "Option::is_none")]
349 : #[serde(with = "humantime_serde")]
350 : #[serde(default)]
351 : pub lsn_lease_length_for_ts: Option<Duration>,
352 :
353 : #[serde(skip_serializing_if = "Option::is_none")]
354 : #[serde(default)]
355 : pub timeline_offloading: Option<bool>,
356 : }
357 :
358 : impl TenantConfOpt {
359 0 : pub fn merge(&self, global_conf: TenantConf) -> TenantConf {
360 0 : TenantConf {
361 0 : checkpoint_distance: self
362 0 : .checkpoint_distance
363 0 : .unwrap_or(global_conf.checkpoint_distance),
364 0 : checkpoint_timeout: self
365 0 : .checkpoint_timeout
366 0 : .unwrap_or(global_conf.checkpoint_timeout),
367 0 : compaction_target_size: self
368 0 : .compaction_target_size
369 0 : .unwrap_or(global_conf.compaction_target_size),
370 0 : compaction_period: self
371 0 : .compaction_period
372 0 : .unwrap_or(global_conf.compaction_period),
373 0 : compaction_threshold: self
374 0 : .compaction_threshold
375 0 : .unwrap_or(global_conf.compaction_threshold),
376 0 : compaction_algorithm: self
377 0 : .compaction_algorithm
378 0 : .as_ref()
379 0 : .unwrap_or(&global_conf.compaction_algorithm)
380 0 : .clone(),
381 0 : gc_horizon: self.gc_horizon.unwrap_or(global_conf.gc_horizon),
382 0 : gc_period: self.gc_period.unwrap_or(global_conf.gc_period),
383 0 : image_creation_threshold: self
384 0 : .image_creation_threshold
385 0 : .unwrap_or(global_conf.image_creation_threshold),
386 0 : pitr_interval: self.pitr_interval.unwrap_or(global_conf.pitr_interval),
387 0 : walreceiver_connect_timeout: self
388 0 : .walreceiver_connect_timeout
389 0 : .unwrap_or(global_conf.walreceiver_connect_timeout),
390 0 : lagging_wal_timeout: self
391 0 : .lagging_wal_timeout
392 0 : .unwrap_or(global_conf.lagging_wal_timeout),
393 0 : max_lsn_wal_lag: self.max_lsn_wal_lag.unwrap_or(global_conf.max_lsn_wal_lag),
394 0 : eviction_policy: self.eviction_policy.unwrap_or(global_conf.eviction_policy),
395 0 : min_resident_size_override: self
396 0 : .min_resident_size_override
397 0 : .or(global_conf.min_resident_size_override),
398 0 : evictions_low_residence_duration_metric_threshold: self
399 0 : .evictions_low_residence_duration_metric_threshold
400 0 : .unwrap_or(global_conf.evictions_low_residence_duration_metric_threshold),
401 0 : heatmap_period: self.heatmap_period.unwrap_or(global_conf.heatmap_period),
402 0 : lazy_slru_download: self
403 0 : .lazy_slru_download
404 0 : .unwrap_or(global_conf.lazy_slru_download),
405 0 : timeline_get_throttle: self
406 0 : .timeline_get_throttle
407 0 : .clone()
408 0 : .unwrap_or(global_conf.timeline_get_throttle),
409 0 : image_layer_creation_check_threshold: self
410 0 : .image_layer_creation_check_threshold
411 0 : .unwrap_or(global_conf.image_layer_creation_check_threshold),
412 0 : lsn_lease_length: self
413 0 : .lsn_lease_length
414 0 : .unwrap_or(global_conf.lsn_lease_length),
415 0 : lsn_lease_length_for_ts: self
416 0 : .lsn_lease_length_for_ts
417 0 : .unwrap_or(global_conf.lsn_lease_length_for_ts),
418 0 : timeline_offloading: self
419 0 : .lazy_slru_download
420 0 : .unwrap_or(global_conf.timeline_offloading),
421 0 : }
422 0 : }
423 : }
424 :
425 : impl TryFrom<&'_ models::TenantConfig> for TenantConfOpt {
426 : type Error = anyhow::Error;
427 :
428 4 : fn try_from(request_data: &'_ models::TenantConfig) -> Result<Self, Self::Error> {
429 : // Convert the request_data to a JSON Value
430 4 : let json_value: Value = serde_json::to_value(request_data)?;
431 :
432 : // Create a Deserializer from the JSON Value
433 4 : let deserializer = json_value.into_deserializer();
434 :
435 : // Use serde_path_to_error to deserialize the JSON Value into TenantConfOpt
436 4 : let tenant_conf: TenantConfOpt = serde_path_to_error::deserialize(deserializer)?;
437 :
438 2 : Ok(tenant_conf)
439 4 : }
440 : }
441 :
442 : /// This is a conversion from our internal tenant config object to the one used
443 : /// in external APIs.
444 : impl From<TenantConfOpt> for models::TenantConfig {
445 0 : fn from(value: TenantConfOpt) -> Self {
446 0 : fn humantime(d: Duration) -> String {
447 0 : format!("{}s", d.as_secs())
448 0 : }
449 0 : Self {
450 0 : checkpoint_distance: value.checkpoint_distance,
451 0 : checkpoint_timeout: value.checkpoint_timeout.map(humantime),
452 0 : compaction_algorithm: value.compaction_algorithm,
453 0 : compaction_target_size: value.compaction_target_size,
454 0 : compaction_period: value.compaction_period.map(humantime),
455 0 : compaction_threshold: value.compaction_threshold,
456 0 : gc_horizon: value.gc_horizon,
457 0 : gc_period: value.gc_period.map(humantime),
458 0 : image_creation_threshold: value.image_creation_threshold,
459 0 : pitr_interval: value.pitr_interval.map(humantime),
460 0 : walreceiver_connect_timeout: value.walreceiver_connect_timeout.map(humantime),
461 0 : lagging_wal_timeout: value.lagging_wal_timeout.map(humantime),
462 0 : max_lsn_wal_lag: value.max_lsn_wal_lag,
463 0 : eviction_policy: value.eviction_policy,
464 0 : min_resident_size_override: value.min_resident_size_override,
465 0 : evictions_low_residence_duration_metric_threshold: value
466 0 : .evictions_low_residence_duration_metric_threshold
467 0 : .map(humantime),
468 0 : heatmap_period: value.heatmap_period.map(humantime),
469 0 : lazy_slru_download: value.lazy_slru_download,
470 0 : timeline_get_throttle: value.timeline_get_throttle.map(ThrottleConfig::from),
471 0 : image_layer_creation_check_threshold: value.image_layer_creation_check_threshold,
472 0 : lsn_lease_length: value.lsn_lease_length.map(humantime),
473 0 : lsn_lease_length_for_ts: value.lsn_lease_length_for_ts.map(humantime),
474 0 : timeline_offloading: value.timeline_offloading,
475 0 : }
476 0 : }
477 : }
478 :
479 : #[cfg(test)]
480 : mod tests {
481 : use super::*;
482 : use models::TenantConfig;
483 :
484 : #[test]
485 2 : fn de_serializing_pageserver_config_omits_empty_values() {
486 2 : let small_conf = TenantConfOpt {
487 2 : gc_horizon: Some(42),
488 2 : ..TenantConfOpt::default()
489 2 : };
490 2 :
491 2 : let toml_form = toml_edit::ser::to_string(&small_conf).unwrap();
492 2 : assert_eq!(toml_form, "gc_horizon = 42\n");
493 2 : assert_eq!(small_conf, toml_edit::de::from_str(&toml_form).unwrap());
494 :
495 2 : let json_form = serde_json::to_string(&small_conf).unwrap();
496 2 : assert_eq!(json_form, "{\"gc_horizon\":42}");
497 2 : assert_eq!(small_conf, serde_json::from_str(&json_form).unwrap());
498 2 : }
499 :
500 : #[test]
501 2 : fn test_try_from_models_tenant_config_err() {
502 2 : let tenant_config = models::TenantConfig {
503 2 : lagging_wal_timeout: Some("5a".to_string()),
504 2 : ..TenantConfig::default()
505 2 : };
506 2 :
507 2 : let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config);
508 2 :
509 2 : assert!(
510 2 : tenant_conf_opt.is_err(),
511 0 : "Suceeded to convert TenantConfig to TenantConfOpt"
512 : );
513 :
514 2 : let expected_error_str =
515 2 : "lagging_wal_timeout: invalid value: string \"5a\", expected a duration";
516 2 : assert_eq!(tenant_conf_opt.unwrap_err().to_string(), expected_error_str);
517 2 : }
518 :
519 : #[test]
520 2 : fn test_try_from_models_tenant_config_success() {
521 2 : let tenant_config = models::TenantConfig {
522 2 : lagging_wal_timeout: Some("5s".to_string()),
523 2 : ..TenantConfig::default()
524 2 : };
525 2 :
526 2 : let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config).unwrap();
527 2 :
528 2 : assert_eq!(
529 2 : tenant_conf_opt.lagging_wal_timeout,
530 2 : Some(Duration::from_secs(5))
531 2 : );
532 2 : }
533 : }
|