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 : use anyhow::bail;
12 : use pageserver_api::models::EvictionPolicy;
13 : use pageserver_api::models::{self, ThrottleConfig};
14 : use pageserver_api::shard::{ShardCount, ShardIdentity, ShardNumber, ShardStripeSize};
15 : use serde::de::IntoDeserializer;
16 : use serde::{Deserialize, Serialize};
17 : use serde_json::Value;
18 : use std::num::NonZeroU64;
19 : use std::time::Duration;
20 : use utils::generation::Generation;
21 :
22 : pub mod defaults {
23 : // FIXME: This current value is very low. I would imagine something like 1 GB or 10 GB
24 : // would be more appropriate. But a low value forces the code to be exercised more,
25 : // which is good for now to trigger bugs.
26 : // This parameter actually determines L0 layer file size.
27 : pub const DEFAULT_CHECKPOINT_DISTANCE: u64 = 256 * 1024 * 1024;
28 : pub const DEFAULT_CHECKPOINT_TIMEOUT: &str = "10 m";
29 :
30 : // Target file size, when creating image and delta layers.
31 : // This parameter determines L1 layer file size.
32 : pub const DEFAULT_COMPACTION_TARGET_SIZE: u64 = 128 * 1024 * 1024;
33 :
34 : pub const DEFAULT_COMPACTION_PERIOD: &str = "20 s";
35 : pub const DEFAULT_COMPACTION_THRESHOLD: usize = 10;
36 :
37 : pub const DEFAULT_GC_HORIZON: u64 = 64 * 1024 * 1024;
38 :
39 : // Large DEFAULT_GC_PERIOD is fine as long as PITR_INTERVAL is larger.
40 : // If there's a need to decrease this value, first make sure that GC
41 : // doesn't hold a layer map write lock for non-trivial operations.
42 : // Relevant: https://github.com/neondatabase/neon/issues/3394
43 : pub const DEFAULT_GC_PERIOD: &str = "1 hr";
44 : pub const DEFAULT_IMAGE_CREATION_THRESHOLD: usize = 3;
45 : pub const DEFAULT_PITR_INTERVAL: &str = "7 days";
46 : pub const DEFAULT_WALRECEIVER_CONNECT_TIMEOUT: &str = "10 seconds";
47 : pub const DEFAULT_WALRECEIVER_LAGGING_WAL_TIMEOUT: &str = "10 seconds";
48 : pub const DEFAULT_MAX_WALRECEIVER_LSN_WAL_LAG: u64 = 10 * 1024 * 1024;
49 : pub const DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD: &str = "24 hour";
50 :
51 : pub const DEFAULT_INGEST_BATCH_SIZE: u64 = 100;
52 : }
53 :
54 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
55 : pub(crate) enum AttachmentMode {
56 : /// Our generation is current as far as we know, and as far as we know we are the only attached
57 : /// pageserver. This is the "normal" attachment mode.
58 : Single,
59 : /// Our generation number is current as far as we know, but we are advised that another
60 : /// pageserver is still attached, and therefore to avoid executing deletions. This is
61 : /// the attachment mode of a pagesever that is the destination of a migration.
62 : Multi,
63 : /// Our generation number is superseded, or about to be superseded. We are advised
64 : /// to avoid remote storage writes if possible, and to avoid sending billing data. This
65 : /// is the attachment mode of a pageserver that is the origin of a migration.
66 : Stale,
67 : }
68 :
69 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
70 : pub(crate) struct AttachedLocationConfig {
71 : pub(crate) generation: Generation,
72 : pub(crate) attach_mode: AttachmentMode,
73 : // TODO: add a flag to override AttachmentMode's policies under
74 : // disk pressure (i.e. unblock uploads under disk pressure in Stale
75 : // state, unblock deletions after timeout in Multi state)
76 : }
77 :
78 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79 : pub(crate) struct SecondaryLocationConfig {
80 : /// If true, keep the local cache warm by polling remote storage
81 : pub(crate) warm: bool,
82 : }
83 :
84 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85 : pub(crate) enum LocationMode {
86 : Attached(AttachedLocationConfig),
87 : Secondary(SecondaryLocationConfig),
88 : }
89 :
90 : /// Per-tenant, per-pageserver configuration. All pageservers use the same TenantConf,
91 : /// but have distinct LocationConf.
92 0 : #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
93 : pub(crate) struct LocationConf {
94 : /// The location-specific part of the configuration, describes the operating
95 : /// mode of this pageserver for this tenant.
96 : pub(crate) mode: LocationMode,
97 :
98 : /// The detailed shard identity. This structure is already scoped within
99 : /// a TenantShardId, but we need the full ShardIdentity to enable calculating
100 : /// key->shard mappings.
101 : #[serde(default = "ShardIdentity::unsharded")]
102 : #[serde(skip_serializing_if = "ShardIdentity::is_unsharded")]
103 : pub(crate) shard: ShardIdentity,
104 :
105 : /// The pan-cluster tenant configuration, the same on all locations
106 : pub(crate) tenant_conf: TenantConfOpt,
107 : }
108 :
109 : impl std::fmt::Debug for LocationConf {
110 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 0 : match &self.mode {
112 0 : LocationMode::Attached(conf) => {
113 0 : write!(
114 0 : f,
115 0 : "Attached {:?}, gen={:?}",
116 0 : conf.attach_mode, conf.generation
117 0 : )
118 : }
119 0 : LocationMode::Secondary(conf) => {
120 0 : write!(f, "Secondary, warm={}", conf.warm)
121 : }
122 : }
123 0 : }
124 : }
125 :
126 : impl AttachedLocationConfig {
127 : /// Consult attachment mode to determine whether we are currently permitted
128 : /// to delete layers. This is only advisory, not required for data safety.
129 : /// See [`AttachmentMode`] for more context.
130 8 : pub(crate) fn may_delete_layers_hint(&self) -> bool {
131 8 : // TODO: add an override for disk pressure in AttachedLocationConfig,
132 8 : // and respect it here.
133 8 : match &self.attach_mode {
134 8 : AttachmentMode::Single => true,
135 : AttachmentMode::Multi | AttachmentMode::Stale => {
136 : // In Multi mode we avoid doing deletions because some other
137 : // attached pageserver might get 404 while trying to read
138 : // a layer we delete which is still referenced in their metadata.
139 : //
140 : // In Stale mode, we avoid doing deletions because we expect
141 : // that they would ultimately fail validation in the deletion
142 : // queue due to our stale generation.
143 0 : false
144 : }
145 : }
146 8 : }
147 :
148 : /// Whether we are currently hinted that it is worthwhile to upload layers.
149 : /// This is only advisory, not required for data safety.
150 : /// See [`AttachmentMode`] for more context.
151 0 : pub(crate) fn may_upload_layers_hint(&self) -> bool {
152 0 : // TODO: add an override for disk pressure in AttachedLocationConfig,
153 0 : // and respect it here.
154 0 : match &self.attach_mode {
155 0 : AttachmentMode::Single | AttachmentMode::Multi => true,
156 : AttachmentMode::Stale => {
157 : // In Stale mode, we avoid doing uploads because we expect that
158 : // our replacement pageserver will already have started its own
159 : // IndexPart that will never reference layers we upload: it is
160 : // wasteful.
161 0 : false
162 : }
163 : }
164 0 : }
165 : }
166 :
167 : impl LocationConf {
168 : /// For use when loading from a legacy configuration: presence of a tenant
169 : /// implies it is in AttachmentMode::Single, which used to be the only
170 : /// possible state. This function should eventually be removed.
171 84 : pub(crate) fn attached_single(
172 84 : tenant_conf: TenantConfOpt,
173 84 : generation: Generation,
174 84 : shard_params: &models::ShardParameters,
175 84 : ) -> Self {
176 84 : Self {
177 84 : mode: LocationMode::Attached(AttachedLocationConfig {
178 84 : generation,
179 84 : attach_mode: AttachmentMode::Single,
180 84 : }),
181 84 : shard: ShardIdentity::from_params(ShardNumber(0), shard_params),
182 84 : tenant_conf,
183 84 : }
184 84 : }
185 :
186 : /// For use when attaching/re-attaching: update the generation stored in this
187 : /// structure. If we were in a secondary state, promote to attached (posession
188 : /// of a fresh generation implies this).
189 0 : pub(crate) fn attach_in_generation(&mut self, generation: Generation) {
190 0 : match &mut self.mode {
191 0 : LocationMode::Attached(attach_conf) => {
192 0 : attach_conf.generation = generation;
193 0 : }
194 : LocationMode::Secondary(_) => {
195 : // We are promoted to attached by the control plane's re-attach response
196 0 : self.mode = LocationMode::Attached(AttachedLocationConfig {
197 0 : generation,
198 0 : attach_mode: AttachmentMode::Single,
199 0 : })
200 : }
201 : }
202 0 : }
203 :
204 0 : pub(crate) fn try_from(conf: &'_ models::LocationConfig) -> anyhow::Result<Self> {
205 0 : let tenant_conf = TenantConfOpt::try_from(&conf.tenant_conf)?;
206 :
207 0 : fn get_generation(conf: &'_ models::LocationConfig) -> Result<Generation, anyhow::Error> {
208 0 : conf.generation
209 0 : .map(Generation::new)
210 0 : .ok_or_else(|| anyhow::anyhow!("Generation must be set when attaching"))
211 0 : }
212 :
213 0 : let mode = match &conf.mode {
214 : models::LocationConfigMode::AttachedMulti => {
215 : LocationMode::Attached(AttachedLocationConfig {
216 0 : generation: get_generation(conf)?,
217 0 : attach_mode: AttachmentMode::Multi,
218 : })
219 : }
220 : models::LocationConfigMode::AttachedSingle => {
221 : LocationMode::Attached(AttachedLocationConfig {
222 0 : generation: get_generation(conf)?,
223 0 : attach_mode: AttachmentMode::Single,
224 : })
225 : }
226 : models::LocationConfigMode::AttachedStale => {
227 : LocationMode::Attached(AttachedLocationConfig {
228 0 : generation: get_generation(conf)?,
229 0 : attach_mode: AttachmentMode::Stale,
230 : })
231 : }
232 : models::LocationConfigMode::Secondary => {
233 0 : anyhow::ensure!(conf.generation.is_none());
234 :
235 0 : let warm = conf
236 0 : .secondary_conf
237 0 : .as_ref()
238 0 : .map(|c| c.warm)
239 0 : .unwrap_or(false);
240 0 : LocationMode::Secondary(SecondaryLocationConfig { warm })
241 : }
242 : models::LocationConfigMode::Detached => {
243 : // Should not have been called: API code should translate this mode
244 : // into a detach rather than trying to decode it as a LocationConf
245 0 : return Err(anyhow::anyhow!("Cannot decode a Detached configuration"));
246 : }
247 : };
248 :
249 0 : let shard = if conf.shard_count == 0 {
250 0 : ShardIdentity::unsharded()
251 : } else {
252 0 : ShardIdentity::new(
253 0 : ShardNumber(conf.shard_number),
254 0 : ShardCount::new(conf.shard_count),
255 0 : ShardStripeSize(conf.shard_stripe_size),
256 0 : )?
257 : };
258 :
259 0 : Ok(Self {
260 0 : shard,
261 0 : mode,
262 0 : tenant_conf,
263 0 : })
264 0 : }
265 : }
266 :
267 : impl Default for LocationConf {
268 : // TODO: this should be removed once tenant loading can guarantee that we are never
269 : // loading from a directory without a configuration.
270 : // => tech debt since https://github.com/neondatabase/neon/issues/1555
271 0 : fn default() -> Self {
272 0 : Self {
273 0 : mode: LocationMode::Attached(AttachedLocationConfig {
274 0 : generation: Generation::none(),
275 0 : attach_mode: AttachmentMode::Single,
276 0 : }),
277 0 : tenant_conf: TenantConfOpt::default(),
278 0 : shard: ShardIdentity::unsharded(),
279 0 : }
280 0 : }
281 : }
282 :
283 : /// A tenant's calcuated configuration, which is the result of merging a
284 : /// tenant's TenantConfOpt with the global TenantConf from PageServerConf.
285 : ///
286 : /// For storing and transmitting individual tenant's configuration, see
287 : /// TenantConfOpt.
288 84 : #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289 : pub struct TenantConf {
290 : // Flush out an inmemory layer, if it's holding WAL older than this
291 : // This puts a backstop on how much WAL needs to be re-digested if the
292 : // page server crashes.
293 : // This parameter actually determines L0 layer file size.
294 : pub checkpoint_distance: u64,
295 : // Inmemory layer is also flushed at least once in checkpoint_timeout to
296 : // eventually upload WAL after activity is stopped.
297 : #[serde(with = "humantime_serde")]
298 : pub checkpoint_timeout: Duration,
299 : // Target file size, when creating image and delta layers.
300 : // This parameter determines L1 layer file size.
301 : pub compaction_target_size: u64,
302 : // How often to check if there's compaction work to be done.
303 : // Duration::ZERO means automatic compaction is disabled.
304 : #[serde(with = "humantime_serde")]
305 : pub compaction_period: Duration,
306 : // Level0 delta layer threshold for compaction.
307 : pub compaction_threshold: usize,
308 : // Determines how much history is retained, to allow
309 : // branching and read replicas at an older point in time.
310 : // The unit is #of bytes of WAL.
311 : // Page versions older than this are garbage collected away.
312 : pub gc_horizon: u64,
313 : // Interval at which garbage collection is triggered.
314 : // Duration::ZERO means automatic GC is disabled
315 : #[serde(with = "humantime_serde")]
316 : pub gc_period: Duration,
317 : // Delta layer churn threshold to create L1 image layers.
318 : pub image_creation_threshold: usize,
319 : // Determines how much history is retained, to allow
320 : // branching and read replicas at an older point in time.
321 : // The unit is time.
322 : // Page versions older than this are garbage collected away.
323 : #[serde(with = "humantime_serde")]
324 : pub pitr_interval: Duration,
325 : /// Maximum amount of time to wait while opening a connection to receive wal, before erroring.
326 : #[serde(with = "humantime_serde")]
327 : pub walreceiver_connect_timeout: Duration,
328 : /// Considers safekeepers stalled after no WAL updates were received longer than this threshold.
329 : /// A stalled safekeeper will be changed to a newer one when it appears.
330 : #[serde(with = "humantime_serde")]
331 : pub lagging_wal_timeout: Duration,
332 : /// Considers safekeepers lagging when their WAL is behind another safekeeper for more than this threshold.
333 : /// A lagging safekeeper will be changed after `lagging_wal_timeout` time elapses since the last WAL update,
334 : /// to avoid eager reconnects.
335 : pub max_lsn_wal_lag: NonZeroU64,
336 : pub trace_read_requests: bool,
337 : pub eviction_policy: EvictionPolicy,
338 : pub min_resident_size_override: Option<u64>,
339 : // See the corresponding metric's help string.
340 : #[serde(with = "humantime_serde")]
341 : pub evictions_low_residence_duration_metric_threshold: Duration,
342 : pub gc_feedback: bool,
343 :
344 : /// If non-zero, the period between uploads of a heatmap from attached tenants. This
345 : /// may be disabled if a Tenant will not have secondary locations: only secondary
346 : /// locations will use the heatmap uploaded by attached locations.
347 : pub heatmap_period: Duration,
348 :
349 : /// If true then SLRU segments are dowloaded on demand, if false SLRU segments are included in basebackup
350 : pub lazy_slru_download: bool,
351 :
352 : pub timeline_get_throttle: pageserver_api::models::ThrottleConfig,
353 : }
354 :
355 : /// Same as TenantConf, but this struct preserves the information about
356 : /// which parameters are set and which are not.
357 4122 : #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
358 : pub struct TenantConfOpt {
359 : #[serde(skip_serializing_if = "Option::is_none")]
360 : #[serde(default)]
361 : pub checkpoint_distance: Option<u64>,
362 :
363 : #[serde(skip_serializing_if = "Option::is_none")]
364 : #[serde(with = "humantime_serde")]
365 : #[serde(default)]
366 : pub checkpoint_timeout: Option<Duration>,
367 :
368 : #[serde(skip_serializing_if = "Option::is_none")]
369 : #[serde(default)]
370 : pub compaction_target_size: Option<u64>,
371 :
372 : #[serde(skip_serializing_if = "Option::is_none")]
373 : #[serde(with = "humantime_serde")]
374 : #[serde(default)]
375 : pub compaction_period: Option<Duration>,
376 :
377 : #[serde(skip_serializing_if = "Option::is_none")]
378 : #[serde(default)]
379 : pub compaction_threshold: Option<usize>,
380 :
381 : #[serde(skip_serializing_if = "Option::is_none")]
382 : #[serde(default)]
383 : pub gc_horizon: Option<u64>,
384 :
385 : #[serde(skip_serializing_if = "Option::is_none")]
386 : #[serde(with = "humantime_serde")]
387 : #[serde(default)]
388 : pub gc_period: Option<Duration>,
389 :
390 : #[serde(skip_serializing_if = "Option::is_none")]
391 : #[serde(default)]
392 : pub image_creation_threshold: Option<usize>,
393 :
394 : #[serde(skip_serializing_if = "Option::is_none")]
395 : #[serde(with = "humantime_serde")]
396 : #[serde(default)]
397 : pub pitr_interval: Option<Duration>,
398 :
399 : #[serde(skip_serializing_if = "Option::is_none")]
400 : #[serde(with = "humantime_serde")]
401 : #[serde(default)]
402 : pub walreceiver_connect_timeout: Option<Duration>,
403 :
404 : #[serde(skip_serializing_if = "Option::is_none")]
405 : #[serde(with = "humantime_serde")]
406 : #[serde(default)]
407 : pub lagging_wal_timeout: Option<Duration>,
408 :
409 : #[serde(skip_serializing_if = "Option::is_none")]
410 : #[serde(default)]
411 : pub max_lsn_wal_lag: Option<NonZeroU64>,
412 :
413 : #[serde(skip_serializing_if = "Option::is_none")]
414 : #[serde(default)]
415 : pub trace_read_requests: Option<bool>,
416 :
417 : #[serde(skip_serializing_if = "Option::is_none")]
418 : #[serde(default)]
419 : pub eviction_policy: Option<EvictionPolicy>,
420 :
421 : #[serde(skip_serializing_if = "Option::is_none")]
422 : #[serde(default)]
423 : pub min_resident_size_override: Option<u64>,
424 :
425 : #[serde(skip_serializing_if = "Option::is_none")]
426 : #[serde(with = "humantime_serde")]
427 : #[serde(default)]
428 : pub evictions_low_residence_duration_metric_threshold: Option<Duration>,
429 :
430 : #[serde(skip_serializing_if = "Option::is_none")]
431 : #[serde(default)]
432 : pub gc_feedback: Option<bool>,
433 :
434 : #[serde(skip_serializing_if = "Option::is_none")]
435 : #[serde(with = "humantime_serde")]
436 : #[serde(default)]
437 : pub heatmap_period: Option<Duration>,
438 :
439 : #[serde(skip_serializing_if = "Option::is_none")]
440 : #[serde(default)]
441 : pub lazy_slru_download: Option<bool>,
442 :
443 : #[serde(skip_serializing_if = "Option::is_none")]
444 : pub timeline_get_throttle: Option<pageserver_api::models::ThrottleConfig>,
445 : }
446 :
447 : impl TenantConfOpt {
448 18 : pub fn merge(&self, global_conf: TenantConf) -> TenantConf {
449 18 : TenantConf {
450 18 : checkpoint_distance: self
451 18 : .checkpoint_distance
452 18 : .unwrap_or(global_conf.checkpoint_distance),
453 18 : checkpoint_timeout: self
454 18 : .checkpoint_timeout
455 18 : .unwrap_or(global_conf.checkpoint_timeout),
456 18 : compaction_target_size: self
457 18 : .compaction_target_size
458 18 : .unwrap_or(global_conf.compaction_target_size),
459 18 : compaction_period: self
460 18 : .compaction_period
461 18 : .unwrap_or(global_conf.compaction_period),
462 18 : compaction_threshold: self
463 18 : .compaction_threshold
464 18 : .unwrap_or(global_conf.compaction_threshold),
465 18 : gc_horizon: self.gc_horizon.unwrap_or(global_conf.gc_horizon),
466 18 : gc_period: self.gc_period.unwrap_or(global_conf.gc_period),
467 18 : image_creation_threshold: self
468 18 : .image_creation_threshold
469 18 : .unwrap_or(global_conf.image_creation_threshold),
470 18 : pitr_interval: self.pitr_interval.unwrap_or(global_conf.pitr_interval),
471 18 : walreceiver_connect_timeout: self
472 18 : .walreceiver_connect_timeout
473 18 : .unwrap_or(global_conf.walreceiver_connect_timeout),
474 18 : lagging_wal_timeout: self
475 18 : .lagging_wal_timeout
476 18 : .unwrap_or(global_conf.lagging_wal_timeout),
477 18 : max_lsn_wal_lag: self.max_lsn_wal_lag.unwrap_or(global_conf.max_lsn_wal_lag),
478 18 : trace_read_requests: self
479 18 : .trace_read_requests
480 18 : .unwrap_or(global_conf.trace_read_requests),
481 18 : eviction_policy: self.eviction_policy.unwrap_or(global_conf.eviction_policy),
482 18 : min_resident_size_override: self
483 18 : .min_resident_size_override
484 18 : .or(global_conf.min_resident_size_override),
485 18 : evictions_low_residence_duration_metric_threshold: self
486 18 : .evictions_low_residence_duration_metric_threshold
487 18 : .unwrap_or(global_conf.evictions_low_residence_duration_metric_threshold),
488 18 : gc_feedback: self.gc_feedback.unwrap_or(global_conf.gc_feedback),
489 18 : heatmap_period: self.heatmap_period.unwrap_or(global_conf.heatmap_period),
490 18 : lazy_slru_download: self
491 18 : .lazy_slru_download
492 18 : .unwrap_or(global_conf.lazy_slru_download),
493 18 : timeline_get_throttle: self
494 18 : .timeline_get_throttle
495 18 : .clone()
496 18 : .unwrap_or(global_conf.timeline_get_throttle),
497 18 : }
498 18 : }
499 : }
500 :
501 : impl Default for TenantConf {
502 216 : fn default() -> Self {
503 216 : use defaults::*;
504 216 : Self {
505 216 : checkpoint_distance: DEFAULT_CHECKPOINT_DISTANCE,
506 216 : checkpoint_timeout: humantime::parse_duration(DEFAULT_CHECKPOINT_TIMEOUT)
507 216 : .expect("cannot parse default checkpoint timeout"),
508 216 : compaction_target_size: DEFAULT_COMPACTION_TARGET_SIZE,
509 216 : compaction_period: humantime::parse_duration(DEFAULT_COMPACTION_PERIOD)
510 216 : .expect("cannot parse default compaction period"),
511 216 : compaction_threshold: DEFAULT_COMPACTION_THRESHOLD,
512 216 : gc_horizon: DEFAULT_GC_HORIZON,
513 216 : gc_period: humantime::parse_duration(DEFAULT_GC_PERIOD)
514 216 : .expect("cannot parse default gc period"),
515 216 : image_creation_threshold: DEFAULT_IMAGE_CREATION_THRESHOLD,
516 216 : pitr_interval: humantime::parse_duration(DEFAULT_PITR_INTERVAL)
517 216 : .expect("cannot parse default PITR interval"),
518 216 : walreceiver_connect_timeout: humantime::parse_duration(
519 216 : DEFAULT_WALRECEIVER_CONNECT_TIMEOUT,
520 216 : )
521 216 : .expect("cannot parse default walreceiver connect timeout"),
522 216 : lagging_wal_timeout: humantime::parse_duration(DEFAULT_WALRECEIVER_LAGGING_WAL_TIMEOUT)
523 216 : .expect("cannot parse default walreceiver lagging wal timeout"),
524 216 : max_lsn_wal_lag: NonZeroU64::new(DEFAULT_MAX_WALRECEIVER_LSN_WAL_LAG)
525 216 : .expect("cannot parse default max walreceiver Lsn wal lag"),
526 216 : trace_read_requests: false,
527 216 : eviction_policy: EvictionPolicy::NoEviction,
528 216 : min_resident_size_override: None,
529 216 : evictions_low_residence_duration_metric_threshold: humantime::parse_duration(
530 216 : DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD,
531 216 : )
532 216 : .expect("cannot parse default evictions_low_residence_duration_metric_threshold"),
533 216 : gc_feedback: false,
534 216 : heatmap_period: Duration::ZERO,
535 216 : lazy_slru_download: false,
536 216 : timeline_get_throttle: crate::tenant::throttle::Config::disabled(),
537 216 : }
538 216 : }
539 : }
540 :
541 : impl TryFrom<&'_ models::TenantConfig> for TenantConfOpt {
542 : type Error = anyhow::Error;
543 :
544 4 : fn try_from(request_data: &'_ models::TenantConfig) -> Result<Self, Self::Error> {
545 : // Convert the request_data to a JSON Value
546 4 : let json_value: Value = serde_json::to_value(request_data)?;
547 :
548 : // Create a Deserializer from the JSON Value
549 4 : let deserializer = json_value.into_deserializer();
550 :
551 : // Use serde_path_to_error to deserialize the JSON Value into TenantConfOpt
552 4 : let tenant_conf: TenantConfOpt = serde_path_to_error::deserialize(deserializer)?;
553 :
554 2 : Ok(tenant_conf)
555 4 : }
556 : }
557 :
558 : impl TryFrom<toml_edit::Item> for TenantConfOpt {
559 : type Error = anyhow::Error;
560 :
561 10 : fn try_from(item: toml_edit::Item) -> Result<Self, Self::Error> {
562 10 : match item {
563 2 : toml_edit::Item::Value(value) => {
564 2 : let d = value.into_deserializer();
565 2 : return serde_path_to_error::deserialize(d)
566 2 : .map_err(|e| anyhow::anyhow!("{}: {}", e.path(), e.inner().message()));
567 : }
568 8 : toml_edit::Item::Table(table) => {
569 8 : let deserializer = toml_edit::de::Deserializer::new(table.into());
570 8 : return serde_path_to_error::deserialize(deserializer)
571 8 : .map_err(|e| anyhow::anyhow!("{}: {}", e.path(), e.inner().message()));
572 : }
573 : _ => {
574 0 : bail!("expected non-inline table but found {item}")
575 : }
576 : }
577 10 : }
578 : }
579 :
580 : /// This is a conversion from our internal tenant config object to the one used
581 : /// in external APIs.
582 : impl From<TenantConfOpt> for models::TenantConfig {
583 0 : fn from(value: TenantConfOpt) -> Self {
584 0 : fn humantime(d: Duration) -> String {
585 0 : format!("{}s", d.as_secs())
586 0 : }
587 0 : Self {
588 0 : checkpoint_distance: value.checkpoint_distance,
589 0 : checkpoint_timeout: value.checkpoint_timeout.map(humantime),
590 0 : compaction_target_size: value.compaction_target_size,
591 0 : compaction_period: value.compaction_period.map(humantime),
592 0 : compaction_threshold: value.compaction_threshold,
593 0 : gc_horizon: value.gc_horizon,
594 0 : gc_period: value.gc_period.map(humantime),
595 0 : image_creation_threshold: value.image_creation_threshold,
596 0 : pitr_interval: value.pitr_interval.map(humantime),
597 0 : walreceiver_connect_timeout: value.walreceiver_connect_timeout.map(humantime),
598 0 : lagging_wal_timeout: value.lagging_wal_timeout.map(humantime),
599 0 : max_lsn_wal_lag: value.max_lsn_wal_lag,
600 0 : trace_read_requests: value.trace_read_requests,
601 0 : eviction_policy: value.eviction_policy,
602 0 : min_resident_size_override: value.min_resident_size_override,
603 0 : evictions_low_residence_duration_metric_threshold: value
604 0 : .evictions_low_residence_duration_metric_threshold
605 0 : .map(humantime),
606 0 : gc_feedback: value.gc_feedback,
607 0 : heatmap_period: value.heatmap_period.map(humantime),
608 0 : lazy_slru_download: value.lazy_slru_download,
609 0 : timeline_get_throttle: value.timeline_get_throttle.map(ThrottleConfig::from),
610 0 : }
611 0 : }
612 : }
613 :
614 : #[cfg(test)]
615 : mod tests {
616 : use super::*;
617 : use models::TenantConfig;
618 :
619 2 : #[test]
620 2 : fn de_serializing_pageserver_config_omits_empty_values() {
621 2 : let small_conf = TenantConfOpt {
622 2 : gc_horizon: Some(42),
623 2 : ..TenantConfOpt::default()
624 2 : };
625 2 :
626 2 : let toml_form = toml_edit::ser::to_string(&small_conf).unwrap();
627 2 : assert_eq!(toml_form, "gc_horizon = 42\n");
628 2 : assert_eq!(small_conf, toml_edit::de::from_str(&toml_form).unwrap());
629 :
630 2 : let json_form = serde_json::to_string(&small_conf).unwrap();
631 2 : assert_eq!(json_form, "{\"gc_horizon\":42}");
632 2 : assert_eq!(small_conf, serde_json::from_str(&json_form).unwrap());
633 2 : }
634 :
635 2 : #[test]
636 2 : fn test_try_from_models_tenant_config_err() {
637 2 : let tenant_config = models::TenantConfig {
638 2 : lagging_wal_timeout: Some("5a".to_string()),
639 2 : ..TenantConfig::default()
640 2 : };
641 2 :
642 2 : let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config);
643 2 :
644 2 : assert!(
645 2 : tenant_conf_opt.is_err(),
646 0 : "Suceeded to convert TenantConfig to TenantConfOpt"
647 : );
648 :
649 2 : let expected_error_str =
650 2 : "lagging_wal_timeout: invalid value: string \"5a\", expected a duration";
651 2 : assert_eq!(tenant_conf_opt.unwrap_err().to_string(), expected_error_str);
652 2 : }
653 :
654 2 : #[test]
655 2 : fn test_try_from_models_tenant_config_success() {
656 2 : let tenant_config = models::TenantConfig {
657 2 : lagging_wal_timeout: Some("5s".to_string()),
658 2 : ..TenantConfig::default()
659 2 : };
660 2 :
661 2 : let tenant_conf_opt = TenantConfOpt::try_from(&tenant_config).unwrap();
662 2 :
663 2 : assert_eq!(
664 2 : tenant_conf_opt.lagging_wal_timeout,
665 2 : Some(Duration::from_secs(5))
666 2 : );
667 2 : }
668 : }
|