Line data Source code
1 : //! Functions for handling page server configuration options
2 : //!
3 : //! Configuration options can be set in the pageserver.toml configuration
4 : //! file, or on the command line.
5 : //! See also `settings.md` for better description on every parameter.
6 :
7 : use anyhow::{anyhow, bail, ensure, Context, Result};
8 : use pageserver_api::{models::ImageCompressionAlgorithm, shard::TenantShardId};
9 : use remote_storage::{RemotePath, RemoteStorageConfig};
10 : use serde::de::IntoDeserializer;
11 : use serde::{self, Deserialize};
12 : use std::env;
13 : use storage_broker::Uri;
14 : use utils::crashsafe::path_with_suffix_extension;
15 : use utils::logging::SecretString;
16 :
17 : use once_cell::sync::OnceCell;
18 : use reqwest::Url;
19 : use std::num::NonZeroUsize;
20 : use std::str::FromStr;
21 : use std::sync::Arc;
22 : use std::time::Duration;
23 : use toml_edit::{Document, Item};
24 :
25 : use camino::{Utf8Path, Utf8PathBuf};
26 : use postgres_backend::AuthType;
27 : use utils::{
28 : id::{NodeId, TimelineId},
29 : logging::LogFormat,
30 : };
31 :
32 : use crate::l0_flush::L0FlushConfig;
33 : use crate::tenant::config::TenantConfOpt;
34 : use crate::tenant::storage_layer::inmemory_layer::IndexEntry;
35 : use crate::tenant::timeline::compaction::CompactL0Phase1ValueAccess;
36 : use crate::tenant::vectored_blob_io::MaxVectoredReadBytes;
37 : use crate::tenant::{TENANTS_SEGMENT_NAME, TIMELINES_SEGMENT_NAME};
38 : use crate::{disk_usage_eviction_task::DiskUsageEvictionTaskConfig, virtual_file::io_engine};
39 : use crate::{tenant::config::TenantConf, virtual_file};
40 : use crate::{TENANT_HEATMAP_BASENAME, TENANT_LOCATION_CONFIG_NAME, TIMELINE_DELETE_MARK_SUFFIX};
41 :
42 : use self::defaults::DEFAULT_CONCURRENT_TENANT_WARMUP;
43 :
44 : use self::defaults::DEFAULT_VIRTUAL_FILE_IO_ENGINE;
45 :
46 : pub mod defaults {
47 : use crate::tenant::config::defaults::*;
48 : use const_format::formatcp;
49 :
50 : pub use pageserver_api::config::{
51 : DEFAULT_HTTP_LISTEN_ADDR, DEFAULT_HTTP_LISTEN_PORT, DEFAULT_PG_LISTEN_ADDR,
52 : DEFAULT_PG_LISTEN_PORT,
53 : };
54 : pub use storage_broker::DEFAULT_ENDPOINT as BROKER_DEFAULT_ENDPOINT;
55 :
56 : pub const DEFAULT_WAIT_LSN_TIMEOUT: &str = "300 s";
57 : pub const DEFAULT_WAL_REDO_TIMEOUT: &str = "60 s";
58 :
59 : pub const DEFAULT_SUPERUSER: &str = "cloud_admin";
60 :
61 : pub const DEFAULT_PAGE_CACHE_SIZE: usize = 8192;
62 : pub const DEFAULT_MAX_FILE_DESCRIPTORS: usize = 100;
63 :
64 : pub const DEFAULT_LOG_FORMAT: &str = "plain";
65 :
66 : pub const DEFAULT_CONCURRENT_TENANT_WARMUP: usize = 8;
67 :
68 : pub const DEFAULT_CONCURRENT_TENANT_SIZE_LOGICAL_SIZE_QUERIES: usize =
69 : super::ConfigurableSemaphore::DEFAULT_INITIAL.get();
70 :
71 : pub const DEFAULT_METRIC_COLLECTION_INTERVAL: &str = "10 min";
72 : pub const DEFAULT_METRIC_COLLECTION_ENDPOINT: Option<reqwest::Url> = None;
73 : pub const DEFAULT_SYNTHETIC_SIZE_CALCULATION_INTERVAL: &str = "10 min";
74 : pub const DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY: &str = "10s";
75 :
76 : pub const DEFAULT_HEATMAP_UPLOAD_CONCURRENCY: usize = 8;
77 : pub const DEFAULT_SECONDARY_DOWNLOAD_CONCURRENCY: usize = 1;
78 :
79 : pub const DEFAULT_INGEST_BATCH_SIZE: u64 = 100;
80 :
81 : #[cfg(target_os = "linux")]
82 : pub const DEFAULT_VIRTUAL_FILE_IO_ENGINE: &str = "tokio-epoll-uring";
83 :
84 : #[cfg(not(target_os = "linux"))]
85 : pub const DEFAULT_VIRTUAL_FILE_IO_ENGINE: &str = "std-fs";
86 :
87 : pub const DEFAULT_GET_VECTORED_IMPL: &str = "vectored";
88 :
89 : pub const DEFAULT_GET_IMPL: &str = "vectored";
90 :
91 : pub const DEFAULT_MAX_VECTORED_READ_BYTES: usize = 128 * 1024; // 128 KiB
92 :
93 : pub const DEFAULT_IMAGE_COMPRESSION: &str = "zstd(1)";
94 :
95 : pub const DEFAULT_VALIDATE_VECTORED_GET: bool = false;
96 :
97 : pub const DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB: usize = 0;
98 :
99 : pub const DEFAULT_IO_BUFFER_ALIGNMENT: usize = 512;
100 :
101 : ///
102 : /// Default built-in configuration file.
103 : ///
104 : pub const DEFAULT_CONFIG_FILE: &str = formatcp!(
105 : r#"
106 : # Initial configuration file created by 'pageserver --init'
107 : #listen_pg_addr = '{DEFAULT_PG_LISTEN_ADDR}'
108 : #listen_http_addr = '{DEFAULT_HTTP_LISTEN_ADDR}'
109 :
110 : #wait_lsn_timeout = '{DEFAULT_WAIT_LSN_TIMEOUT}'
111 : #wal_redo_timeout = '{DEFAULT_WAL_REDO_TIMEOUT}'
112 :
113 : #page_cache_size = {DEFAULT_PAGE_CACHE_SIZE}
114 : #max_file_descriptors = {DEFAULT_MAX_FILE_DESCRIPTORS}
115 :
116 : # initial superuser role name to use when creating a new tenant
117 : #initial_superuser_name = '{DEFAULT_SUPERUSER}'
118 :
119 : #broker_endpoint = '{BROKER_DEFAULT_ENDPOINT}'
120 :
121 : #log_format = '{DEFAULT_LOG_FORMAT}'
122 :
123 : #concurrent_tenant_size_logical_size_queries = '{DEFAULT_CONCURRENT_TENANT_SIZE_LOGICAL_SIZE_QUERIES}'
124 : #concurrent_tenant_warmup = '{DEFAULT_CONCURRENT_TENANT_WARMUP}'
125 :
126 : #metric_collection_interval = '{DEFAULT_METRIC_COLLECTION_INTERVAL}'
127 : #synthetic_size_calculation_interval = '{DEFAULT_SYNTHETIC_SIZE_CALCULATION_INTERVAL}'
128 :
129 : #disk_usage_based_eviction = {{ max_usage_pct = .., min_avail_bytes = .., period = "10s"}}
130 :
131 : #background_task_maximum_delay = '{DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY}'
132 :
133 : #ingest_batch_size = {DEFAULT_INGEST_BATCH_SIZE}
134 :
135 : #virtual_file_io_engine = '{DEFAULT_VIRTUAL_FILE_IO_ENGINE}'
136 :
137 : #max_vectored_read_bytes = '{DEFAULT_MAX_VECTORED_READ_BYTES}'
138 :
139 : [tenant_config]
140 : #checkpoint_distance = {DEFAULT_CHECKPOINT_DISTANCE} # in bytes
141 : #checkpoint_timeout = {DEFAULT_CHECKPOINT_TIMEOUT}
142 : #compaction_target_size = {DEFAULT_COMPACTION_TARGET_SIZE} # in bytes
143 : #compaction_period = '{DEFAULT_COMPACTION_PERIOD}'
144 : #compaction_threshold = {DEFAULT_COMPACTION_THRESHOLD}
145 :
146 : #gc_period = '{DEFAULT_GC_PERIOD}'
147 : #gc_horizon = {DEFAULT_GC_HORIZON}
148 : #image_creation_threshold = {DEFAULT_IMAGE_CREATION_THRESHOLD}
149 : #pitr_interval = '{DEFAULT_PITR_INTERVAL}'
150 :
151 : #min_resident_size_override = .. # in bytes
152 : #evictions_low_residence_duration_metric_threshold = '{DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD}'
153 :
154 : #heatmap_upload_concurrency = {DEFAULT_HEATMAP_UPLOAD_CONCURRENCY}
155 : #secondary_download_concurrency = {DEFAULT_SECONDARY_DOWNLOAD_CONCURRENCY}
156 :
157 : #ephemeral_bytes_per_memory_kb = {DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB}
158 :
159 : #[remote_storage]
160 :
161 : "#
162 : );
163 : }
164 :
165 : #[derive(Debug, Clone, PartialEq, Eq)]
166 : pub struct PageServerConf {
167 : // Identifier of that particular pageserver so e g safekeepers
168 : // can safely distinguish different pageservers
169 : pub id: NodeId,
170 :
171 : /// Example (default): 127.0.0.1:64000
172 : pub listen_pg_addr: String,
173 : /// Example (default): 127.0.0.1:9898
174 : pub listen_http_addr: String,
175 :
176 : /// Current availability zone. Used for traffic metrics.
177 : pub availability_zone: Option<String>,
178 :
179 : // Timeout when waiting for WAL receiver to catch up to an LSN given in a GetPage@LSN call.
180 : pub wait_lsn_timeout: Duration,
181 : // How long to wait for WAL redo to complete.
182 : pub wal_redo_timeout: Duration,
183 :
184 : pub superuser: String,
185 :
186 : pub page_cache_size: usize,
187 : pub max_file_descriptors: usize,
188 :
189 : // Repository directory, relative to current working directory.
190 : // Normally, the page server changes the current working directory
191 : // to the repository, and 'workdir' is always '.'. But we don't do
192 : // that during unit testing, because the current directory is global
193 : // to the process but different unit tests work on different
194 : // repositories.
195 : pub workdir: Utf8PathBuf,
196 :
197 : pub pg_distrib_dir: Utf8PathBuf,
198 :
199 : // Authentication
200 : /// authentication method for the HTTP mgmt API
201 : pub http_auth_type: AuthType,
202 : /// authentication method for libpq connections from compute
203 : pub pg_auth_type: AuthType,
204 : /// Path to a file or directory containing public key(s) for verifying JWT tokens.
205 : /// Used for both mgmt and compute auth, if enabled.
206 : pub auth_validation_public_key_path: Option<Utf8PathBuf>,
207 :
208 : pub remote_storage_config: Option<RemoteStorageConfig>,
209 :
210 : pub default_tenant_conf: TenantConf,
211 :
212 : /// Storage broker endpoints to connect to.
213 : pub broker_endpoint: Uri,
214 : pub broker_keepalive_interval: Duration,
215 :
216 : pub log_format: LogFormat,
217 :
218 : /// Number of tenants which will be concurrently loaded from remote storage proactively on startup or attach.
219 : ///
220 : /// A lower value implicitly deprioritizes loading such tenants, vs. other work in the system.
221 : pub concurrent_tenant_warmup: ConfigurableSemaphore,
222 :
223 : /// Number of concurrent [`Tenant::gather_size_inputs`](crate::tenant::Tenant::gather_size_inputs) allowed.
224 : pub concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore,
225 : /// Limit of concurrent [`Tenant::gather_size_inputs`] issued by module `eviction_task`.
226 : /// The number of permits is the same as `concurrent_tenant_size_logical_size_queries`.
227 : /// See the comment in `eviction_task` for details.
228 : ///
229 : /// [`Tenant::gather_size_inputs`]: crate::tenant::Tenant::gather_size_inputs
230 : pub eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore,
231 :
232 : // How often to collect metrics and send them to the metrics endpoint.
233 : pub metric_collection_interval: Duration,
234 : // How often to send unchanged cached metrics to the metrics endpoint.
235 : pub metric_collection_endpoint: Option<Url>,
236 : pub metric_collection_bucket: Option<RemoteStorageConfig>,
237 : pub synthetic_size_calculation_interval: Duration,
238 :
239 : pub disk_usage_based_eviction: Option<DiskUsageEvictionTaskConfig>,
240 :
241 : pub test_remote_failures: u64,
242 :
243 : pub ondemand_download_behavior_treat_error_as_warn: bool,
244 :
245 : /// How long will background tasks be delayed at most after initial load of tenants.
246 : ///
247 : /// Our largest initialization completions are in the range of 100-200s, so perhaps 10s works
248 : /// as we now isolate initial loading, initial logical size calculation and background tasks.
249 : /// Smaller nodes will have background tasks "not running" for this long unless every timeline
250 : /// has it's initial logical size calculated. Not running background tasks for some seconds is
251 : /// not terrible.
252 : pub background_task_maximum_delay: Duration,
253 :
254 : pub control_plane_api: Option<Url>,
255 :
256 : /// JWT token for use with the control plane API.
257 : pub control_plane_api_token: Option<SecretString>,
258 :
259 : /// If true, pageserver will make best-effort to operate without a control plane: only
260 : /// for use in major incidents.
261 : pub control_plane_emergency_mode: bool,
262 :
263 : /// How many heatmap uploads may be done concurrency: lower values implicitly deprioritize
264 : /// heatmap uploads vs. other remote storage operations.
265 : pub heatmap_upload_concurrency: usize,
266 :
267 : /// How many remote storage downloads may be done for secondary tenants concurrently. Implicitly
268 : /// deprioritises secondary downloads vs. remote storage operations for attached tenants.
269 : pub secondary_download_concurrency: usize,
270 :
271 : /// Maximum number of WAL records to be ingested and committed at the same time
272 : pub ingest_batch_size: u64,
273 :
274 : pub virtual_file_io_engine: virtual_file::IoEngineKind,
275 :
276 : pub max_vectored_read_bytes: MaxVectoredReadBytes,
277 :
278 : pub image_compression: ImageCompressionAlgorithm,
279 :
280 : /// How many bytes of ephemeral layer content will we allow per kilobyte of RAM. When this
281 : /// is exceeded, we start proactively closing ephemeral layers to limit the total amount
282 : /// of ephemeral data.
283 : ///
284 : /// Setting this to zero disables limits on total ephemeral layer size.
285 : pub ephemeral_bytes_per_memory_kb: usize,
286 :
287 : pub l0_flush: L0FlushConfig,
288 :
289 : /// This flag is temporary and will be removed after gradual rollout.
290 : /// See <https://github.com/neondatabase/neon/issues/8184>.
291 : pub compact_level0_phase1_value_access: CompactL0Phase1ValueAccess,
292 :
293 : /// Direct IO settings
294 : pub virtual_file_direct_io: virtual_file::DirectIoMode,
295 :
296 : pub io_buffer_alignment: usize,
297 : }
298 :
299 : /// We do not want to store this in a PageServerConf because the latter may be logged
300 : /// and/or serialized at a whim, while the token is secret. Currently this token is the
301 : /// same for accessing all tenants/timelines, but may become per-tenant/per-timeline in
302 : /// the future, more tokens and auth may arrive for storage broker, completely changing the logic.
303 : /// Hence, we resort to a global variable for now instead of passing the token from the
304 : /// startup code to the connection code through a dozen layers.
305 : pub static SAFEKEEPER_AUTH_TOKEN: OnceCell<Arc<String>> = OnceCell::new();
306 :
307 : // use dedicated enum for builder to better indicate the intention
308 : // and avoid possible confusion with nested options
309 : #[derive(Clone, Default)]
310 : pub enum BuilderValue<T> {
311 : Set(T),
312 : #[default]
313 : NotSet,
314 : }
315 :
316 : impl<T: Clone> BuilderValue<T> {
317 1968 : pub fn ok_or(&self, field_name: &'static str, default: BuilderValue<T>) -> anyhow::Result<T> {
318 1968 : match self {
319 546 : Self::Set(v) => Ok(v.clone()),
320 1422 : Self::NotSet => match default {
321 1422 : BuilderValue::Set(v) => Ok(v.clone()),
322 : BuilderValue::NotSet => {
323 0 : anyhow::bail!("missing config value {field_name:?}")
324 : }
325 : },
326 : }
327 1968 : }
328 : }
329 :
330 : // needed to simplify config construction
331 : #[derive(Default)]
332 : struct PageServerConfigBuilder {
333 : listen_pg_addr: BuilderValue<String>,
334 :
335 : listen_http_addr: BuilderValue<String>,
336 :
337 : availability_zone: BuilderValue<Option<String>>,
338 :
339 : wait_lsn_timeout: BuilderValue<Duration>,
340 : wal_redo_timeout: BuilderValue<Duration>,
341 :
342 : superuser: BuilderValue<String>,
343 :
344 : page_cache_size: BuilderValue<usize>,
345 : max_file_descriptors: BuilderValue<usize>,
346 :
347 : workdir: BuilderValue<Utf8PathBuf>,
348 :
349 : pg_distrib_dir: BuilderValue<Utf8PathBuf>,
350 :
351 : http_auth_type: BuilderValue<AuthType>,
352 : pg_auth_type: BuilderValue<AuthType>,
353 :
354 : //
355 : auth_validation_public_key_path: BuilderValue<Option<Utf8PathBuf>>,
356 : remote_storage_config: BuilderValue<Option<RemoteStorageConfig>>,
357 :
358 : broker_endpoint: BuilderValue<Uri>,
359 : broker_keepalive_interval: BuilderValue<Duration>,
360 :
361 : log_format: BuilderValue<LogFormat>,
362 :
363 : concurrent_tenant_warmup: BuilderValue<NonZeroUsize>,
364 : concurrent_tenant_size_logical_size_queries: BuilderValue<NonZeroUsize>,
365 :
366 : metric_collection_interval: BuilderValue<Duration>,
367 : metric_collection_endpoint: BuilderValue<Option<Url>>,
368 : synthetic_size_calculation_interval: BuilderValue<Duration>,
369 : metric_collection_bucket: BuilderValue<Option<RemoteStorageConfig>>,
370 :
371 : disk_usage_based_eviction: BuilderValue<Option<DiskUsageEvictionTaskConfig>>,
372 :
373 : test_remote_failures: BuilderValue<u64>,
374 :
375 : ondemand_download_behavior_treat_error_as_warn: BuilderValue<bool>,
376 :
377 : background_task_maximum_delay: BuilderValue<Duration>,
378 :
379 : control_plane_api: BuilderValue<Option<Url>>,
380 : control_plane_api_token: BuilderValue<Option<SecretString>>,
381 : control_plane_emergency_mode: BuilderValue<bool>,
382 :
383 : heatmap_upload_concurrency: BuilderValue<usize>,
384 : secondary_download_concurrency: BuilderValue<usize>,
385 :
386 : ingest_batch_size: BuilderValue<u64>,
387 :
388 : virtual_file_io_engine: BuilderValue<virtual_file::IoEngineKind>,
389 :
390 : max_vectored_read_bytes: BuilderValue<MaxVectoredReadBytes>,
391 :
392 : image_compression: BuilderValue<ImageCompressionAlgorithm>,
393 :
394 : ephemeral_bytes_per_memory_kb: BuilderValue<usize>,
395 :
396 : l0_flush: BuilderValue<L0FlushConfig>,
397 :
398 : compact_level0_phase1_value_access: BuilderValue<CompactL0Phase1ValueAccess>,
399 :
400 : virtual_file_direct_io: BuilderValue<virtual_file::DirectIoMode>,
401 :
402 : io_buffer_alignment: BuilderValue<usize>,
403 : }
404 :
405 : impl PageServerConfigBuilder {
406 54 : fn new() -> Self {
407 54 : Self::default()
408 54 : }
409 :
410 : #[inline(always)]
411 48 : fn default_values() -> Self {
412 48 : use self::BuilderValue::*;
413 48 : use defaults::*;
414 48 : Self {
415 48 : listen_pg_addr: Set(DEFAULT_PG_LISTEN_ADDR.to_string()),
416 48 : listen_http_addr: Set(DEFAULT_HTTP_LISTEN_ADDR.to_string()),
417 48 : availability_zone: Set(None),
418 48 : wait_lsn_timeout: Set(humantime::parse_duration(DEFAULT_WAIT_LSN_TIMEOUT)
419 48 : .expect("cannot parse default wait lsn timeout")),
420 48 : wal_redo_timeout: Set(humantime::parse_duration(DEFAULT_WAL_REDO_TIMEOUT)
421 48 : .expect("cannot parse default wal redo timeout")),
422 48 : superuser: Set(DEFAULT_SUPERUSER.to_string()),
423 48 : page_cache_size: Set(DEFAULT_PAGE_CACHE_SIZE),
424 48 : max_file_descriptors: Set(DEFAULT_MAX_FILE_DESCRIPTORS),
425 48 : workdir: Set(Utf8PathBuf::new()),
426 48 : pg_distrib_dir: Set(Utf8PathBuf::from_path_buf(
427 48 : env::current_dir().expect("cannot access current directory"),
428 48 : )
429 48 : .expect("non-Unicode path")
430 48 : .join("pg_install")),
431 48 : http_auth_type: Set(AuthType::Trust),
432 48 : pg_auth_type: Set(AuthType::Trust),
433 48 : auth_validation_public_key_path: Set(None),
434 48 : remote_storage_config: Set(None),
435 48 : broker_endpoint: Set(storage_broker::DEFAULT_ENDPOINT
436 48 : .parse()
437 48 : .expect("failed to parse default broker endpoint")),
438 48 : broker_keepalive_interval: Set(humantime::parse_duration(
439 48 : storage_broker::DEFAULT_KEEPALIVE_INTERVAL,
440 48 : )
441 48 : .expect("cannot parse default keepalive interval")),
442 48 : log_format: Set(LogFormat::from_str(DEFAULT_LOG_FORMAT).unwrap()),
443 48 :
444 48 : concurrent_tenant_warmup: Set(NonZeroUsize::new(DEFAULT_CONCURRENT_TENANT_WARMUP)
445 48 : .expect("Invalid default constant")),
446 48 : concurrent_tenant_size_logical_size_queries: Set(
447 48 : ConfigurableSemaphore::DEFAULT_INITIAL,
448 48 : ),
449 48 : metric_collection_interval: Set(humantime::parse_duration(
450 48 : DEFAULT_METRIC_COLLECTION_INTERVAL,
451 48 : )
452 48 : .expect("cannot parse default metric collection interval")),
453 48 : synthetic_size_calculation_interval: Set(humantime::parse_duration(
454 48 : DEFAULT_SYNTHETIC_SIZE_CALCULATION_INTERVAL,
455 48 : )
456 48 : .expect("cannot parse default synthetic size calculation interval")),
457 48 : metric_collection_endpoint: Set(DEFAULT_METRIC_COLLECTION_ENDPOINT),
458 48 :
459 48 : metric_collection_bucket: Set(None),
460 48 :
461 48 : disk_usage_based_eviction: Set(None),
462 48 :
463 48 : test_remote_failures: Set(0),
464 48 :
465 48 : ondemand_download_behavior_treat_error_as_warn: Set(false),
466 48 :
467 48 : background_task_maximum_delay: Set(humantime::parse_duration(
468 48 : DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY,
469 48 : )
470 48 : .unwrap()),
471 48 :
472 48 : control_plane_api: Set(None),
473 48 : control_plane_api_token: Set(None),
474 48 : control_plane_emergency_mode: Set(false),
475 48 :
476 48 : heatmap_upload_concurrency: Set(DEFAULT_HEATMAP_UPLOAD_CONCURRENCY),
477 48 : secondary_download_concurrency: Set(DEFAULT_SECONDARY_DOWNLOAD_CONCURRENCY),
478 48 :
479 48 : ingest_batch_size: Set(DEFAULT_INGEST_BATCH_SIZE),
480 48 :
481 48 : virtual_file_io_engine: Set(DEFAULT_VIRTUAL_FILE_IO_ENGINE.parse().unwrap()),
482 48 :
483 48 : max_vectored_read_bytes: Set(MaxVectoredReadBytes(
484 48 : NonZeroUsize::new(DEFAULT_MAX_VECTORED_READ_BYTES).unwrap(),
485 48 : )),
486 48 : image_compression: Set(DEFAULT_IMAGE_COMPRESSION.parse().unwrap()),
487 48 : ephemeral_bytes_per_memory_kb: Set(DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB),
488 48 : l0_flush: Set(L0FlushConfig::default()),
489 48 : compact_level0_phase1_value_access: Set(CompactL0Phase1ValueAccess::default()),
490 48 : virtual_file_direct_io: Set(virtual_file::DirectIoMode::default()),
491 48 : io_buffer_alignment: Set(DEFAULT_IO_BUFFER_ALIGNMENT),
492 48 : }
493 48 : }
494 : }
495 :
496 : impl PageServerConfigBuilder {
497 30 : pub fn listen_pg_addr(&mut self, listen_pg_addr: String) {
498 30 : self.listen_pg_addr = BuilderValue::Set(listen_pg_addr)
499 30 : }
500 :
501 30 : pub fn listen_http_addr(&mut self, listen_http_addr: String) {
502 30 : self.listen_http_addr = BuilderValue::Set(listen_http_addr)
503 30 : }
504 :
505 0 : pub fn availability_zone(&mut self, availability_zone: Option<String>) {
506 0 : self.availability_zone = BuilderValue::Set(availability_zone)
507 0 : }
508 :
509 30 : pub fn wait_lsn_timeout(&mut self, wait_lsn_timeout: Duration) {
510 30 : self.wait_lsn_timeout = BuilderValue::Set(wait_lsn_timeout)
511 30 : }
512 :
513 30 : pub fn wal_redo_timeout(&mut self, wal_redo_timeout: Duration) {
514 30 : self.wal_redo_timeout = BuilderValue::Set(wal_redo_timeout)
515 30 : }
516 :
517 30 : pub fn superuser(&mut self, superuser: String) {
518 30 : self.superuser = BuilderValue::Set(superuser)
519 30 : }
520 :
521 30 : pub fn page_cache_size(&mut self, page_cache_size: usize) {
522 30 : self.page_cache_size = BuilderValue::Set(page_cache_size)
523 30 : }
524 :
525 30 : pub fn max_file_descriptors(&mut self, max_file_descriptors: usize) {
526 30 : self.max_file_descriptors = BuilderValue::Set(max_file_descriptors)
527 30 : }
528 :
529 54 : pub fn workdir(&mut self, workdir: Utf8PathBuf) {
530 54 : self.workdir = BuilderValue::Set(workdir)
531 54 : }
532 :
533 48 : pub fn pg_distrib_dir(&mut self, pg_distrib_dir: Utf8PathBuf) {
534 48 : self.pg_distrib_dir = BuilderValue::Set(pg_distrib_dir)
535 48 : }
536 :
537 0 : pub fn http_auth_type(&mut self, auth_type: AuthType) {
538 0 : self.http_auth_type = BuilderValue::Set(auth_type)
539 0 : }
540 :
541 0 : pub fn pg_auth_type(&mut self, auth_type: AuthType) {
542 0 : self.pg_auth_type = BuilderValue::Set(auth_type)
543 0 : }
544 :
545 0 : pub fn auth_validation_public_key_path(
546 0 : &mut self,
547 0 : auth_validation_public_key_path: Option<Utf8PathBuf>,
548 0 : ) {
549 0 : self.auth_validation_public_key_path = BuilderValue::Set(auth_validation_public_key_path)
550 0 : }
551 :
552 24 : pub fn remote_storage_config(&mut self, remote_storage_config: Option<RemoteStorageConfig>) {
553 24 : self.remote_storage_config = BuilderValue::Set(remote_storage_config)
554 24 : }
555 :
556 36 : pub fn broker_endpoint(&mut self, broker_endpoint: Uri) {
557 36 : self.broker_endpoint = BuilderValue::Set(broker_endpoint)
558 36 : }
559 :
560 0 : pub fn broker_keepalive_interval(&mut self, broker_keepalive_interval: Duration) {
561 0 : self.broker_keepalive_interval = BuilderValue::Set(broker_keepalive_interval)
562 0 : }
563 :
564 30 : pub fn log_format(&mut self, log_format: LogFormat) {
565 30 : self.log_format = BuilderValue::Set(log_format)
566 30 : }
567 :
568 0 : pub fn concurrent_tenant_warmup(&mut self, u: NonZeroUsize) {
569 0 : self.concurrent_tenant_warmup = BuilderValue::Set(u);
570 0 : }
571 :
572 0 : pub fn concurrent_tenant_size_logical_size_queries(&mut self, u: NonZeroUsize) {
573 0 : self.concurrent_tenant_size_logical_size_queries = BuilderValue::Set(u);
574 0 : }
575 :
576 42 : pub fn metric_collection_interval(&mut self, metric_collection_interval: Duration) {
577 42 : self.metric_collection_interval = BuilderValue::Set(metric_collection_interval)
578 42 : }
579 :
580 42 : pub fn metric_collection_endpoint(&mut self, metric_collection_endpoint: Option<Url>) {
581 42 : self.metric_collection_endpoint = BuilderValue::Set(metric_collection_endpoint)
582 42 : }
583 :
584 0 : pub fn metric_collection_bucket(
585 0 : &mut self,
586 0 : metric_collection_bucket: Option<RemoteStorageConfig>,
587 0 : ) {
588 0 : self.metric_collection_bucket = BuilderValue::Set(metric_collection_bucket)
589 0 : }
590 :
591 30 : pub fn synthetic_size_calculation_interval(
592 30 : &mut self,
593 30 : synthetic_size_calculation_interval: Duration,
594 30 : ) {
595 30 : self.synthetic_size_calculation_interval =
596 30 : BuilderValue::Set(synthetic_size_calculation_interval)
597 30 : }
598 :
599 0 : pub fn test_remote_failures(&mut self, fail_first: u64) {
600 0 : self.test_remote_failures = BuilderValue::Set(fail_first);
601 0 : }
602 :
603 6 : pub fn disk_usage_based_eviction(&mut self, value: Option<DiskUsageEvictionTaskConfig>) {
604 6 : self.disk_usage_based_eviction = BuilderValue::Set(value);
605 6 : }
606 :
607 0 : pub fn ondemand_download_behavior_treat_error_as_warn(
608 0 : &mut self,
609 0 : ondemand_download_behavior_treat_error_as_warn: bool,
610 0 : ) {
611 0 : self.ondemand_download_behavior_treat_error_as_warn =
612 0 : BuilderValue::Set(ondemand_download_behavior_treat_error_as_warn);
613 0 : }
614 :
615 30 : pub fn background_task_maximum_delay(&mut self, delay: Duration) {
616 30 : self.background_task_maximum_delay = BuilderValue::Set(delay);
617 30 : }
618 :
619 0 : pub fn control_plane_api(&mut self, api: Option<Url>) {
620 0 : self.control_plane_api = BuilderValue::Set(api)
621 0 : }
622 :
623 0 : pub fn control_plane_api_token(&mut self, token: Option<SecretString>) {
624 0 : self.control_plane_api_token = BuilderValue::Set(token)
625 0 : }
626 :
627 0 : pub fn control_plane_emergency_mode(&mut self, enabled: bool) {
628 0 : self.control_plane_emergency_mode = BuilderValue::Set(enabled)
629 0 : }
630 :
631 0 : pub fn heatmap_upload_concurrency(&mut self, value: usize) {
632 0 : self.heatmap_upload_concurrency = BuilderValue::Set(value)
633 0 : }
634 :
635 0 : pub fn secondary_download_concurrency(&mut self, value: usize) {
636 0 : self.secondary_download_concurrency = BuilderValue::Set(value)
637 0 : }
638 :
639 0 : pub fn ingest_batch_size(&mut self, ingest_batch_size: u64) {
640 0 : self.ingest_batch_size = BuilderValue::Set(ingest_batch_size)
641 0 : }
642 :
643 0 : pub fn virtual_file_io_engine(&mut self, value: virtual_file::IoEngineKind) {
644 0 : self.virtual_file_io_engine = BuilderValue::Set(value);
645 0 : }
646 :
647 0 : pub fn get_max_vectored_read_bytes(&mut self, value: MaxVectoredReadBytes) {
648 0 : self.max_vectored_read_bytes = BuilderValue::Set(value);
649 0 : }
650 :
651 0 : pub fn get_image_compression(&mut self, value: ImageCompressionAlgorithm) {
652 0 : self.image_compression = BuilderValue::Set(value);
653 0 : }
654 :
655 0 : pub fn get_ephemeral_bytes_per_memory_kb(&mut self, value: usize) {
656 0 : self.ephemeral_bytes_per_memory_kb = BuilderValue::Set(value);
657 0 : }
658 :
659 0 : pub fn l0_flush(&mut self, value: L0FlushConfig) {
660 0 : self.l0_flush = BuilderValue::Set(value);
661 0 : }
662 :
663 0 : pub fn compact_level0_phase1_value_access(&mut self, value: CompactL0Phase1ValueAccess) {
664 0 : self.compact_level0_phase1_value_access = BuilderValue::Set(value);
665 0 : }
666 :
667 0 : pub fn virtual_file_direct_io(&mut self, value: virtual_file::DirectIoMode) {
668 0 : self.virtual_file_direct_io = BuilderValue::Set(value);
669 0 : }
670 :
671 0 : pub fn io_buffer_alignment(&mut self, value: usize) {
672 0 : self.io_buffer_alignment = BuilderValue::Set(value);
673 0 : }
674 :
675 48 : pub fn build(self, id: NodeId) -> anyhow::Result<PageServerConf> {
676 48 : let default = Self::default_values();
677 48 :
678 48 : macro_rules! conf {
679 48 : (USING DEFAULT { $($field:ident,)* } CUSTOM LOGIC { $($custom_field:ident : $custom_value:expr,)* } ) => {
680 48 : PageServerConf {
681 48 : $(
682 48 : $field: self.$field.ok_or(stringify!($field), default.$field)?,
683 48 : )*
684 48 : $(
685 48 : $custom_field: $custom_value,
686 48 : )*
687 48 : }
688 48 : };
689 48 : }
690 48 :
691 48 : Ok(conf!(
692 : USING DEFAULT
693 : {
694 : listen_pg_addr,
695 : listen_http_addr,
696 : availability_zone,
697 : wait_lsn_timeout,
698 : wal_redo_timeout,
699 : superuser,
700 : page_cache_size,
701 : max_file_descriptors,
702 : workdir,
703 : pg_distrib_dir,
704 : http_auth_type,
705 : pg_auth_type,
706 : auth_validation_public_key_path,
707 : remote_storage_config,
708 : broker_endpoint,
709 : broker_keepalive_interval,
710 : log_format,
711 : metric_collection_interval,
712 : metric_collection_endpoint,
713 : metric_collection_bucket,
714 : synthetic_size_calculation_interval,
715 : disk_usage_based_eviction,
716 : test_remote_failures,
717 : ondemand_download_behavior_treat_error_as_warn,
718 : background_task_maximum_delay,
719 : control_plane_api,
720 : control_plane_api_token,
721 : control_plane_emergency_mode,
722 : heatmap_upload_concurrency,
723 : secondary_download_concurrency,
724 : ingest_batch_size,
725 : max_vectored_read_bytes,
726 : image_compression,
727 : ephemeral_bytes_per_memory_kb,
728 : l0_flush,
729 : compact_level0_phase1_value_access,
730 : virtual_file_direct_io,
731 : io_buffer_alignment,
732 : }
733 : CUSTOM LOGIC
734 : {
735 48 : id: id,
736 48 : // TenantConf is handled separately
737 48 : default_tenant_conf: TenantConf::default(),
738 48 : concurrent_tenant_warmup: ConfigurableSemaphore::new({
739 48 : self
740 48 : .concurrent_tenant_warmup
741 48 : .ok_or("concurrent_tenant_warmpup",
742 48 : default.concurrent_tenant_warmup)?
743 : }),
744 : concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore::new(
745 48 : self
746 48 : .concurrent_tenant_size_logical_size_queries
747 48 : .ok_or("concurrent_tenant_size_logical_size_queries",
748 48 : default.concurrent_tenant_size_logical_size_queries.clone())?
749 : ),
750 : eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore::new(
751 : // re-use `concurrent_tenant_size_logical_size_queries`
752 48 : self
753 48 : .concurrent_tenant_size_logical_size_queries
754 48 : .ok_or("eviction_task_immitated_concurrent_logical_size_queries",
755 48 : default.concurrent_tenant_size_logical_size_queries.clone())?,
756 : ),
757 48 : virtual_file_io_engine: match self.virtual_file_io_engine {
758 0 : BuilderValue::Set(v) => v,
759 48 : BuilderValue::NotSet => match crate::virtual_file::io_engine_feature_test().context("auto-detect virtual_file_io_engine")? {
760 48 : io_engine::FeatureTestResult::PlatformPreferred(v) => v, // make no noise
761 0 : io_engine::FeatureTestResult::Worse { engine, remark } => {
762 0 : // TODO: bubble this up to the caller so we can tracing::warn! it.
763 0 : eprintln!("auto-detected IO engine is not platform-preferred: engine={engine:?} remark={remark:?}");
764 0 : engine
765 : }
766 : },
767 : },
768 : }
769 : ))
770 48 : }
771 : }
772 :
773 : impl PageServerConf {
774 : //
775 : // Repository paths, relative to workdir.
776 : //
777 :
778 20706 : pub fn tenants_path(&self) -> Utf8PathBuf {
779 20706 : self.workdir.join(TENANTS_SEGMENT_NAME)
780 20706 : }
781 :
782 216 : pub fn deletion_prefix(&self) -> Utf8PathBuf {
783 216 : self.workdir.join("deletion")
784 216 : }
785 :
786 0 : pub fn metadata_path(&self) -> Utf8PathBuf {
787 0 : self.workdir.join("metadata.json")
788 0 : }
789 :
790 84 : pub fn deletion_list_path(&self, sequence: u64) -> Utf8PathBuf {
791 84 : // Encode a version in the filename, so that if we ever switch away from JSON we can
792 84 : // increment this.
793 84 : const VERSION: u8 = 1;
794 84 :
795 84 : self.deletion_prefix()
796 84 : .join(format!("{sequence:016x}-{VERSION:02x}.list"))
797 84 : }
798 :
799 72 : pub fn deletion_header_path(&self) -> Utf8PathBuf {
800 72 : // Encode a version in the filename, so that if we ever switch away from JSON we can
801 72 : // increment this.
802 72 : const VERSION: u8 = 1;
803 72 :
804 72 : self.deletion_prefix().join(format!("header-{VERSION:02x}"))
805 72 : }
806 :
807 20706 : pub fn tenant_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
808 20706 : self.tenants_path().join(tenant_shard_id.to_string())
809 20706 : }
810 :
811 : /// Points to a place in pageserver's local directory,
812 : /// where certain tenant's LocationConf be stored.
813 0 : pub(crate) fn tenant_location_config_path(
814 0 : &self,
815 0 : tenant_shard_id: &TenantShardId,
816 0 : ) -> Utf8PathBuf {
817 0 : self.tenant_path(tenant_shard_id)
818 0 : .join(TENANT_LOCATION_CONFIG_NAME)
819 0 : }
820 :
821 0 : pub(crate) fn tenant_heatmap_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
822 0 : self.tenant_path(tenant_shard_id)
823 0 : .join(TENANT_HEATMAP_BASENAME)
824 0 : }
825 :
826 20130 : pub fn timelines_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
827 20130 : self.tenant_path(tenant_shard_id)
828 20130 : .join(TIMELINES_SEGMENT_NAME)
829 20130 : }
830 :
831 18990 : pub fn timeline_path(
832 18990 : &self,
833 18990 : tenant_shard_id: &TenantShardId,
834 18990 : timeline_id: &TimelineId,
835 18990 : ) -> Utf8PathBuf {
836 18990 : self.timelines_path(tenant_shard_id)
837 18990 : .join(timeline_id.to_string())
838 18990 : }
839 :
840 0 : pub(crate) fn timeline_delete_mark_file_path(
841 0 : &self,
842 0 : tenant_shard_id: TenantShardId,
843 0 : timeline_id: TimelineId,
844 0 : ) -> Utf8PathBuf {
845 0 : path_with_suffix_extension(
846 0 : self.timeline_path(&tenant_shard_id, &timeline_id),
847 0 : TIMELINE_DELETE_MARK_SUFFIX,
848 0 : )
849 0 : }
850 :
851 : /// Turns storage remote path of a file into its local path.
852 0 : pub fn local_path(&self, remote_path: &RemotePath) -> Utf8PathBuf {
853 0 : remote_path.with_base(&self.workdir)
854 0 : }
855 :
856 : //
857 : // Postgres distribution paths
858 : //
859 48 : pub fn pg_distrib_dir(&self, pg_version: u32) -> anyhow::Result<Utf8PathBuf> {
860 48 : let path = self.pg_distrib_dir.clone();
861 48 :
862 48 : #[allow(clippy::manual_range_patterns)]
863 48 : match pg_version {
864 48 : 14 | 15 | 16 => Ok(path.join(format!("v{pg_version}"))),
865 0 : _ => bail!("Unsupported postgres version: {}", pg_version),
866 : }
867 48 : }
868 :
869 24 : pub fn pg_bin_dir(&self, pg_version: u32) -> anyhow::Result<Utf8PathBuf> {
870 24 : Ok(self.pg_distrib_dir(pg_version)?.join("bin"))
871 24 : }
872 24 : pub fn pg_lib_dir(&self, pg_version: u32) -> anyhow::Result<Utf8PathBuf> {
873 24 : Ok(self.pg_distrib_dir(pg_version)?.join("lib"))
874 24 : }
875 :
876 : /// Parse a configuration file (pageserver.toml) into a PageServerConf struct,
877 : /// validating the input and failing on errors.
878 : ///
879 : /// This leaves any options not present in the file in the built-in defaults.
880 54 : pub fn parse_and_validate(
881 54 : node_id: NodeId,
882 54 : toml: &Document,
883 54 : workdir: &Utf8Path,
884 54 : ) -> anyhow::Result<Self> {
885 54 : let mut builder = PageServerConfigBuilder::new();
886 54 : builder.workdir(workdir.to_owned());
887 54 :
888 54 : let mut t_conf = TenantConfOpt::default();
889 :
890 516 : for (key, item) in toml.iter() {
891 516 : match key {
892 516 : "listen_pg_addr" => builder.listen_pg_addr(parse_toml_string(key, item)?),
893 486 : "listen_http_addr" => builder.listen_http_addr(parse_toml_string(key, item)?),
894 456 : "availability_zone" => builder.availability_zone(Some(parse_toml_string(key, item)?)),
895 456 : "wait_lsn_timeout" => builder.wait_lsn_timeout(parse_toml_duration(key, item)?),
896 426 : "wal_redo_timeout" => builder.wal_redo_timeout(parse_toml_duration(key, item)?),
897 396 : "initial_superuser_name" => builder.superuser(parse_toml_string(key, item)?),
898 366 : "page_cache_size" => builder.page_cache_size(parse_toml_u64(key, item)? as usize),
899 336 : "max_file_descriptors" => {
900 30 : builder.max_file_descriptors(parse_toml_u64(key, item)? as usize)
901 : }
902 306 : "pg_distrib_dir" => {
903 48 : builder.pg_distrib_dir(Utf8PathBuf::from(parse_toml_string(key, item)?))
904 : }
905 258 : "auth_validation_public_key_path" => builder.auth_validation_public_key_path(Some(
906 0 : Utf8PathBuf::from(parse_toml_string(key, item)?),
907 : )),
908 258 : "http_auth_type" => builder.http_auth_type(parse_toml_from_str(key, item)?),
909 258 : "pg_auth_type" => builder.pg_auth_type(parse_toml_from_str(key, item)?),
910 258 : "remote_storage" => {
911 30 : builder.remote_storage_config(Some(RemoteStorageConfig::from_toml(item).context("remote_storage")?))
912 : }
913 228 : "tenant_config" => {
914 12 : t_conf = TenantConfOpt::try_from(item.to_owned()).context(format!("failed to parse: '{key}'"))?;
915 : }
916 216 : "broker_endpoint" => builder.broker_endpoint(parse_toml_string(key, item)?.parse().context("failed to parse broker endpoint")?),
917 180 : "broker_keepalive_interval" => builder.broker_keepalive_interval(parse_toml_duration(key, item)?),
918 180 : "log_format" => builder.log_format(
919 30 : LogFormat::from_config(&parse_toml_string(key, item)?)?
920 : ),
921 150 : "concurrent_tenant_warmup" => builder.concurrent_tenant_warmup({
922 0 : let input = parse_toml_string(key, item)?;
923 0 : let permits = input.parse::<usize>().context("expected a number of initial permits, not {s:?}")?;
924 0 : NonZeroUsize::new(permits).context("initial semaphore permits out of range: 0, use other configuration to disable a feature")?
925 : }),
926 150 : "concurrent_tenant_size_logical_size_queries" => builder.concurrent_tenant_size_logical_size_queries({
927 0 : let input = parse_toml_string(key, item)?;
928 0 : let permits = input.parse::<usize>().context("expected a number of initial permits, not {s:?}")?;
929 0 : NonZeroUsize::new(permits).context("initial semaphore permits out of range: 0, use other configuration to disable a feature")?
930 : }),
931 150 : "metric_collection_interval" => builder.metric_collection_interval(parse_toml_duration(key, item)?),
932 108 : "metric_collection_endpoint" => {
933 42 : let endpoint = parse_toml_string(key, item)?.parse().context("failed to parse metric_collection_endpoint")?;
934 42 : builder.metric_collection_endpoint(Some(endpoint));
935 : },
936 66 : "metric_collection_bucket" => {
937 0 : builder.metric_collection_bucket(Some(RemoteStorageConfig::from_toml(item)?))
938 : }
939 66 : "synthetic_size_calculation_interval" =>
940 30 : builder.synthetic_size_calculation_interval(parse_toml_duration(key, item)?),
941 36 : "test_remote_failures" => builder.test_remote_failures(parse_toml_u64(key, item)?),
942 36 : "disk_usage_based_eviction" => {
943 6 : tracing::info!("disk_usage_based_eviction: {:#?}", &item);
944 6 : builder.disk_usage_based_eviction(
945 6 : deserialize_from_item("disk_usage_based_eviction", item)
946 6 : .context("parse disk_usage_based_eviction")?
947 : )
948 : },
949 30 : "ondemand_download_behavior_treat_error_as_warn" => builder.ondemand_download_behavior_treat_error_as_warn(parse_toml_bool(key, item)?),
950 30 : "background_task_maximum_delay" => builder.background_task_maximum_delay(parse_toml_duration(key, item)?),
951 0 : "control_plane_api" => {
952 0 : let parsed = parse_toml_string(key, item)?;
953 0 : if parsed.is_empty() {
954 0 : builder.control_plane_api(None)
955 : } else {
956 0 : builder.control_plane_api(Some(parsed.parse().context("failed to parse control plane URL")?))
957 : }
958 : },
959 0 : "control_plane_api_token" => {
960 0 : let parsed = parse_toml_string(key, item)?;
961 0 : if parsed.is_empty() {
962 0 : builder.control_plane_api_token(None)
963 : } else {
964 0 : builder.control_plane_api_token(Some(parsed.into()))
965 : }
966 : },
967 0 : "control_plane_emergency_mode" => {
968 0 : builder.control_plane_emergency_mode(parse_toml_bool(key, item)?)
969 : },
970 0 : "heatmap_upload_concurrency" => {
971 0 : builder.heatmap_upload_concurrency(parse_toml_u64(key, item)? as usize)
972 : },
973 0 : "secondary_download_concurrency" => {
974 0 : builder.secondary_download_concurrency(parse_toml_u64(key, item)? as usize)
975 : },
976 0 : "ingest_batch_size" => builder.ingest_batch_size(parse_toml_u64(key, item)?),
977 0 : "virtual_file_io_engine" => {
978 0 : builder.virtual_file_io_engine(parse_toml_from_str("virtual_file_io_engine", item)?)
979 : }
980 0 : "max_vectored_read_bytes" => {
981 0 : let bytes = parse_toml_u64("max_vectored_read_bytes", item)? as usize;
982 0 : builder.get_max_vectored_read_bytes(
983 0 : MaxVectoredReadBytes(
984 0 : NonZeroUsize::new(bytes).expect("Max byte size of vectored read must be greater than 0")))
985 : }
986 0 : "image_compression" => {
987 0 : builder.get_image_compression(parse_toml_from_str("image_compression", item)?)
988 : }
989 0 : "ephemeral_bytes_per_memory_kb" => {
990 0 : builder.get_ephemeral_bytes_per_memory_kb(parse_toml_u64("ephemeral_bytes_per_memory_kb", item)? as usize)
991 : }
992 0 : "l0_flush" => {
993 0 : builder.l0_flush(utils::toml_edit_ext::deserialize_item(item).context("l0_flush")?)
994 : }
995 0 : "compact_level0_phase1_value_access" => {
996 0 : builder.compact_level0_phase1_value_access(utils::toml_edit_ext::deserialize_item(item).context("compact_level0_phase1_value_access")?)
997 : }
998 0 : "virtual_file_direct_io" => {
999 0 : builder.virtual_file_direct_io(utils::toml_edit_ext::deserialize_item(item).context("virtual_file_direct_io")?)
1000 : }
1001 0 : "io_buffer_alignment" => {
1002 0 : builder.io_buffer_alignment(parse_toml_u64("io_buffer_alignment", item)? as usize)
1003 : }
1004 0 : _ => bail!("unrecognized pageserver option '{key}'"),
1005 : }
1006 : }
1007 :
1008 48 : let mut conf = builder.build(node_id).context("invalid config")?;
1009 :
1010 48 : if conf.http_auth_type == AuthType::NeonJWT || conf.pg_auth_type == AuthType::NeonJWT {
1011 0 : let auth_validation_public_key_path = conf
1012 0 : .auth_validation_public_key_path
1013 0 : .get_or_insert_with(|| workdir.join("auth_public_key.pem"));
1014 0 : ensure!(
1015 0 : auth_validation_public_key_path.exists(),
1016 0 : format!(
1017 0 : "Can't find auth_validation_public_key at '{auth_validation_public_key_path}'",
1018 0 : )
1019 : );
1020 48 : }
1021 :
1022 48 : conf.default_tenant_conf = t_conf.merge(TenantConf::default());
1023 48 :
1024 48 : IndexEntry::validate_checkpoint_distance(conf.default_tenant_conf.checkpoint_distance)
1025 48 : .map_err(|msg| anyhow::anyhow!("{msg}"))
1026 48 : .with_context(|| {
1027 0 : format!(
1028 0 : "effective checkpoint distance is unsupported: {}",
1029 0 : conf.default_tenant_conf.checkpoint_distance
1030 0 : )
1031 48 : })?;
1032 :
1033 48 : Ok(conf)
1034 54 : }
1035 :
1036 : #[cfg(test)]
1037 618 : pub fn test_repo_dir(test_name: &str) -> Utf8PathBuf {
1038 618 : let test_output_dir = std::env::var("TEST_OUTPUT").unwrap_or("../tmp_check".into());
1039 618 : Utf8PathBuf::from(format!("{test_output_dir}/test_{test_name}"))
1040 618 : }
1041 :
1042 606 : pub fn dummy_conf(repo_dir: Utf8PathBuf) -> Self {
1043 606 : let pg_distrib_dir = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../pg_install");
1044 606 :
1045 606 : PageServerConf {
1046 606 : id: NodeId(0),
1047 606 : wait_lsn_timeout: Duration::from_secs(60),
1048 606 : wal_redo_timeout: Duration::from_secs(60),
1049 606 : page_cache_size: defaults::DEFAULT_PAGE_CACHE_SIZE,
1050 606 : max_file_descriptors: defaults::DEFAULT_MAX_FILE_DESCRIPTORS,
1051 606 : listen_pg_addr: defaults::DEFAULT_PG_LISTEN_ADDR.to_string(),
1052 606 : listen_http_addr: defaults::DEFAULT_HTTP_LISTEN_ADDR.to_string(),
1053 606 : availability_zone: None,
1054 606 : superuser: "cloud_admin".to_string(),
1055 606 : workdir: repo_dir,
1056 606 : pg_distrib_dir,
1057 606 : http_auth_type: AuthType::Trust,
1058 606 : pg_auth_type: AuthType::Trust,
1059 606 : auth_validation_public_key_path: None,
1060 606 : remote_storage_config: None,
1061 606 : default_tenant_conf: TenantConf::default(),
1062 606 : broker_endpoint: storage_broker::DEFAULT_ENDPOINT.parse().unwrap(),
1063 606 : broker_keepalive_interval: Duration::from_secs(5000),
1064 606 : log_format: LogFormat::from_str(defaults::DEFAULT_LOG_FORMAT).unwrap(),
1065 606 : concurrent_tenant_warmup: ConfigurableSemaphore::new(
1066 606 : NonZeroUsize::new(DEFAULT_CONCURRENT_TENANT_WARMUP)
1067 606 : .expect("Invalid default constant"),
1068 606 : ),
1069 606 : concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore::default(),
1070 606 : eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore::default(
1071 606 : ),
1072 606 : metric_collection_interval: Duration::from_secs(60),
1073 606 : metric_collection_endpoint: defaults::DEFAULT_METRIC_COLLECTION_ENDPOINT,
1074 606 : metric_collection_bucket: None,
1075 606 : synthetic_size_calculation_interval: Duration::from_secs(60),
1076 606 : disk_usage_based_eviction: None,
1077 606 : test_remote_failures: 0,
1078 606 : ondemand_download_behavior_treat_error_as_warn: false,
1079 606 : background_task_maximum_delay: Duration::ZERO,
1080 606 : control_plane_api: None,
1081 606 : control_plane_api_token: None,
1082 606 : control_plane_emergency_mode: false,
1083 606 : heatmap_upload_concurrency: defaults::DEFAULT_HEATMAP_UPLOAD_CONCURRENCY,
1084 606 : secondary_download_concurrency: defaults::DEFAULT_SECONDARY_DOWNLOAD_CONCURRENCY,
1085 606 : ingest_batch_size: defaults::DEFAULT_INGEST_BATCH_SIZE,
1086 606 : virtual_file_io_engine: DEFAULT_VIRTUAL_FILE_IO_ENGINE.parse().unwrap(),
1087 606 : max_vectored_read_bytes: MaxVectoredReadBytes(
1088 606 : NonZeroUsize::new(defaults::DEFAULT_MAX_VECTORED_READ_BYTES)
1089 606 : .expect("Invalid default constant"),
1090 606 : ),
1091 606 : image_compression: defaults::DEFAULT_IMAGE_COMPRESSION.parse().unwrap(),
1092 606 : ephemeral_bytes_per_memory_kb: defaults::DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB,
1093 606 : l0_flush: L0FlushConfig::default(),
1094 606 : compact_level0_phase1_value_access: CompactL0Phase1ValueAccess::default(),
1095 606 : virtual_file_direct_io: virtual_file::DirectIoMode::default(),
1096 606 : io_buffer_alignment: defaults::DEFAULT_IO_BUFFER_ALIGNMENT,
1097 606 : }
1098 606 : }
1099 : }
1100 :
1101 0 : #[derive(Deserialize)]
1102 : #[serde(deny_unknown_fields)]
1103 : pub struct PageserverIdentity {
1104 : pub id: NodeId,
1105 : }
1106 :
1107 : // Helper functions to parse a toml Item
1108 :
1109 246 : fn parse_toml_string(name: &str, item: &Item) -> Result<String> {
1110 246 : let s = item
1111 246 : .as_str()
1112 246 : .with_context(|| format!("configure option {name} is not a string"))?;
1113 246 : Ok(s.to_string())
1114 246 : }
1115 :
1116 60 : fn parse_toml_u64(name: &str, item: &Item) -> Result<u64> {
1117 : // A toml integer is signed, so it cannot represent the full range of an u64. That's OK
1118 : // for our use, though.
1119 60 : let i: i64 = item
1120 60 : .as_integer()
1121 60 : .with_context(|| format!("configure option {name} is not an integer"))?;
1122 60 : if i < 0 {
1123 0 : bail!("configure option {name} cannot be negative");
1124 60 : }
1125 60 : Ok(i as u64)
1126 60 : }
1127 :
1128 0 : fn parse_toml_bool(name: &str, item: &Item) -> Result<bool> {
1129 0 : item.as_bool()
1130 0 : .with_context(|| format!("configure option {name} is not a bool"))
1131 0 : }
1132 :
1133 162 : fn parse_toml_duration(name: &str, item: &Item) -> Result<Duration> {
1134 162 : let s = item
1135 162 : .as_str()
1136 162 : .with_context(|| format!("configure option {name} is not a string"))?;
1137 :
1138 162 : Ok(humantime::parse_duration(s)?)
1139 162 : }
1140 :
1141 0 : fn parse_toml_from_str<T>(name: &str, item: &Item) -> anyhow::Result<T>
1142 0 : where
1143 0 : T: FromStr,
1144 0 : <T as FromStr>::Err: std::fmt::Display,
1145 0 : {
1146 0 : let v = item
1147 0 : .as_str()
1148 0 : .with_context(|| format!("configure option {name} is not a string"))?;
1149 0 : T::from_str(v).map_err(|e| {
1150 0 : anyhow!(
1151 0 : "Failed to parse string as {parse_type} for configure option {name}: {e}",
1152 0 : parse_type = stringify!(T)
1153 0 : )
1154 0 : })
1155 0 : }
1156 :
1157 6 : fn deserialize_from_item<T>(name: &str, item: &Item) -> anyhow::Result<T>
1158 6 : where
1159 6 : T: serde::de::DeserializeOwned,
1160 6 : {
1161 : // ValueDeserializer::new is not public, so use the ValueDeserializer's documented way
1162 6 : let deserializer = match item.clone().into_value() {
1163 6 : Ok(value) => value.into_deserializer(),
1164 0 : Err(item) => anyhow::bail!("toml_edit::Item '{item}' is not a toml_edit::Value"),
1165 : };
1166 6 : T::deserialize(deserializer).with_context(|| format!("deserializing item for node {name}"))
1167 6 : }
1168 :
1169 : /// Configurable semaphore permits setting.
1170 : ///
1171 : /// Does not allow semaphore permits to be zero, because at runtime initially zero permits and empty
1172 : /// semaphore cannot be distinguished, leading any feature using these to await forever (or until
1173 : /// new permits are added).
1174 : #[derive(Debug, Clone)]
1175 : pub struct ConfigurableSemaphore {
1176 : initial_permits: NonZeroUsize,
1177 : inner: std::sync::Arc<tokio::sync::Semaphore>,
1178 : }
1179 :
1180 : impl ConfigurableSemaphore {
1181 : pub const DEFAULT_INITIAL: NonZeroUsize = match NonZeroUsize::new(1) {
1182 : Some(x) => x,
1183 : None => panic!("const unwrap is not yet stable"),
1184 : };
1185 :
1186 : /// Initializse using a non-zero amount of permits.
1187 : ///
1188 : /// Require a non-zero initial permits, because using permits == 0 is a crude way to disable a
1189 : /// feature such as [`Tenant::gather_size_inputs`]. Otherwise any semaphore using future will
1190 : /// behave like [`futures::future::pending`], just waiting until new permits are added.
1191 : ///
1192 : /// [`Tenant::gather_size_inputs`]: crate::tenant::Tenant::gather_size_inputs
1193 1998 : pub fn new(initial_permits: NonZeroUsize) -> Self {
1194 1998 : ConfigurableSemaphore {
1195 1998 : initial_permits,
1196 1998 : inner: std::sync::Arc::new(tokio::sync::Semaphore::new(initial_permits.get())),
1197 1998 : }
1198 1998 : }
1199 :
1200 : /// Returns the configured amount of permits.
1201 0 : pub fn initial_permits(&self) -> NonZeroUsize {
1202 0 : self.initial_permits
1203 0 : }
1204 : }
1205 :
1206 : impl Default for ConfigurableSemaphore {
1207 1236 : fn default() -> Self {
1208 1236 : Self::new(Self::DEFAULT_INITIAL)
1209 1236 : }
1210 : }
1211 :
1212 : impl PartialEq for ConfigurableSemaphore {
1213 36 : fn eq(&self, other: &Self) -> bool {
1214 36 : // the number of permits can be increased at runtime, so we cannot really fulfill the
1215 36 : // PartialEq value equality otherwise
1216 36 : self.initial_permits == other.initial_permits
1217 36 : }
1218 : }
1219 :
1220 : impl Eq for ConfigurableSemaphore {}
1221 :
1222 : impl ConfigurableSemaphore {
1223 0 : pub fn inner(&self) -> &std::sync::Arc<tokio::sync::Semaphore> {
1224 0 : &self.inner
1225 0 : }
1226 : }
1227 :
1228 : #[cfg(test)]
1229 : mod tests {
1230 : use std::{fs, num::NonZeroU32};
1231 :
1232 : use camino_tempfile::{tempdir, Utf8TempDir};
1233 : use pageserver_api::models::EvictionPolicy;
1234 : use remote_storage::{RemoteStorageKind, S3Config};
1235 : use utils::serde_percent::Percent;
1236 :
1237 : use super::*;
1238 : use crate::DEFAULT_PG_VERSION;
1239 :
1240 : const ALL_BASE_VALUES_TOML: &str = r#"
1241 : # Initial configuration file created by 'pageserver --init'
1242 :
1243 : listen_pg_addr = '127.0.0.1:64000'
1244 : listen_http_addr = '127.0.0.1:9898'
1245 :
1246 : wait_lsn_timeout = '111 s'
1247 : wal_redo_timeout = '111 s'
1248 :
1249 : page_cache_size = 444
1250 : max_file_descriptors = 333
1251 :
1252 : # initial superuser role name to use when creating a new tenant
1253 : initial_superuser_name = 'zzzz'
1254 :
1255 : metric_collection_interval = '222 s'
1256 : metric_collection_endpoint = 'http://localhost:80/metrics'
1257 : synthetic_size_calculation_interval = '333 s'
1258 :
1259 : log_format = 'json'
1260 : background_task_maximum_delay = '334 s'
1261 :
1262 : "#;
1263 :
1264 : #[test]
1265 6 : fn parse_defaults() -> anyhow::Result<()> {
1266 6 : let tempdir = tempdir()?;
1267 6 : let (workdir, pg_distrib_dir) = prepare_fs(&tempdir)?;
1268 6 : let broker_endpoint = storage_broker::DEFAULT_ENDPOINT;
1269 6 : // we have to create dummy values to overcome the validation errors
1270 6 : let config_string =
1271 6 : format!("pg_distrib_dir='{pg_distrib_dir}'\nbroker_endpoint = '{broker_endpoint}'",);
1272 6 : let toml = config_string.parse()?;
1273 :
1274 6 : let parsed_config = PageServerConf::parse_and_validate(NodeId(10), &toml, &workdir)
1275 6 : .unwrap_or_else(|e| panic!("Failed to parse config '{config_string}', reason: {e:?}"));
1276 6 :
1277 6 : assert_eq!(
1278 6 : parsed_config,
1279 6 : PageServerConf {
1280 6 : id: NodeId(10),
1281 6 : listen_pg_addr: defaults::DEFAULT_PG_LISTEN_ADDR.to_string(),
1282 6 : listen_http_addr: defaults::DEFAULT_HTTP_LISTEN_ADDR.to_string(),
1283 6 : availability_zone: None,
1284 6 : wait_lsn_timeout: humantime::parse_duration(defaults::DEFAULT_WAIT_LSN_TIMEOUT)?,
1285 6 : wal_redo_timeout: humantime::parse_duration(defaults::DEFAULT_WAL_REDO_TIMEOUT)?,
1286 6 : superuser: defaults::DEFAULT_SUPERUSER.to_string(),
1287 6 : page_cache_size: defaults::DEFAULT_PAGE_CACHE_SIZE,
1288 6 : max_file_descriptors: defaults::DEFAULT_MAX_FILE_DESCRIPTORS,
1289 6 : workdir,
1290 6 : pg_distrib_dir,
1291 6 : http_auth_type: AuthType::Trust,
1292 6 : pg_auth_type: AuthType::Trust,
1293 6 : auth_validation_public_key_path: None,
1294 6 : remote_storage_config: None,
1295 6 : default_tenant_conf: TenantConf::default(),
1296 6 : broker_endpoint: storage_broker::DEFAULT_ENDPOINT.parse().unwrap(),
1297 6 : broker_keepalive_interval: humantime::parse_duration(
1298 6 : storage_broker::DEFAULT_KEEPALIVE_INTERVAL
1299 6 : )?,
1300 6 : log_format: LogFormat::from_str(defaults::DEFAULT_LOG_FORMAT).unwrap(),
1301 6 : concurrent_tenant_warmup: ConfigurableSemaphore::new(
1302 6 : NonZeroUsize::new(DEFAULT_CONCURRENT_TENANT_WARMUP).unwrap()
1303 6 : ),
1304 6 : concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore::default(),
1305 6 : eviction_task_immitated_concurrent_logical_size_queries:
1306 6 : ConfigurableSemaphore::default(),
1307 6 : metric_collection_interval: humantime::parse_duration(
1308 6 : defaults::DEFAULT_METRIC_COLLECTION_INTERVAL
1309 6 : )?,
1310 6 : metric_collection_endpoint: defaults::DEFAULT_METRIC_COLLECTION_ENDPOINT,
1311 6 : metric_collection_bucket: None,
1312 6 : synthetic_size_calculation_interval: humantime::parse_duration(
1313 6 : defaults::DEFAULT_SYNTHETIC_SIZE_CALCULATION_INTERVAL
1314 6 : )?,
1315 6 : disk_usage_based_eviction: None,
1316 6 : test_remote_failures: 0,
1317 6 : ondemand_download_behavior_treat_error_as_warn: false,
1318 6 : background_task_maximum_delay: humantime::parse_duration(
1319 6 : defaults::DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY
1320 6 : )?,
1321 6 : control_plane_api: None,
1322 6 : control_plane_api_token: None,
1323 6 : control_plane_emergency_mode: false,
1324 6 : heatmap_upload_concurrency: defaults::DEFAULT_HEATMAP_UPLOAD_CONCURRENCY,
1325 6 : secondary_download_concurrency: defaults::DEFAULT_SECONDARY_DOWNLOAD_CONCURRENCY,
1326 6 : ingest_batch_size: defaults::DEFAULT_INGEST_BATCH_SIZE,
1327 6 : virtual_file_io_engine: DEFAULT_VIRTUAL_FILE_IO_ENGINE.parse().unwrap(),
1328 6 : max_vectored_read_bytes: MaxVectoredReadBytes(
1329 6 : NonZeroUsize::new(defaults::DEFAULT_MAX_VECTORED_READ_BYTES)
1330 6 : .expect("Invalid default constant")
1331 6 : ),
1332 6 : image_compression: defaults::DEFAULT_IMAGE_COMPRESSION.parse().unwrap(),
1333 6 : ephemeral_bytes_per_memory_kb: defaults::DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB,
1334 6 : l0_flush: L0FlushConfig::default(),
1335 6 : compact_level0_phase1_value_access: CompactL0Phase1ValueAccess::default(),
1336 6 : virtual_file_direct_io: virtual_file::DirectIoMode::default(),
1337 : io_buffer_alignment: defaults::DEFAULT_IO_BUFFER_ALIGNMENT,
1338 : },
1339 0 : "Correct defaults should be used when no config values are provided"
1340 : );
1341 :
1342 6 : Ok(())
1343 6 : }
1344 :
1345 : #[test]
1346 6 : fn parse_basic_config() -> anyhow::Result<()> {
1347 6 : let tempdir = tempdir()?;
1348 6 : let (workdir, pg_distrib_dir) = prepare_fs(&tempdir)?;
1349 6 : let broker_endpoint = storage_broker::DEFAULT_ENDPOINT;
1350 6 :
1351 6 : let config_string = format!(
1352 6 : "{ALL_BASE_VALUES_TOML}pg_distrib_dir='{pg_distrib_dir}'\nbroker_endpoint = '{broker_endpoint}'",
1353 6 : );
1354 6 : let toml = config_string.parse()?;
1355 :
1356 6 : let parsed_config = PageServerConf::parse_and_validate(NodeId(10), &toml, &workdir)
1357 6 : .unwrap_or_else(|e| panic!("Failed to parse config '{config_string}', reason: {e:?}"));
1358 6 :
1359 6 : assert_eq!(
1360 6 : parsed_config,
1361 6 : PageServerConf {
1362 6 : id: NodeId(10),
1363 6 : listen_pg_addr: "127.0.0.1:64000".to_string(),
1364 6 : listen_http_addr: "127.0.0.1:9898".to_string(),
1365 6 : availability_zone: None,
1366 6 : wait_lsn_timeout: Duration::from_secs(111),
1367 6 : wal_redo_timeout: Duration::from_secs(111),
1368 6 : superuser: "zzzz".to_string(),
1369 6 : page_cache_size: 444,
1370 6 : max_file_descriptors: 333,
1371 6 : workdir,
1372 6 : pg_distrib_dir,
1373 6 : http_auth_type: AuthType::Trust,
1374 6 : pg_auth_type: AuthType::Trust,
1375 6 : auth_validation_public_key_path: None,
1376 6 : remote_storage_config: None,
1377 6 : default_tenant_conf: TenantConf::default(),
1378 6 : broker_endpoint: storage_broker::DEFAULT_ENDPOINT.parse().unwrap(),
1379 6 : broker_keepalive_interval: Duration::from_secs(5),
1380 6 : log_format: LogFormat::Json,
1381 6 : concurrent_tenant_warmup: ConfigurableSemaphore::new(
1382 6 : NonZeroUsize::new(DEFAULT_CONCURRENT_TENANT_WARMUP).unwrap()
1383 6 : ),
1384 6 : concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore::default(),
1385 6 : eviction_task_immitated_concurrent_logical_size_queries:
1386 6 : ConfigurableSemaphore::default(),
1387 6 : metric_collection_interval: Duration::from_secs(222),
1388 6 : metric_collection_endpoint: Some(Url::parse("http://localhost:80/metrics")?),
1389 6 : metric_collection_bucket: None,
1390 6 : synthetic_size_calculation_interval: Duration::from_secs(333),
1391 6 : disk_usage_based_eviction: None,
1392 6 : test_remote_failures: 0,
1393 6 : ondemand_download_behavior_treat_error_as_warn: false,
1394 6 : background_task_maximum_delay: Duration::from_secs(334),
1395 6 : control_plane_api: None,
1396 6 : control_plane_api_token: None,
1397 6 : control_plane_emergency_mode: false,
1398 6 : heatmap_upload_concurrency: defaults::DEFAULT_HEATMAP_UPLOAD_CONCURRENCY,
1399 6 : secondary_download_concurrency: defaults::DEFAULT_SECONDARY_DOWNLOAD_CONCURRENCY,
1400 6 : ingest_batch_size: 100,
1401 6 : virtual_file_io_engine: DEFAULT_VIRTUAL_FILE_IO_ENGINE.parse().unwrap(),
1402 6 : max_vectored_read_bytes: MaxVectoredReadBytes(
1403 6 : NonZeroUsize::new(defaults::DEFAULT_MAX_VECTORED_READ_BYTES)
1404 6 : .expect("Invalid default constant")
1405 6 : ),
1406 6 : image_compression: defaults::DEFAULT_IMAGE_COMPRESSION.parse().unwrap(),
1407 6 : ephemeral_bytes_per_memory_kb: defaults::DEFAULT_EPHEMERAL_BYTES_PER_MEMORY_KB,
1408 6 : l0_flush: L0FlushConfig::default(),
1409 6 : compact_level0_phase1_value_access: CompactL0Phase1ValueAccess::default(),
1410 6 : virtual_file_direct_io: virtual_file::DirectIoMode::default(),
1411 : io_buffer_alignment: defaults::DEFAULT_IO_BUFFER_ALIGNMENT,
1412 : },
1413 0 : "Should be able to parse all basic config values correctly"
1414 : );
1415 :
1416 6 : Ok(())
1417 6 : }
1418 :
1419 : #[test]
1420 6 : fn parse_remote_fs_storage_config() -> anyhow::Result<()> {
1421 6 : let tempdir = tempdir()?;
1422 6 : let (workdir, pg_distrib_dir) = prepare_fs(&tempdir)?;
1423 6 : let broker_endpoint = "http://127.0.0.1:7777";
1424 6 :
1425 6 : let local_storage_path = tempdir.path().join("local_remote_storage");
1426 6 :
1427 6 : let identical_toml_declarations = &[
1428 6 : format!(
1429 6 : r#"[remote_storage]
1430 6 : local_path = '{local_storage_path}'"#,
1431 6 : ),
1432 6 : format!("remote_storage={{local_path='{local_storage_path}'}}"),
1433 6 : ];
1434 :
1435 18 : for remote_storage_config_str in identical_toml_declarations {
1436 12 : let config_string = format!(
1437 12 : r#"{ALL_BASE_VALUES_TOML}
1438 12 : pg_distrib_dir='{pg_distrib_dir}'
1439 12 : broker_endpoint = '{broker_endpoint}'
1440 12 :
1441 12 : {remote_storage_config_str}"#,
1442 12 : );
1443 :
1444 12 : let toml = config_string.parse()?;
1445 :
1446 12 : let parsed_remote_storage_config =
1447 12 : PageServerConf::parse_and_validate(NodeId(10), &toml, &workdir)
1448 12 : .unwrap_or_else(|e| {
1449 0 : panic!("Failed to parse config '{config_string}', reason: {e:?}")
1450 12 : })
1451 12 : .remote_storage_config
1452 12 : .expect("Should have remote storage config for the local FS");
1453 12 :
1454 12 : assert_eq!(
1455 12 : parsed_remote_storage_config,
1456 12 : RemoteStorageConfig {
1457 12 : storage: RemoteStorageKind::LocalFs { local_path: local_storage_path.clone() },
1458 12 : timeout: RemoteStorageConfig::DEFAULT_TIMEOUT,
1459 12 : },
1460 0 : "Remote storage config should correctly parse the local FS config and fill other storage defaults"
1461 : );
1462 : }
1463 6 : Ok(())
1464 6 : }
1465 :
1466 : #[test]
1467 6 : fn parse_remote_s3_storage_config() -> anyhow::Result<()> {
1468 6 : let tempdir = tempdir()?;
1469 6 : let (workdir, pg_distrib_dir) = prepare_fs(&tempdir)?;
1470 :
1471 6 : let bucket_name = "some-sample-bucket".to_string();
1472 6 : let bucket_region = "eu-north-1".to_string();
1473 6 : let prefix_in_bucket = "test_prefix".to_string();
1474 6 : let endpoint = "http://localhost:5000".to_string();
1475 6 : let max_concurrent_syncs = NonZeroUsize::new(111).unwrap();
1476 6 : let max_sync_errors = NonZeroU32::new(222).unwrap();
1477 6 : let s3_concurrency_limit = NonZeroUsize::new(333).unwrap();
1478 6 : let broker_endpoint = "http://127.0.0.1:7777";
1479 6 :
1480 6 : let identical_toml_declarations = &[
1481 6 : format!(
1482 6 : r#"[remote_storage]
1483 6 : max_concurrent_syncs = {max_concurrent_syncs}
1484 6 : max_sync_errors = {max_sync_errors}
1485 6 : bucket_name = '{bucket_name}'
1486 6 : bucket_region = '{bucket_region}'
1487 6 : prefix_in_bucket = '{prefix_in_bucket}'
1488 6 : endpoint = '{endpoint}'
1489 6 : concurrency_limit = {s3_concurrency_limit}"#
1490 6 : ),
1491 6 : format!(
1492 6 : "remote_storage={{max_concurrent_syncs={max_concurrent_syncs}, max_sync_errors={max_sync_errors}, bucket_name='{bucket_name}',\
1493 6 : bucket_region='{bucket_region}', prefix_in_bucket='{prefix_in_bucket}', endpoint='{endpoint}', concurrency_limit={s3_concurrency_limit}}}",
1494 6 : ),
1495 6 : ];
1496 :
1497 18 : for remote_storage_config_str in identical_toml_declarations {
1498 12 : let config_string = format!(
1499 12 : r#"{ALL_BASE_VALUES_TOML}
1500 12 : pg_distrib_dir='{pg_distrib_dir}'
1501 12 : broker_endpoint = '{broker_endpoint}'
1502 12 :
1503 12 : {remote_storage_config_str}"#,
1504 12 : );
1505 :
1506 12 : let toml = config_string.parse()?;
1507 :
1508 12 : let parsed_remote_storage_config =
1509 12 : PageServerConf::parse_and_validate(NodeId(10), &toml, &workdir)
1510 12 : .unwrap_or_else(|e| {
1511 0 : panic!("Failed to parse config '{config_string}', reason: {e:?}")
1512 12 : })
1513 12 : .remote_storage_config
1514 12 : .expect("Should have remote storage config for S3");
1515 12 :
1516 12 : assert_eq!(
1517 12 : parsed_remote_storage_config,
1518 12 : RemoteStorageConfig {
1519 12 : storage: RemoteStorageKind::AwsS3(S3Config {
1520 12 : bucket_name: bucket_name.clone(),
1521 12 : bucket_region: bucket_region.clone(),
1522 12 : prefix_in_bucket: Some(prefix_in_bucket.clone()),
1523 12 : endpoint: Some(endpoint.clone()),
1524 12 : concurrency_limit: s3_concurrency_limit,
1525 12 : max_keys_per_list_response: None,
1526 12 : upload_storage_class: None,
1527 12 : }),
1528 12 : timeout: RemoteStorageConfig::DEFAULT_TIMEOUT,
1529 12 : },
1530 0 : "Remote storage config should correctly parse the S3 config"
1531 : );
1532 : }
1533 6 : Ok(())
1534 6 : }
1535 :
1536 : #[test]
1537 6 : fn parse_incorrect_tenant_config() -> anyhow::Result<()> {
1538 6 : let config_string = r#"
1539 6 : [tenant_config]
1540 6 : checkpoint_distance = -1 # supposed to be an u64
1541 6 : "#
1542 6 : .to_string();
1543 :
1544 6 : let toml: Document = config_string.parse()?;
1545 6 : let item = toml.get("tenant_config").unwrap();
1546 6 : let error = TenantConfOpt::try_from(item.to_owned()).unwrap_err();
1547 6 :
1548 6 : let expected_error_str = "checkpoint_distance: invalid value: integer `-1`, expected u64";
1549 6 : assert_eq!(error.to_string(), expected_error_str);
1550 :
1551 6 : Ok(())
1552 6 : }
1553 :
1554 : #[test]
1555 6 : fn parse_override_tenant_config() -> anyhow::Result<()> {
1556 6 : let config_string = r#"tenant_config={ min_resident_size_override = 400 }"#.to_string();
1557 :
1558 6 : let toml: Document = config_string.parse()?;
1559 6 : let item = toml.get("tenant_config").unwrap();
1560 6 : let conf = TenantConfOpt::try_from(item.to_owned()).unwrap();
1561 6 :
1562 6 : assert_eq!(conf.min_resident_size_override, Some(400));
1563 :
1564 6 : Ok(())
1565 6 : }
1566 :
1567 : #[test]
1568 6 : fn eviction_pageserver_config_parse() -> anyhow::Result<()> {
1569 6 : let tempdir = tempdir()?;
1570 6 : let (workdir, pg_distrib_dir) = prepare_fs(&tempdir)?;
1571 :
1572 6 : let pageserver_conf_toml = format!(
1573 6 : r#"pg_distrib_dir = "{pg_distrib_dir}"
1574 6 : metric_collection_endpoint = "http://sample.url"
1575 6 : metric_collection_interval = "10min"
1576 6 :
1577 6 : [disk_usage_based_eviction]
1578 6 : max_usage_pct = 80
1579 6 : min_avail_bytes = 0
1580 6 : period = "10s"
1581 6 :
1582 6 : [tenant_config]
1583 6 : evictions_low_residence_duration_metric_threshold = "20m"
1584 6 :
1585 6 : [tenant_config.eviction_policy]
1586 6 : kind = "LayerAccessThreshold"
1587 6 : period = "20m"
1588 6 : threshold = "20m"
1589 6 : "#,
1590 6 : );
1591 6 : let toml: Document = pageserver_conf_toml.parse()?;
1592 6 : let conf = PageServerConf::parse_and_validate(NodeId(333), &toml, &workdir)?;
1593 :
1594 6 : assert_eq!(conf.pg_distrib_dir, pg_distrib_dir);
1595 6 : assert_eq!(
1596 6 : conf.metric_collection_endpoint,
1597 6 : Some("http://sample.url".parse().unwrap())
1598 6 : );
1599 6 : assert_eq!(
1600 6 : conf.metric_collection_interval,
1601 6 : Duration::from_secs(10 * 60)
1602 6 : );
1603 6 : assert_eq!(
1604 6 : conf.default_tenant_conf
1605 6 : .evictions_low_residence_duration_metric_threshold,
1606 6 : Duration::from_secs(20 * 60)
1607 6 : );
1608 :
1609 : // Assert that the node id provided by the indentity file (threaded
1610 : // through the call to [`PageServerConf::parse_and_validate`] is
1611 : // used.
1612 6 : assert_eq!(conf.id, NodeId(333));
1613 6 : assert_eq!(
1614 6 : conf.disk_usage_based_eviction,
1615 6 : Some(DiskUsageEvictionTaskConfig {
1616 6 : max_usage_pct: Percent::new(80).unwrap(),
1617 6 : min_avail_bytes: 0,
1618 6 : period: Duration::from_secs(10),
1619 6 : #[cfg(feature = "testing")]
1620 6 : mock_statvfs: None,
1621 6 : eviction_order: Default::default(),
1622 6 : })
1623 6 : );
1624 :
1625 6 : match &conf.default_tenant_conf.eviction_policy {
1626 6 : EvictionPolicy::LayerAccessThreshold(eviction_threshold) => {
1627 6 : assert_eq!(eviction_threshold.period, Duration::from_secs(20 * 60));
1628 6 : assert_eq!(eviction_threshold.threshold, Duration::from_secs(20 * 60));
1629 : }
1630 0 : other => unreachable!("Unexpected eviction policy tenant settings: {other:?}"),
1631 : }
1632 :
1633 6 : Ok(())
1634 6 : }
1635 :
1636 : #[test]
1637 6 : fn parse_imitation_only_pageserver_config() {
1638 6 : let tempdir = tempdir().unwrap();
1639 6 : let (workdir, pg_distrib_dir) = prepare_fs(&tempdir).unwrap();
1640 6 :
1641 6 : let pageserver_conf_toml = format!(
1642 6 : r#"pg_distrib_dir = "{pg_distrib_dir}"
1643 6 : metric_collection_endpoint = "http://sample.url"
1644 6 : metric_collection_interval = "10min"
1645 6 :
1646 6 : [tenant_config]
1647 6 : evictions_low_residence_duration_metric_threshold = "20m"
1648 6 :
1649 6 : [tenant_config.eviction_policy]
1650 6 : kind = "OnlyImitiate"
1651 6 : period = "20m"
1652 6 : threshold = "20m"
1653 6 : "#,
1654 6 : );
1655 6 : let toml: Document = pageserver_conf_toml.parse().unwrap();
1656 6 : let conf = PageServerConf::parse_and_validate(NodeId(222), &toml, &workdir).unwrap();
1657 6 :
1658 6 : match &conf.default_tenant_conf.eviction_policy {
1659 6 : EvictionPolicy::OnlyImitiate(t) => {
1660 6 : assert_eq!(t.period, Duration::from_secs(20 * 60));
1661 6 : assert_eq!(t.threshold, Duration::from_secs(20 * 60));
1662 : }
1663 0 : other => unreachable!("Unexpected eviction policy tenant settings: {other:?}"),
1664 : }
1665 6 : }
1666 :
1667 : #[test]
1668 6 : fn empty_remote_storage_is_error() {
1669 6 : let tempdir = tempdir().unwrap();
1670 6 : let (workdir, _) = prepare_fs(&tempdir).unwrap();
1671 6 : let input = r#"
1672 6 : remote_storage = {}
1673 6 : "#;
1674 6 : let doc = toml_edit::Document::from_str(input).unwrap();
1675 6 : let err = PageServerConf::parse_and_validate(NodeId(222), &doc, &workdir)
1676 6 : .expect_err("empty remote_storage field should fail, don't specify it if you want no remote_storage");
1677 6 : assert!(format!("{err}").contains("remote_storage"), "{err}");
1678 6 : }
1679 :
1680 42 : fn prepare_fs(tempdir: &Utf8TempDir) -> anyhow::Result<(Utf8PathBuf, Utf8PathBuf)> {
1681 42 : let tempdir_path = tempdir.path();
1682 42 :
1683 42 : let workdir = tempdir_path.join("workdir");
1684 42 : fs::create_dir_all(&workdir)?;
1685 :
1686 42 : let pg_distrib_dir = tempdir_path.join("pg_distrib");
1687 42 : let pg_distrib_dir_versioned = pg_distrib_dir.join(format!("v{DEFAULT_PG_VERSION}"));
1688 42 : fs::create_dir_all(&pg_distrib_dir_versioned)?;
1689 42 : let postgres_bin_dir = pg_distrib_dir_versioned.join("bin");
1690 42 : fs::create_dir_all(&postgres_bin_dir)?;
1691 42 : fs::write(postgres_bin_dir.join("postgres"), "I'm postgres, trust me")?;
1692 :
1693 42 : Ok((workdir, pg_distrib_dir))
1694 42 : }
1695 : }
|