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