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, TenantConfigPatch};
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 : use utils::postgres_client::PostgresClientProtocol;
23 :
24 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
25 : pub(crate) enum AttachmentMode {
26 : /// Our generation is current as far as we know, and as far as we know we are the only attached
27 : /// pageserver. This is the "normal" attachment mode.
28 : Single,
29 : /// Our generation number is current as far as we know, but we are advised that another
30 : /// pageserver is still attached, and therefore to avoid executing deletions. This is
31 : /// the attachment mode of a pagesever that is the destination of a migration.
32 : Multi,
33 : /// Our generation number is superseded, or about to be superseded. We are advised
34 : /// to avoid remote storage writes if possible, and to avoid sending billing data. This
35 : /// is the attachment mode of a pageserver that is the origin of a migration.
36 : Stale,
37 : }
38 :
39 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
40 : pub(crate) struct AttachedLocationConfig {
41 : pub(crate) generation: Generation,
42 : pub(crate) attach_mode: AttachmentMode,
43 : // TODO: add a flag to override AttachmentMode's policies under
44 : // disk pressure (i.e. unblock uploads under disk pressure in Stale
45 : // state, unblock deletions after timeout in Multi state)
46 : }
47 :
48 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49 : pub(crate) struct SecondaryLocationConfig {
50 : /// If true, keep the local cache warm by polling remote storage
51 : pub(crate) warm: bool,
52 : }
53 :
54 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55 : pub(crate) enum LocationMode {
56 : Attached(AttachedLocationConfig),
57 : Secondary(SecondaryLocationConfig),
58 : }
59 :
60 : /// Per-tenant, per-pageserver configuration. All pageservers use the same TenantConf,
61 : /// but have distinct LocationConf.
62 0 : #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
63 : pub(crate) struct LocationConf {
64 : /// The location-specific part of the configuration, describes the operating
65 : /// mode of this pageserver for this tenant.
66 : pub(crate) mode: LocationMode,
67 :
68 : /// The detailed shard identity. This structure is already scoped within
69 : /// a TenantShardId, but we need the full ShardIdentity to enable calculating
70 : /// key->shard mappings.
71 : #[serde(default = "ShardIdentity::unsharded")]
72 : #[serde(skip_serializing_if = "ShardIdentity::is_unsharded")]
73 : pub(crate) shard: ShardIdentity,
74 :
75 : /// The pan-cluster tenant configuration, the same on all locations
76 : pub(crate) tenant_conf: TenantConfOpt,
77 : }
78 :
79 : impl std::fmt::Debug for LocationConf {
80 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 0 : match &self.mode {
82 0 : LocationMode::Attached(conf) => {
83 0 : write!(
84 0 : f,
85 0 : "Attached {:?}, gen={:?}",
86 0 : conf.attach_mode, conf.generation
87 0 : )
88 : }
89 0 : LocationMode::Secondary(conf) => {
90 0 : write!(f, "Secondary, warm={}", conf.warm)
91 : }
92 : }
93 0 : }
94 : }
95 :
96 : impl AttachedLocationConfig {
97 : /// Consult attachment mode to determine whether we are currently permitted
98 : /// to delete layers. This is only advisory, not required for data safety.
99 : /// See [`AttachmentMode`] for more context.
100 2420 : pub(crate) fn may_delete_layers_hint(&self) -> bool {
101 2420 : // TODO: add an override for disk pressure in AttachedLocationConfig,
102 2420 : // and respect it here.
103 2420 : match &self.attach_mode {
104 2420 : AttachmentMode::Single => true,
105 : AttachmentMode::Multi | AttachmentMode::Stale => {
106 : // In Multi mode we avoid doing deletions because some other
107 : // attached pageserver might get 404 while trying to read
108 : // a layer we delete which is still referenced in their metadata.
109 : //
110 : // In Stale mode, we avoid doing deletions because we expect
111 : // that they would ultimately fail validation in the deletion
112 : // queue due to our stale generation.
113 0 : false
114 : }
115 : }
116 2420 : }
117 :
118 : /// Whether we are currently hinted that it is worthwhile to upload layers.
119 : /// This is only advisory, not required for data safety.
120 : /// See [`AttachmentMode`] for more context.
121 912 : pub(crate) fn may_upload_layers_hint(&self) -> bool {
122 912 : // TODO: add an override for disk pressure in AttachedLocationConfig,
123 912 : // and respect it here.
124 912 : match &self.attach_mode {
125 912 : AttachmentMode::Single | AttachmentMode::Multi => true,
126 : AttachmentMode::Stale => {
127 : // In Stale mode, we avoid doing uploads because we expect that
128 : // our replacement pageserver will already have started its own
129 : // IndexPart that will never reference layers we upload: it is
130 : // wasteful.
131 0 : false
132 : }
133 : }
134 912 : }
135 : }
136 :
137 : impl LocationConf {
138 : /// For use when loading from a legacy configuration: presence of a tenant
139 : /// implies it is in AttachmentMode::Single, which used to be the only
140 : /// possible state. This function should eventually be removed.
141 440 : pub(crate) fn attached_single(
142 440 : tenant_conf: TenantConfOpt,
143 440 : generation: Generation,
144 440 : shard_params: &models::ShardParameters,
145 440 : ) -> Self {
146 440 : Self {
147 440 : mode: LocationMode::Attached(AttachedLocationConfig {
148 440 : generation,
149 440 : attach_mode: AttachmentMode::Single,
150 440 : }),
151 440 : shard: ShardIdentity::from_params(ShardNumber(0), shard_params),
152 440 : tenant_conf,
153 440 : }
154 440 : }
155 :
156 : /// For use when attaching/re-attaching: update the generation stored in this
157 : /// structure. If we were in a secondary state, promote to attached (posession
158 : /// of a fresh generation implies this).
159 0 : pub(crate) fn attach_in_generation(&mut self, mode: AttachmentMode, generation: Generation) {
160 0 : match &mut self.mode {
161 0 : LocationMode::Attached(attach_conf) => {
162 0 : attach_conf.generation = generation;
163 0 : attach_conf.attach_mode = mode;
164 0 : }
165 : LocationMode::Secondary(_) => {
166 : // We are promoted to attached by the control plane's re-attach response
167 0 : self.mode = LocationMode::Attached(AttachedLocationConfig {
168 0 : generation,
169 0 : attach_mode: mode,
170 0 : })
171 : }
172 : }
173 0 : }
174 :
175 0 : pub(crate) fn try_from(conf: &'_ models::LocationConfig) -> anyhow::Result<Self> {
176 0 : let tenant_conf = TenantConfOpt::try_from(&conf.tenant_conf)?;
177 :
178 0 : fn get_generation(conf: &'_ models::LocationConfig) -> Result<Generation, anyhow::Error> {
179 0 : conf.generation
180 0 : .map(Generation::new)
181 0 : .ok_or_else(|| anyhow::anyhow!("Generation must be set when attaching"))
182 0 : }
183 :
184 0 : let mode = match &conf.mode {
185 : models::LocationConfigMode::AttachedMulti => {
186 : LocationMode::Attached(AttachedLocationConfig {
187 0 : generation: get_generation(conf)?,
188 0 : attach_mode: AttachmentMode::Multi,
189 : })
190 : }
191 : models::LocationConfigMode::AttachedSingle => {
192 : LocationMode::Attached(AttachedLocationConfig {
193 0 : generation: get_generation(conf)?,
194 0 : attach_mode: AttachmentMode::Single,
195 : })
196 : }
197 : models::LocationConfigMode::AttachedStale => {
198 : LocationMode::Attached(AttachedLocationConfig {
199 0 : generation: get_generation(conf)?,
200 0 : attach_mode: AttachmentMode::Stale,
201 : })
202 : }
203 : models::LocationConfigMode::Secondary => {
204 0 : anyhow::ensure!(conf.generation.is_none());
205 :
206 0 : let warm = conf
207 0 : .secondary_conf
208 0 : .as_ref()
209 0 : .map(|c| c.warm)
210 0 : .unwrap_or(false);
211 0 : LocationMode::Secondary(SecondaryLocationConfig { warm })
212 : }
213 : models::LocationConfigMode::Detached => {
214 : // Should not have been called: API code should translate this mode
215 : // into a detach rather than trying to decode it as a LocationConf
216 0 : return Err(anyhow::anyhow!("Cannot decode a Detached configuration"));
217 : }
218 : };
219 :
220 0 : let shard = if conf.shard_count == 0 {
221 0 : ShardIdentity::unsharded()
222 : } else {
223 0 : ShardIdentity::new(
224 0 : ShardNumber(conf.shard_number),
225 0 : ShardCount::new(conf.shard_count),
226 0 : ShardStripeSize(conf.shard_stripe_size),
227 0 : )?
228 : };
229 :
230 0 : Ok(Self {
231 0 : shard,
232 0 : mode,
233 0 : tenant_conf,
234 0 : })
235 0 : }
236 : }
237 :
238 : impl Default for LocationConf {
239 : // TODO: this should be removed once tenant loading can guarantee that we are never
240 : // loading from a directory without a configuration.
241 : // => tech debt since https://github.com/neondatabase/neon/issues/1555
242 0 : fn default() -> Self {
243 0 : Self {
244 0 : mode: LocationMode::Attached(AttachedLocationConfig {
245 0 : generation: Generation::none(),
246 0 : attach_mode: AttachmentMode::Single,
247 0 : }),
248 0 : tenant_conf: TenantConfOpt::default(),
249 0 : shard: ShardIdentity::unsharded(),
250 0 : }
251 0 : }
252 : }
253 :
254 : /// Same as TenantConf, but this struct preserves the information about
255 : /// which parameters are set and which are not.
256 204 : #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
257 : pub struct TenantConfOpt {
258 : #[serde(skip_serializing_if = "Option::is_none")]
259 : #[serde(default)]
260 : pub checkpoint_distance: Option<u64>,
261 :
262 : #[serde(skip_serializing_if = "Option::is_none")]
263 : #[serde(with = "humantime_serde")]
264 : #[serde(default)]
265 : pub checkpoint_timeout: Option<Duration>,
266 :
267 : #[serde(skip_serializing_if = "Option::is_none")]
268 : #[serde(default)]
269 : pub compaction_target_size: Option<u64>,
270 :
271 : #[serde(skip_serializing_if = "Option::is_none")]
272 : #[serde(with = "humantime_serde")]
273 : #[serde(default)]
274 : pub compaction_period: Option<Duration>,
275 :
276 : #[serde(skip_serializing_if = "Option::is_none")]
277 : #[serde(default)]
278 : pub compaction_threshold: Option<usize>,
279 :
280 : #[serde(skip_serializing_if = "Option::is_none")]
281 : #[serde(default)]
282 : pub compaction_algorithm: Option<CompactionAlgorithmSettings>,
283 :
284 : #[serde(skip_serializing_if = "Option::is_none")]
285 : #[serde(default)]
286 : pub l0_flush_delay_threshold: Option<usize>,
287 :
288 : #[serde(skip_serializing_if = "Option::is_none")]
289 : #[serde(default)]
290 : pub l0_flush_stall_threshold: Option<usize>,
291 :
292 : #[serde(skip_serializing_if = "Option::is_none")]
293 : #[serde(default)]
294 : pub gc_horizon: Option<u64>,
295 :
296 : #[serde(skip_serializing_if = "Option::is_none")]
297 : #[serde(with = "humantime_serde")]
298 : #[serde(default)]
299 : pub gc_period: Option<Duration>,
300 :
301 : #[serde(skip_serializing_if = "Option::is_none")]
302 : #[serde(default)]
303 : pub image_creation_threshold: Option<usize>,
304 :
305 : #[serde(skip_serializing_if = "Option::is_none")]
306 : #[serde(with = "humantime_serde")]
307 : #[serde(default)]
308 : pub pitr_interval: Option<Duration>,
309 :
310 : #[serde(skip_serializing_if = "Option::is_none")]
311 : #[serde(with = "humantime_serde")]
312 : #[serde(default)]
313 : pub walreceiver_connect_timeout: Option<Duration>,
314 :
315 : #[serde(skip_serializing_if = "Option::is_none")]
316 : #[serde(with = "humantime_serde")]
317 : #[serde(default)]
318 : pub lagging_wal_timeout: Option<Duration>,
319 :
320 : #[serde(skip_serializing_if = "Option::is_none")]
321 : #[serde(default)]
322 : pub max_lsn_wal_lag: Option<NonZeroU64>,
323 :
324 : #[serde(skip_serializing_if = "Option::is_none")]
325 : #[serde(default)]
326 : pub eviction_policy: Option<EvictionPolicy>,
327 :
328 : #[serde(skip_serializing_if = "Option::is_none")]
329 : #[serde(default)]
330 : pub min_resident_size_override: Option<u64>,
331 :
332 : #[serde(skip_serializing_if = "Option::is_none")]
333 : #[serde(with = "humantime_serde")]
334 : #[serde(default)]
335 : pub evictions_low_residence_duration_metric_threshold: Option<Duration>,
336 :
337 : #[serde(skip_serializing_if = "Option::is_none")]
338 : #[serde(with = "humantime_serde")]
339 : #[serde(default)]
340 : pub heatmap_period: Option<Duration>,
341 :
342 : #[serde(skip_serializing_if = "Option::is_none")]
343 : #[serde(default)]
344 : pub lazy_slru_download: Option<bool>,
345 :
346 : #[serde(skip_serializing_if = "Option::is_none")]
347 : pub timeline_get_throttle: Option<pageserver_api::models::ThrottleConfig>,
348 :
349 : #[serde(skip_serializing_if = "Option::is_none")]
350 : pub image_layer_creation_check_threshold: Option<u8>,
351 :
352 : #[serde(skip_serializing_if = "Option::is_none")]
353 : #[serde(with = "humantime_serde")]
354 : #[serde(default)]
355 : pub lsn_lease_length: Option<Duration>,
356 :
357 : #[serde(skip_serializing_if = "Option::is_none")]
358 : #[serde(with = "humantime_serde")]
359 : #[serde(default)]
360 : pub lsn_lease_length_for_ts: Option<Duration>,
361 :
362 : #[serde(skip_serializing_if = "Option::is_none")]
363 : #[serde(default)]
364 : pub timeline_offloading: Option<bool>,
365 :
366 : #[serde(skip_serializing_if = "Option::is_none")]
367 : pub wal_receiver_protocol_override: Option<PostgresClientProtocol>,
368 :
369 : #[serde(skip_serializing_if = "Option::is_none")]
370 : pub rel_size_v2_enabled: Option<bool>,
371 :
372 : #[serde(skip_serializing_if = "Option::is_none")]
373 : pub gc_compaction_enabled: Option<bool>,
374 :
375 : #[serde(skip_serializing_if = "Option::is_none")]
376 : pub gc_compaction_initial_threshold_kb: Option<u64>,
377 :
378 : #[serde(skip_serializing_if = "Option::is_none")]
379 : pub gc_compaction_ratio_percent: Option<u64>,
380 : }
381 :
382 : impl TenantConfOpt {
383 0 : pub fn merge(&self, global_conf: TenantConf) -> TenantConf {
384 0 : TenantConf {
385 0 : checkpoint_distance: self
386 0 : .checkpoint_distance
387 0 : .unwrap_or(global_conf.checkpoint_distance),
388 0 : checkpoint_timeout: self
389 0 : .checkpoint_timeout
390 0 : .unwrap_or(global_conf.checkpoint_timeout),
391 0 : compaction_target_size: self
392 0 : .compaction_target_size
393 0 : .unwrap_or(global_conf.compaction_target_size),
394 0 : compaction_period: self
395 0 : .compaction_period
396 0 : .unwrap_or(global_conf.compaction_period),
397 0 : compaction_threshold: self
398 0 : .compaction_threshold
399 0 : .unwrap_or(global_conf.compaction_threshold),
400 0 : compaction_algorithm: self
401 0 : .compaction_algorithm
402 0 : .as_ref()
403 0 : .unwrap_or(&global_conf.compaction_algorithm)
404 0 : .clone(),
405 0 : l0_flush_delay_threshold: self
406 0 : .l0_flush_delay_threshold
407 0 : .or(global_conf.l0_flush_delay_threshold),
408 0 : l0_flush_stall_threshold: self
409 0 : .l0_flush_stall_threshold
410 0 : .or(global_conf.l0_flush_stall_threshold),
411 0 : gc_horizon: self.gc_horizon.unwrap_or(global_conf.gc_horizon),
412 0 : gc_period: self.gc_period.unwrap_or(global_conf.gc_period),
413 0 : image_creation_threshold: self
414 0 : .image_creation_threshold
415 0 : .unwrap_or(global_conf.image_creation_threshold),
416 0 : pitr_interval: self.pitr_interval.unwrap_or(global_conf.pitr_interval),
417 0 : walreceiver_connect_timeout: self
418 0 : .walreceiver_connect_timeout
419 0 : .unwrap_or(global_conf.walreceiver_connect_timeout),
420 0 : lagging_wal_timeout: self
421 0 : .lagging_wal_timeout
422 0 : .unwrap_or(global_conf.lagging_wal_timeout),
423 0 : max_lsn_wal_lag: self.max_lsn_wal_lag.unwrap_or(global_conf.max_lsn_wal_lag),
424 0 : eviction_policy: self.eviction_policy.unwrap_or(global_conf.eviction_policy),
425 0 : min_resident_size_override: self
426 0 : .min_resident_size_override
427 0 : .or(global_conf.min_resident_size_override),
428 0 : evictions_low_residence_duration_metric_threshold: self
429 0 : .evictions_low_residence_duration_metric_threshold
430 0 : .unwrap_or(global_conf.evictions_low_residence_duration_metric_threshold),
431 0 : heatmap_period: self.heatmap_period.unwrap_or(global_conf.heatmap_period),
432 0 : lazy_slru_download: self
433 0 : .lazy_slru_download
434 0 : .unwrap_or(global_conf.lazy_slru_download),
435 0 : timeline_get_throttle: self
436 0 : .timeline_get_throttle
437 0 : .clone()
438 0 : .unwrap_or(global_conf.timeline_get_throttle),
439 0 : image_layer_creation_check_threshold: self
440 0 : .image_layer_creation_check_threshold
441 0 : .unwrap_or(global_conf.image_layer_creation_check_threshold),
442 0 : lsn_lease_length: self
443 0 : .lsn_lease_length
444 0 : .unwrap_or(global_conf.lsn_lease_length),
445 0 : lsn_lease_length_for_ts: self
446 0 : .lsn_lease_length_for_ts
447 0 : .unwrap_or(global_conf.lsn_lease_length_for_ts),
448 0 : timeline_offloading: self
449 0 : .lazy_slru_download
450 0 : .unwrap_or(global_conf.timeline_offloading),
451 0 : wal_receiver_protocol_override: self
452 0 : .wal_receiver_protocol_override
453 0 : .or(global_conf.wal_receiver_protocol_override),
454 0 : rel_size_v2_enabled: self.rel_size_v2_enabled.or(global_conf.rel_size_v2_enabled),
455 0 : gc_compaction_enabled: self
456 0 : .gc_compaction_enabled
457 0 : .unwrap_or(global_conf.gc_compaction_enabled),
458 0 : gc_compaction_initial_threshold_kb: self
459 0 : .gc_compaction_initial_threshold_kb
460 0 : .unwrap_or(global_conf.gc_compaction_initial_threshold_kb),
461 0 : gc_compaction_ratio_percent: self
462 0 : .gc_compaction_ratio_percent
463 0 : .unwrap_or(global_conf.gc_compaction_ratio_percent),
464 0 : }
465 0 : }
466 :
467 0 : pub fn apply_patch(self, patch: TenantConfigPatch) -> anyhow::Result<TenantConfOpt> {
468 0 : let Self {
469 0 : mut checkpoint_distance,
470 0 : mut checkpoint_timeout,
471 0 : mut compaction_target_size,
472 0 : mut compaction_period,
473 0 : mut compaction_threshold,
474 0 : mut compaction_algorithm,
475 0 : mut l0_flush_delay_threshold,
476 0 : mut l0_flush_stall_threshold,
477 0 : mut gc_horizon,
478 0 : mut gc_period,
479 0 : mut image_creation_threshold,
480 0 : mut pitr_interval,
481 0 : mut walreceiver_connect_timeout,
482 0 : mut lagging_wal_timeout,
483 0 : mut max_lsn_wal_lag,
484 0 : mut eviction_policy,
485 0 : mut min_resident_size_override,
486 0 : mut evictions_low_residence_duration_metric_threshold,
487 0 : mut heatmap_period,
488 0 : mut lazy_slru_download,
489 0 : mut timeline_get_throttle,
490 0 : mut image_layer_creation_check_threshold,
491 0 : mut lsn_lease_length,
492 0 : mut lsn_lease_length_for_ts,
493 0 : mut timeline_offloading,
494 0 : mut wal_receiver_protocol_override,
495 0 : mut rel_size_v2_enabled,
496 0 : mut gc_compaction_enabled,
497 0 : mut gc_compaction_initial_threshold_kb,
498 0 : mut gc_compaction_ratio_percent,
499 0 : } = self;
500 0 :
501 0 : patch.checkpoint_distance.apply(&mut checkpoint_distance);
502 0 : patch
503 0 : .checkpoint_timeout
504 0 : .map(|v| humantime::parse_duration(&v))?
505 0 : .apply(&mut checkpoint_timeout);
506 0 : patch
507 0 : .compaction_target_size
508 0 : .apply(&mut compaction_target_size);
509 0 : patch
510 0 : .compaction_period
511 0 : .map(|v| humantime::parse_duration(&v))?
512 0 : .apply(&mut compaction_period);
513 0 : patch.compaction_threshold.apply(&mut compaction_threshold);
514 0 : patch.compaction_algorithm.apply(&mut compaction_algorithm);
515 0 : patch
516 0 : .l0_flush_delay_threshold
517 0 : .apply(&mut l0_flush_delay_threshold);
518 0 : patch
519 0 : .l0_flush_stall_threshold
520 0 : .apply(&mut l0_flush_stall_threshold);
521 0 : patch.gc_horizon.apply(&mut gc_horizon);
522 0 : patch
523 0 : .gc_period
524 0 : .map(|v| humantime::parse_duration(&v))?
525 0 : .apply(&mut gc_period);
526 0 : patch
527 0 : .image_creation_threshold
528 0 : .apply(&mut image_creation_threshold);
529 0 : patch
530 0 : .pitr_interval
531 0 : .map(|v| humantime::parse_duration(&v))?
532 0 : .apply(&mut pitr_interval);
533 0 : patch
534 0 : .walreceiver_connect_timeout
535 0 : .map(|v| humantime::parse_duration(&v))?
536 0 : .apply(&mut walreceiver_connect_timeout);
537 0 : patch
538 0 : .lagging_wal_timeout
539 0 : .map(|v| humantime::parse_duration(&v))?
540 0 : .apply(&mut lagging_wal_timeout);
541 0 : patch.max_lsn_wal_lag.apply(&mut max_lsn_wal_lag);
542 0 : patch.eviction_policy.apply(&mut eviction_policy);
543 0 : patch
544 0 : .min_resident_size_override
545 0 : .apply(&mut min_resident_size_override);
546 0 : patch
547 0 : .evictions_low_residence_duration_metric_threshold
548 0 : .map(|v| humantime::parse_duration(&v))?
549 0 : .apply(&mut evictions_low_residence_duration_metric_threshold);
550 0 : patch
551 0 : .heatmap_period
552 0 : .map(|v| humantime::parse_duration(&v))?
553 0 : .apply(&mut heatmap_period);
554 0 : patch.lazy_slru_download.apply(&mut lazy_slru_download);
555 0 : patch
556 0 : .timeline_get_throttle
557 0 : .apply(&mut timeline_get_throttle);
558 0 : patch
559 0 : .image_layer_creation_check_threshold
560 0 : .apply(&mut image_layer_creation_check_threshold);
561 0 : patch
562 0 : .lsn_lease_length
563 0 : .map(|v| humantime::parse_duration(&v))?
564 0 : .apply(&mut lsn_lease_length);
565 0 : patch
566 0 : .lsn_lease_length_for_ts
567 0 : .map(|v| humantime::parse_duration(&v))?
568 0 : .apply(&mut lsn_lease_length_for_ts);
569 0 : patch.timeline_offloading.apply(&mut timeline_offloading);
570 0 : patch
571 0 : .wal_receiver_protocol_override
572 0 : .apply(&mut wal_receiver_protocol_override);
573 0 : patch.rel_size_v2_enabled.apply(&mut rel_size_v2_enabled);
574 0 : patch
575 0 : .gc_compaction_enabled
576 0 : .apply(&mut gc_compaction_enabled);
577 0 : patch
578 0 : .gc_compaction_initial_threshold_kb
579 0 : .apply(&mut gc_compaction_initial_threshold_kb);
580 0 : patch
581 0 : .gc_compaction_ratio_percent
582 0 : .apply(&mut gc_compaction_ratio_percent);
583 0 :
584 0 : Ok(Self {
585 0 : checkpoint_distance,
586 0 : checkpoint_timeout,
587 0 : compaction_target_size,
588 0 : compaction_period,
589 0 : compaction_threshold,
590 0 : compaction_algorithm,
591 0 : l0_flush_delay_threshold,
592 0 : l0_flush_stall_threshold,
593 0 : gc_horizon,
594 0 : gc_period,
595 0 : image_creation_threshold,
596 0 : pitr_interval,
597 0 : walreceiver_connect_timeout,
598 0 : lagging_wal_timeout,
599 0 : max_lsn_wal_lag,
600 0 : eviction_policy,
601 0 : min_resident_size_override,
602 0 : evictions_low_residence_duration_metric_threshold,
603 0 : heatmap_period,
604 0 : lazy_slru_download,
605 0 : timeline_get_throttle,
606 0 : image_layer_creation_check_threshold,
607 0 : lsn_lease_length,
608 0 : lsn_lease_length_for_ts,
609 0 : timeline_offloading,
610 0 : wal_receiver_protocol_override,
611 0 : rel_size_v2_enabled,
612 0 : gc_compaction_enabled,
613 0 : gc_compaction_initial_threshold_kb,
614 0 : gc_compaction_ratio_percent,
615 0 : })
616 0 : }
617 : }
618 :
619 : impl TryFrom<&'_ models::TenantConfig> for TenantConfOpt {
620 : type Error = anyhow::Error;
621 :
622 8 : fn try_from(request_data: &'_ models::TenantConfig) -> Result<Self, Self::Error> {
623 : // Convert the request_data to a JSON Value
624 8 : let json_value: Value = serde_json::to_value(request_data)?;
625 :
626 : // Create a Deserializer from the JSON Value
627 8 : let deserializer = json_value.into_deserializer();
628 :
629 : // Use serde_path_to_error to deserialize the JSON Value into TenantConfOpt
630 8 : let tenant_conf: TenantConfOpt = serde_path_to_error::deserialize(deserializer)?;
631 :
632 4 : Ok(tenant_conf)
633 8 : }
634 : }
635 :
636 : /// This is a conversion from our internal tenant config object to the one used
637 : /// in external APIs.
638 : impl From<TenantConfOpt> for models::TenantConfig {
639 0 : fn from(value: TenantConfOpt) -> Self {
640 0 : fn humantime(d: Duration) -> String {
641 0 : format!("{}s", d.as_secs())
642 0 : }
643 0 : Self {
644 0 : checkpoint_distance: value.checkpoint_distance,
645 0 : checkpoint_timeout: value.checkpoint_timeout.map(humantime),
646 0 : compaction_algorithm: value.compaction_algorithm,
647 0 : compaction_target_size: value.compaction_target_size,
648 0 : compaction_period: value.compaction_period.map(humantime),
649 0 : compaction_threshold: value.compaction_threshold,
650 0 : l0_flush_delay_threshold: value.l0_flush_delay_threshold,
651 0 : l0_flush_stall_threshold: value.l0_flush_stall_threshold,
652 0 : gc_horizon: value.gc_horizon,
653 0 : gc_period: value.gc_period.map(humantime),
654 0 : image_creation_threshold: value.image_creation_threshold,
655 0 : pitr_interval: value.pitr_interval.map(humantime),
656 0 : walreceiver_connect_timeout: value.walreceiver_connect_timeout.map(humantime),
657 0 : lagging_wal_timeout: value.lagging_wal_timeout.map(humantime),
658 0 : max_lsn_wal_lag: value.max_lsn_wal_lag,
659 0 : eviction_policy: value.eviction_policy,
660 0 : min_resident_size_override: value.min_resident_size_override,
661 0 : evictions_low_residence_duration_metric_threshold: value
662 0 : .evictions_low_residence_duration_metric_threshold
663 0 : .map(humantime),
664 0 : heatmap_period: value.heatmap_period.map(humantime),
665 0 : lazy_slru_download: value.lazy_slru_download,
666 0 : timeline_get_throttle: value.timeline_get_throttle,
667 0 : image_layer_creation_check_threshold: value.image_layer_creation_check_threshold,
668 0 : lsn_lease_length: value.lsn_lease_length.map(humantime),
669 0 : lsn_lease_length_for_ts: value.lsn_lease_length_for_ts.map(humantime),
670 0 : timeline_offloading: value.timeline_offloading,
671 0 : wal_receiver_protocol_override: value.wal_receiver_protocol_override,
672 0 : rel_size_v2_enabled: value.rel_size_v2_enabled,
673 0 : gc_compaction_enabled: value.gc_compaction_enabled,
674 0 : gc_compaction_initial_threshold_kb: value.gc_compaction_initial_threshold_kb,
675 0 : gc_compaction_ratio_percent: value.gc_compaction_ratio_percent,
676 0 : }
677 0 : }
678 : }
679 :
680 : #[cfg(test)]
681 : mod tests {
682 : use super::*;
683 : use models::TenantConfig;
684 :
685 : #[test]
686 4 : fn de_serializing_pageserver_config_omits_empty_values() {
687 4 : let small_conf = TenantConfOpt {
688 4 : gc_horizon: Some(42),
689 4 : ..TenantConfOpt::default()
690 4 : };
691 4 :
692 4 : let toml_form = toml_edit::ser::to_string(&small_conf).unwrap();
693 4 : assert_eq!(toml_form, "gc_horizon = 42\n");
694 4 : assert_eq!(small_conf, toml_edit::de::from_str(&toml_form).unwrap());
695 :
696 4 : let json_form = serde_json::to_string(&small_conf).unwrap();
697 4 : assert_eq!(json_form, "{\"gc_horizon\":42}");
698 4 : assert_eq!(small_conf, serde_json::from_str(&json_form).unwrap());
699 4 : }
700 :
701 : #[test]
702 4 : fn test_try_from_models_tenant_config_err() {
703 4 : let tenant_config = models::TenantConfig {
704 4 : lagging_wal_timeout: Some("5a".to_string()),
705 4 : ..TenantConfig::default()
706 4 : };
707 4 :
708 4 : let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config);
709 4 :
710 4 : assert!(
711 4 : tenant_conf_opt.is_err(),
712 0 : "Suceeded to convert TenantConfig to TenantConfOpt"
713 : );
714 :
715 4 : let expected_error_str =
716 4 : "lagging_wal_timeout: invalid value: string \"5a\", expected a duration";
717 4 : assert_eq!(tenant_conf_opt.unwrap_err().to_string(), expected_error_str);
718 4 : }
719 :
720 : #[test]
721 4 : fn test_try_from_models_tenant_config_success() {
722 4 : let tenant_config = models::TenantConfig {
723 4 : lagging_wal_timeout: Some("5s".to_string()),
724 4 : ..TenantConfig::default()
725 4 : };
726 4 :
727 4 : let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config).unwrap();
728 4 :
729 4 : assert_eq!(
730 4 : tenant_conf_opt.lagging_wal_timeout,
731 4 : Some(Duration::from_secs(5))
732 4 : );
733 4 : }
734 : }
|