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 : pub mod ignored_fields;
8 :
9 : use std::env;
10 : use std::num::NonZeroUsize;
11 : use std::sync::Arc;
12 : use std::time::Duration;
13 :
14 : use anyhow::{Context, ensure};
15 : use camino::{Utf8Path, Utf8PathBuf};
16 : use once_cell::sync::OnceCell;
17 : use pageserver_api::config::{
18 : DiskUsageEvictionTaskConfig, MaxGetVectoredKeys, MaxVectoredReadBytes,
19 : PageServicePipeliningConfig, PageServicePipeliningConfigPipelined, PostHogConfig,
20 : };
21 : use pageserver_api::models::ImageCompressionAlgorithm;
22 : use pageserver_api::shard::TenantShardId;
23 : use pem::Pem;
24 : use postgres_backend::AuthType;
25 : use postgres_ffi::PgMajorVersion;
26 : use remote_storage::{RemotePath, RemoteStorageConfig};
27 : use reqwest::Url;
28 : use storage_broker::Uri;
29 : use utils::id::{NodeId, TimelineId};
30 : use utils::logging::{LogFormat, SecretString};
31 :
32 : use crate::tenant::storage_layer::inmemory_layer::IndexEntry;
33 : use crate::tenant::{TENANTS_SEGMENT_NAME, TIMELINES_SEGMENT_NAME};
34 : use crate::virtual_file::io_engine;
35 : use crate::{TENANT_HEATMAP_BASENAME, TENANT_LOCATION_CONFIG_NAME, virtual_file};
36 :
37 : /// Global state of pageserver.
38 : ///
39 : /// It's mostly immutable configuration, but some semaphores and the
40 : /// like crept in over time and the name stuck.
41 : ///
42 : /// Instantiated by deserializing `pageserver.toml` into [`pageserver_api::config::ConfigToml`]
43 : /// and passing that to [`PageServerConf::parse_and_validate`].
44 : ///
45 : /// # Adding a New Field
46 : ///
47 : /// 1. Add the field to `pageserver_api::config::ConfigToml`.
48 : /// 2. Fix compiler errors (exhaustive destructuring will guide you).
49 : ///
50 : /// For fields that require additional validation or filling in of defaults at runtime,
51 : /// check for examples in the [`PageServerConf::parse_and_validate`] method.
52 : #[derive(Debug, Clone)]
53 : pub struct PageServerConf {
54 : // Identifier of that particular pageserver so e g safekeepers
55 : // can safely distinguish different pageservers
56 : pub id: NodeId,
57 :
58 : /// Example (default): 127.0.0.1:64000
59 : pub listen_pg_addr: String,
60 : /// Example (default): 127.0.0.1:9898
61 : pub listen_http_addr: String,
62 : /// Example: 127.0.0.1:9899
63 : pub listen_https_addr: Option<String>,
64 : /// If set, expose a gRPC API on this address.
65 : /// Example: 127.0.0.1:51051
66 : ///
67 : /// EXPERIMENTAL: this protocol is unstable and under active development.
68 : pub listen_grpc_addr: Option<String>,
69 :
70 : /// Path to a file with certificate's private key for https and gRPC API.
71 : /// Default: server.key
72 : pub ssl_key_file: Utf8PathBuf,
73 : /// Path to a file with a X509 certificate for https and gRPC API.
74 : /// Default: server.crt
75 : pub ssl_cert_file: Utf8PathBuf,
76 : /// Period to reload certificate and private key from files.
77 : /// Default: 60s.
78 : pub ssl_cert_reload_period: Duration,
79 : /// Trusted root CA certificates to use in https APIs in PEM format.
80 : pub ssl_ca_certs: Vec<Pem>,
81 :
82 : /// Current availability zone. Used for traffic metrics.
83 : pub availability_zone: Option<String>,
84 :
85 : // Timeout when waiting for WAL receiver to catch up to an LSN given in a GetPage@LSN call.
86 : pub wait_lsn_timeout: Duration,
87 : // How long to wait for WAL redo to complete.
88 : pub wal_redo_timeout: Duration,
89 :
90 : pub superuser: String,
91 : pub locale: String,
92 :
93 : pub page_cache_size: usize,
94 : pub max_file_descriptors: usize,
95 :
96 : // Repository directory, relative to current working directory.
97 : // Normally, the page server changes the current working directory
98 : // to the repository, and 'workdir' is always '.'. But we don't do
99 : // that during unit testing, because the current directory is global
100 : // to the process but different unit tests work on different
101 : // repositories.
102 : pub workdir: Utf8PathBuf,
103 :
104 : pub pg_distrib_dir: Utf8PathBuf,
105 :
106 : // Authentication
107 : /// authentication method for the HTTP mgmt API
108 : pub http_auth_type: AuthType,
109 : /// authentication method for libpq connections from compute
110 : pub pg_auth_type: AuthType,
111 : /// authentication method for gRPC connections from compute
112 : pub grpc_auth_type: AuthType,
113 : /// Path to a file or directory containing public key(s) for verifying JWT tokens.
114 : /// Used for both mgmt and compute auth, if enabled.
115 : pub auth_validation_public_key_path: Option<Utf8PathBuf>,
116 :
117 : pub remote_storage_config: Option<RemoteStorageConfig>,
118 :
119 : pub default_tenant_conf: pageserver_api::config::TenantConfigToml,
120 :
121 : /// Storage broker endpoints to connect to.
122 : pub broker_endpoint: Uri,
123 : pub broker_keepalive_interval: Duration,
124 :
125 : pub log_format: LogFormat,
126 :
127 : /// Number of tenants which will be concurrently loaded from remote storage proactively on startup or attach.
128 : ///
129 : /// A lower value implicitly deprioritizes loading such tenants, vs. other work in the system.
130 : pub concurrent_tenant_warmup: ConfigurableSemaphore,
131 :
132 : /// Number of concurrent [`TenantShard::gather_size_inputs`](crate::tenant::TenantShard::gather_size_inputs) allowed.
133 : pub concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore,
134 : /// Limit of concurrent [`TenantShard::gather_size_inputs`] issued by module `eviction_task`.
135 : /// The number of permits is the same as `concurrent_tenant_size_logical_size_queries`.
136 : /// See the comment in `eviction_task` for details.
137 : ///
138 : /// [`TenantShard::gather_size_inputs`]: crate::tenant::TenantShard::gather_size_inputs
139 : pub eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore,
140 :
141 : // How often to collect metrics and send them to the metrics endpoint.
142 : pub metric_collection_interval: Duration,
143 : // How often to send unchanged cached metrics to the metrics endpoint.
144 : pub metric_collection_endpoint: Option<Url>,
145 : pub metric_collection_bucket: Option<RemoteStorageConfig>,
146 : pub synthetic_size_calculation_interval: Duration,
147 :
148 : pub disk_usage_based_eviction: DiskUsageEvictionTaskConfig,
149 :
150 : // The number of allowed failures in remote storage operations.
151 : pub test_remote_failures: u64,
152 : // The probability of failure in remote storage operations. Only works when test_remote_failures > 1.
153 : // Use 100 for 100% failure, 0 for no failure.
154 : pub test_remote_failures_probability: u64,
155 :
156 : pub ondemand_download_behavior_treat_error_as_warn: bool,
157 :
158 : /// How long will background tasks be delayed at most after initial load of tenants.
159 : ///
160 : /// Our largest initialization completions are in the range of 100-200s, so perhaps 10s works
161 : /// as we now isolate initial loading, initial logical size calculation and background tasks.
162 : /// Smaller nodes will have background tasks "not running" for this long unless every timeline
163 : /// has it's initial logical size calculated. Not running background tasks for some seconds is
164 : /// not terrible.
165 : pub background_task_maximum_delay: Duration,
166 :
167 : pub control_plane_api: Url,
168 :
169 : /// JWT token for use with the control plane API.
170 : pub control_plane_api_token: Option<SecretString>,
171 :
172 : pub import_pgdata_upcall_api: Option<Url>,
173 : pub import_pgdata_upcall_api_token: Option<SecretString>,
174 : pub import_pgdata_aws_endpoint_url: Option<Url>,
175 :
176 : /// If true, pageserver will make best-effort to operate without a control plane: only
177 : /// for use in major incidents.
178 : pub control_plane_emergency_mode: bool,
179 :
180 : /// How many heatmap uploads may be done concurrency: lower values implicitly deprioritize
181 : /// heatmap uploads vs. other remote storage operations.
182 : pub heatmap_upload_concurrency: usize,
183 :
184 : /// How many remote storage downloads may be done for secondary tenants concurrently. Implicitly
185 : /// deprioritises secondary downloads vs. remote storage operations for attached tenants.
186 : pub secondary_download_concurrency: usize,
187 :
188 : /// Maximum number of WAL records to be ingested and committed at the same time
189 : pub ingest_batch_size: u64,
190 :
191 : pub virtual_file_io_engine: virtual_file::IoEngineKind,
192 :
193 : pub max_vectored_read_bytes: MaxVectoredReadBytes,
194 :
195 : /// Maximum number of keys to be read in a single get_vectored call.
196 : pub max_get_vectored_keys: MaxGetVectoredKeys,
197 :
198 : pub image_compression: ImageCompressionAlgorithm,
199 :
200 : /// Whether to offload archived timelines automatically
201 : pub timeline_offloading: bool,
202 :
203 : /// How many bytes of ephemeral layer content will we allow per kilobyte of RAM. When this
204 : /// is exceeded, we start proactively closing ephemeral layers to limit the total amount
205 : /// of ephemeral data.
206 : ///
207 : /// Setting this to zero disables limits on total ephemeral layer size.
208 : pub ephemeral_bytes_per_memory_kb: usize,
209 :
210 : pub l0_flush: crate::l0_flush::L0FlushConfig,
211 :
212 : /// Direct IO settings
213 : pub virtual_file_io_mode: virtual_file::IoMode,
214 :
215 : /// Optionally disable disk syncs (unsafe!)
216 : pub no_sync: bool,
217 :
218 : pub page_service_pipelining: pageserver_api::config::PageServicePipeliningConfig,
219 :
220 : pub get_vectored_concurrent_io: pageserver_api::config::GetVectoredConcurrentIo,
221 :
222 : /// Enable read path debugging. If enabled, read key errors will print a backtrace of the layer
223 : /// files read.
224 : pub enable_read_path_debugging: bool,
225 :
226 : /// Interpreted protocol feature: if enabled, validate that the logical WAL received from
227 : /// safekeepers does not have gaps.
228 : pub validate_wal_contiguity: bool,
229 :
230 : /// When set, the previously written to disk heatmap is loaded on tenant attach and used
231 : /// to avoid clobbering the heatmap from new, cold, attached locations.
232 : pub load_previous_heatmap: bool,
233 :
234 : /// When set, include visible layers in the next uploaded heatmaps of an unarchived timeline.
235 : pub generate_unarchival_heatmap: bool,
236 :
237 : pub tracing: Option<pageserver_api::config::Tracing>,
238 :
239 : /// Enable TLS in page service API.
240 : /// Does not force TLS: the client negotiates TLS usage during the handshake.
241 : /// Uses key and certificate from ssl_key_file/ssl_cert_file.
242 : pub enable_tls_page_service_api: bool,
243 :
244 : /// Run in development mode, which disables certain safety checks
245 : /// such as authentication requirements for HTTP and PostgreSQL APIs.
246 : /// This is insecure and should only be used in development environments.
247 : pub dev_mode: bool,
248 :
249 : /// PostHog integration config.
250 : pub posthog_config: Option<PostHogConfig>,
251 :
252 : pub timeline_import_config: pageserver_api::config::TimelineImportConfig,
253 :
254 : pub basebackup_cache_config: Option<pageserver_api::config::BasebackupCacheConfig>,
255 :
256 : /// Defines what is a big tenant for the purpose of image layer generation.
257 : /// See Timeline::should_check_if_image_layers_required
258 : pub image_layer_generation_large_timeline_threshold: Option<u64>,
259 :
260 : /// Controls whether to collect all metrics on each scrape or to return potentially stale
261 : /// results.
262 : pub force_metric_collection_on_scrape: bool,
263 : }
264 :
265 : /// Token for authentication to safekeepers
266 : ///
267 : /// We do not want to store this in a PageServerConf because the latter may be logged
268 : /// and/or serialized at a whim, while the token is secret. Currently this token is the
269 : /// same for accessing all tenants/timelines, but may become per-tenant/per-timeline in
270 : /// the future, more tokens and auth may arrive for storage broker, completely changing the logic.
271 : /// Hence, we resort to a global variable for now instead of passing the token from the
272 : /// startup code to the connection code through a dozen layers.
273 : pub static SAFEKEEPER_AUTH_TOKEN: OnceCell<Arc<String>> = OnceCell::new();
274 :
275 : impl PageServerConf {
276 : //
277 : // Repository paths, relative to workdir.
278 : //
279 :
280 4041 : pub fn tenants_path(&self) -> Utf8PathBuf {
281 4041 : self.workdir.join(TENANTS_SEGMENT_NAME)
282 4041 : }
283 :
284 36 : pub fn deletion_prefix(&self) -> Utf8PathBuf {
285 36 : self.workdir.join("deletion")
286 36 : }
287 :
288 0 : pub fn metadata_path(&self) -> Utf8PathBuf {
289 0 : self.workdir.join("metadata.json")
290 0 : }
291 :
292 0 : pub fn basebackup_cache_dir(&self) -> Utf8PathBuf {
293 0 : self.workdir.join("basebackup_cache")
294 0 : }
295 :
296 14 : pub fn deletion_list_path(&self, sequence: u64) -> Utf8PathBuf {
297 : // Encode a version in the filename, so that if we ever switch away from JSON we can
298 : // increment this.
299 : const VERSION: u8 = 1;
300 :
301 14 : self.deletion_prefix()
302 14 : .join(format!("{sequence:016x}-{VERSION:02x}.list"))
303 14 : }
304 :
305 12 : pub fn deletion_header_path(&self) -> Utf8PathBuf {
306 : // Encode a version in the filename, so that if we ever switch away from JSON we can
307 : // increment this.
308 : const VERSION: u8 = 1;
309 :
310 12 : self.deletion_prefix().join(format!("header-{VERSION:02x}"))
311 12 : }
312 :
313 4014 : pub fn tenant_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
314 4014 : self.tenants_path().join(tenant_shard_id.to_string())
315 4014 : }
316 :
317 : /// Points to a place in pageserver's local directory,
318 : /// where certain tenant's LocationConf be stored.
319 0 : pub(crate) fn tenant_location_config_path(
320 0 : &self,
321 0 : tenant_shard_id: &TenantShardId,
322 0 : ) -> Utf8PathBuf {
323 0 : self.tenant_path(tenant_shard_id)
324 0 : .join(TENANT_LOCATION_CONFIG_NAME)
325 0 : }
326 :
327 119 : pub(crate) fn tenant_heatmap_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
328 119 : self.tenant_path(tenant_shard_id)
329 119 : .join(TENANT_HEATMAP_BASENAME)
330 119 : }
331 :
332 3773 : pub fn timelines_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
333 3773 : self.tenant_path(tenant_shard_id)
334 3773 : .join(TIMELINES_SEGMENT_NAME)
335 3773 : }
336 :
337 3532 : pub fn timeline_path(
338 3532 : &self,
339 3532 : tenant_shard_id: &TenantShardId,
340 3532 : timeline_id: &TimelineId,
341 3532 : ) -> Utf8PathBuf {
342 3532 : self.timelines_path(tenant_shard_id)
343 3532 : .join(timeline_id.to_string())
344 3532 : }
345 :
346 : /// Turns storage remote path of a file into its local path.
347 0 : pub fn local_path(&self, remote_path: &RemotePath) -> Utf8PathBuf {
348 0 : remote_path.with_base(&self.workdir)
349 0 : }
350 :
351 : //
352 : // Postgres distribution paths
353 : //
354 12 : pub fn pg_distrib_dir(&self, pg_version: PgMajorVersion) -> anyhow::Result<Utf8PathBuf> {
355 12 : let path = self.pg_distrib_dir.clone();
356 :
357 12 : Ok(path.join(pg_version.v_str()))
358 12 : }
359 :
360 6 : pub fn pg_bin_dir(&self, pg_version: PgMajorVersion) -> anyhow::Result<Utf8PathBuf> {
361 6 : Ok(self.pg_distrib_dir(pg_version)?.join("bin"))
362 6 : }
363 6 : pub fn pg_lib_dir(&self, pg_version: PgMajorVersion) -> anyhow::Result<Utf8PathBuf> {
364 6 : Ok(self.pg_distrib_dir(pg_version)?.join("lib"))
365 6 : }
366 :
367 : /// Parse a configuration file (pageserver.toml) into a PageServerConf struct,
368 : /// validating the input and failing on errors.
369 : ///
370 : /// This leaves any options not present in the file in the built-in defaults.
371 138 : pub fn parse_and_validate(
372 138 : id: NodeId,
373 138 : config_toml: pageserver_api::config::ConfigToml,
374 138 : workdir: &Utf8Path,
375 138 : ) -> anyhow::Result<Self> {
376 : let pageserver_api::config::ConfigToml {
377 138 : listen_pg_addr,
378 138 : listen_http_addr,
379 138 : listen_https_addr,
380 138 : listen_grpc_addr,
381 138 : ssl_key_file,
382 138 : ssl_cert_file,
383 138 : ssl_cert_reload_period,
384 138 : ssl_ca_file,
385 138 : availability_zone,
386 138 : wait_lsn_timeout,
387 138 : wal_redo_timeout,
388 138 : superuser,
389 138 : locale,
390 138 : page_cache_size,
391 138 : max_file_descriptors,
392 138 : pg_distrib_dir,
393 138 : http_auth_type,
394 138 : pg_auth_type,
395 138 : grpc_auth_type,
396 138 : auth_validation_public_key_path,
397 138 : remote_storage,
398 138 : broker_endpoint,
399 138 : broker_keepalive_interval,
400 138 : log_format,
401 138 : metric_collection_interval,
402 138 : metric_collection_endpoint,
403 138 : metric_collection_bucket,
404 138 : synthetic_size_calculation_interval,
405 138 : disk_usage_based_eviction,
406 138 : test_remote_failures,
407 138 : test_remote_failures_probability,
408 138 : ondemand_download_behavior_treat_error_as_warn,
409 138 : background_task_maximum_delay,
410 138 : control_plane_api,
411 138 : control_plane_api_token,
412 138 : control_plane_emergency_mode,
413 138 : import_pgdata_upcall_api,
414 138 : import_pgdata_upcall_api_token,
415 138 : import_pgdata_aws_endpoint_url,
416 138 : heatmap_upload_concurrency,
417 138 : secondary_download_concurrency,
418 138 : ingest_batch_size,
419 138 : max_vectored_read_bytes,
420 138 : max_get_vectored_keys,
421 138 : image_compression,
422 138 : timeline_offloading,
423 138 : ephemeral_bytes_per_memory_kb,
424 138 : l0_flush,
425 138 : virtual_file_io_mode,
426 138 : concurrent_tenant_warmup,
427 138 : concurrent_tenant_size_logical_size_queries,
428 138 : virtual_file_io_engine,
429 138 : tenant_config,
430 138 : no_sync,
431 138 : page_service_pipelining,
432 138 : get_vectored_concurrent_io,
433 138 : enable_read_path_debugging,
434 138 : validate_wal_contiguity,
435 138 : load_previous_heatmap,
436 138 : generate_unarchival_heatmap,
437 138 : tracing,
438 138 : enable_tls_page_service_api,
439 138 : dev_mode,
440 138 : posthog_config,
441 138 : timeline_import_config,
442 138 : basebackup_cache_config,
443 138 : image_layer_generation_large_timeline_threshold,
444 138 : force_metric_collection_on_scrape,
445 138 : } = config_toml;
446 :
447 138 : let mut conf = PageServerConf {
448 : // ------------------------------------------------------------
449 : // fields that are already fully validated by the ConfigToml Deserialize impl
450 : // ------------------------------------------------------------
451 138 : listen_pg_addr,
452 138 : listen_http_addr,
453 138 : listen_https_addr,
454 138 : listen_grpc_addr,
455 138 : ssl_key_file,
456 138 : ssl_cert_file,
457 138 : ssl_cert_reload_period,
458 138 : availability_zone,
459 138 : wait_lsn_timeout,
460 138 : wal_redo_timeout,
461 138 : superuser,
462 138 : locale,
463 138 : page_cache_size,
464 138 : max_file_descriptors,
465 138 : http_auth_type,
466 138 : pg_auth_type,
467 138 : grpc_auth_type,
468 138 : auth_validation_public_key_path,
469 138 : remote_storage_config: remote_storage,
470 138 : broker_endpoint,
471 138 : broker_keepalive_interval,
472 138 : log_format,
473 138 : metric_collection_interval,
474 138 : metric_collection_endpoint,
475 138 : metric_collection_bucket,
476 138 : synthetic_size_calculation_interval,
477 138 : disk_usage_based_eviction,
478 138 : test_remote_failures,
479 138 : test_remote_failures_probability,
480 138 : ondemand_download_behavior_treat_error_as_warn,
481 138 : background_task_maximum_delay,
482 138 : control_plane_api: control_plane_api
483 138 : .ok_or_else(|| anyhow::anyhow!("`control_plane_api` must be set"))?,
484 138 : control_plane_emergency_mode,
485 138 : heatmap_upload_concurrency,
486 138 : secondary_download_concurrency,
487 138 : ingest_batch_size,
488 138 : max_vectored_read_bytes,
489 138 : max_get_vectored_keys,
490 138 : image_compression,
491 138 : timeline_offloading,
492 138 : ephemeral_bytes_per_memory_kb,
493 138 : import_pgdata_upcall_api,
494 138 : import_pgdata_upcall_api_token: import_pgdata_upcall_api_token.map(SecretString::from),
495 138 : import_pgdata_aws_endpoint_url,
496 138 : page_service_pipelining,
497 138 : get_vectored_concurrent_io,
498 138 : tracing,
499 138 : enable_tls_page_service_api,
500 138 : dev_mode,
501 138 : timeline_import_config,
502 138 : basebackup_cache_config,
503 138 : image_layer_generation_large_timeline_threshold,
504 138 : force_metric_collection_on_scrape,
505 :
506 : // ------------------------------------------------------------
507 : // fields that require additional validation or custom handling
508 : // ------------------------------------------------------------
509 138 : workdir: workdir.to_owned(),
510 138 : pg_distrib_dir: pg_distrib_dir.unwrap_or_else(|| {
511 11 : std::env::current_dir()
512 11 : .expect("current_dir() failed")
513 11 : .try_into()
514 11 : .expect("current_dir() is not a valid Utf8Path")
515 11 : }),
516 138 : control_plane_api_token: control_plane_api_token.map(SecretString::from),
517 138 : id,
518 138 : default_tenant_conf: tenant_config,
519 138 : concurrent_tenant_warmup: ConfigurableSemaphore::new(concurrent_tenant_warmup),
520 138 : concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore::new(
521 138 : concurrent_tenant_size_logical_size_queries,
522 : ),
523 138 : eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore::new(
524 : // re-use `concurrent_tenant_size_logical_size_queries`
525 138 : concurrent_tenant_size_logical_size_queries,
526 : ),
527 138 : virtual_file_io_engine: match virtual_file_io_engine {
528 0 : Some(v) => v,
529 138 : None => match crate::virtual_file::io_engine_feature_test()
530 138 : .context("auto-detect virtual_file_io_engine")?
531 : {
532 138 : io_engine::FeatureTestResult::PlatformPreferred(v) => v, // make no noise
533 0 : io_engine::FeatureTestResult::Worse { engine, remark } => {
534 : // TODO: bubble this up to the caller so we can tracing::warn! it.
535 0 : eprintln!(
536 0 : "auto-detected IO engine is not platform-preferred: engine={engine:?} remark={remark:?}"
537 : );
538 0 : engine
539 : }
540 : },
541 : },
542 138 : l0_flush: l0_flush
543 138 : .map(crate::l0_flush::L0FlushConfig::from)
544 138 : .unwrap_or_default(),
545 138 : virtual_file_io_mode: virtual_file_io_mode.unwrap_or(virtual_file::IoMode::preferred()),
546 138 : no_sync: no_sync.unwrap_or(false),
547 138 : enable_read_path_debugging: enable_read_path_debugging.unwrap_or(false),
548 138 : validate_wal_contiguity: validate_wal_contiguity.unwrap_or(false),
549 138 : load_previous_heatmap: load_previous_heatmap.unwrap_or(true),
550 138 : generate_unarchival_heatmap: generate_unarchival_heatmap.unwrap_or(true),
551 138 : ssl_ca_certs: match ssl_ca_file {
552 0 : Some(ssl_ca_file) => {
553 0 : let buf = std::fs::read(ssl_ca_file)?;
554 0 : pem::parse_many(&buf)?
555 0 : .into_iter()
556 0 : .filter(|pem| pem.tag() == "CERTIFICATE")
557 0 : .collect()
558 : }
559 138 : None => Vec::new(),
560 : },
561 138 : posthog_config,
562 : };
563 :
564 : // ------------------------------------------------------------
565 : // custom validation code that covers more than one field in isolation
566 : // ------------------------------------------------------------
567 :
568 138 : if [conf.http_auth_type, conf.pg_auth_type, conf.grpc_auth_type]
569 138 : .contains(&AuthType::NeonJWT)
570 : {
571 0 : let auth_validation_public_key_path = conf
572 0 : .auth_validation_public_key_path
573 0 : .get_or_insert_with(|| workdir.join("auth_public_key.pem"));
574 0 : ensure!(
575 0 : auth_validation_public_key_path.exists(),
576 0 : format!(
577 0 : "Can't find auth_validation_public_key at '{auth_validation_public_key_path}'",
578 : )
579 : );
580 138 : }
581 :
582 138 : if let Some(tracing_config) = conf.tracing.as_ref() {
583 1 : let ratio = &tracing_config.sampling_ratio;
584 1 : ensure!(
585 1 : ratio.denominator != 0 && ratio.denominator >= ratio.numerator,
586 1 : format!(
587 1 : "Invalid sampling ratio: {}/{}",
588 : ratio.numerator, ratio.denominator
589 : )
590 : );
591 :
592 0 : let url = Url::parse(&tracing_config.export_config.endpoint)
593 0 : .map_err(anyhow::Error::msg)
594 0 : .with_context(|| {
595 0 : format!(
596 0 : "tracing endpoint URL is invalid : {}",
597 : tracing_config.export_config.endpoint
598 : )
599 0 : })?;
600 :
601 0 : ensure!(
602 0 : url.scheme() == "http" || url.scheme() == "https",
603 0 : format!(
604 0 : "tracing endpoint URL must start with http:// or https://: {}",
605 : tracing_config.export_config.endpoint
606 : )
607 : );
608 137 : }
609 :
610 137 : IndexEntry::validate_checkpoint_distance(conf.default_tenant_conf.checkpoint_distance)
611 137 : .map_err(anyhow::Error::msg)
612 137 : .with_context(|| {
613 0 : format!(
614 0 : "effective checkpoint distance is unsupported: {}",
615 : conf.default_tenant_conf.checkpoint_distance
616 : )
617 0 : })?;
618 :
619 : if let PageServicePipeliningConfig::Pipelined(PageServicePipeliningConfigPipelined {
620 137 : max_batch_size,
621 : ..
622 137 : }) = conf.page_service_pipelining
623 : {
624 137 : if max_batch_size.get() > conf.max_get_vectored_keys.get() {
625 1 : return Err(anyhow::anyhow!(
626 1 : "`max_batch_size` ({max_batch_size}) must be less than or equal to `max_get_vectored_keys` ({})",
627 1 : conf.max_get_vectored_keys.get()
628 1 : ));
629 136 : }
630 0 : };
631 :
632 136 : Ok(conf)
633 138 : }
634 :
635 : #[cfg(test)]
636 127 : pub fn test_repo_dir(test_name: &str) -> Utf8PathBuf {
637 127 : let test_output_dir = std::env::var("TEST_OUTPUT").unwrap_or("../tmp_check".into());
638 :
639 127 : let test_id = uuid::Uuid::new_v4();
640 127 : Utf8PathBuf::from(format!("{test_output_dir}/test_{test_name}_{test_id}"))
641 127 : }
642 :
643 127 : pub fn dummy_conf(repo_dir: Utf8PathBuf) -> Self {
644 127 : let pg_distrib_dir = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../pg_install");
645 :
646 127 : let mut config_toml = pageserver_api::config::ConfigToml {
647 127 : wait_lsn_timeout: Duration::from_secs(60),
648 127 : wal_redo_timeout: Duration::from_secs(60),
649 127 : pg_distrib_dir: Some(pg_distrib_dir),
650 127 : metric_collection_interval: Duration::from_secs(60),
651 127 : synthetic_size_calculation_interval: Duration::from_secs(60),
652 127 : background_task_maximum_delay: Duration::ZERO,
653 127 : load_previous_heatmap: Some(true),
654 127 : generate_unarchival_heatmap: Some(true),
655 127 : control_plane_api: Some(Url::parse("http://localhost:6666").unwrap()),
656 127 : ..Default::default()
657 127 : };
658 :
659 : // Test authors tend to forget about the default 10min initial lease deadline
660 : // when writing tests, which turns their immediate gc requests via mgmt API
661 : // into no-ops. Override the binary default here, such that there is no initial
662 : // lease deadline by default in tests. Tests that care can always override it
663 : // themselves.
664 : // Cf https://databricks.atlassian.net/browse/LKB-92?focusedCommentId=6722329
665 127 : config_toml.tenant_config.lsn_lease_length = Duration::from_secs(0);
666 :
667 127 : PageServerConf::parse_and_validate(NodeId(0), config_toml, &repo_dir).unwrap()
668 127 : }
669 : }
670 :
671 0 : #[derive(serde::Deserialize, serde::Serialize)]
672 : pub struct PageserverIdentity {
673 : pub id: NodeId,
674 : }
675 :
676 : /// Configurable semaphore permits setting.
677 : ///
678 : /// Does not allow semaphore permits to be zero, because at runtime initially zero permits and empty
679 : /// semaphore cannot be distinguished, leading any feature using these to await forever (or until
680 : /// new permits are added).
681 : #[derive(Debug, Clone)]
682 : pub struct ConfigurableSemaphore {
683 : initial_permits: NonZeroUsize,
684 : inner: std::sync::Arc<tokio::sync::Semaphore>,
685 : }
686 :
687 : impl ConfigurableSemaphore {
688 : /// Initializse using a non-zero amount of permits.
689 : ///
690 : /// Require a non-zero initial permits, because using permits == 0 is a crude way to disable a
691 : /// feature such as [`TenantShard::gather_size_inputs`]. Otherwise any semaphore using future will
692 : /// behave like [`futures::future::pending`], just waiting until new permits are added.
693 : ///
694 : /// [`TenantShard::gather_size_inputs`]: crate::tenant::TenantShard::gather_size_inputs
695 414 : pub fn new(initial_permits: NonZeroUsize) -> Self {
696 414 : ConfigurableSemaphore {
697 414 : initial_permits,
698 414 : inner: std::sync::Arc::new(tokio::sync::Semaphore::new(initial_permits.get())),
699 414 : }
700 414 : }
701 :
702 : /// Returns the configured amount of permits.
703 0 : pub fn initial_permits(&self) -> NonZeroUsize {
704 0 : self.initial_permits
705 0 : }
706 : }
707 :
708 : impl PartialEq for ConfigurableSemaphore {
709 0 : fn eq(&self, other: &Self) -> bool {
710 : // the number of permits can be increased at runtime, so we cannot really fulfill the
711 : // PartialEq value equality otherwise
712 0 : self.initial_permits == other.initial_permits
713 0 : }
714 : }
715 :
716 : impl Eq for ConfigurableSemaphore {}
717 :
718 : impl ConfigurableSemaphore {
719 0 : pub fn inner(&self) -> &std::sync::Arc<tokio::sync::Semaphore> {
720 0 : &self.inner
721 0 : }
722 : }
723 :
724 : #[cfg(test)]
725 : mod tests {
726 :
727 : use std::time::Duration;
728 :
729 : use camino::Utf8PathBuf;
730 : use pageserver_api::config::{DiskUsageEvictionTaskConfig, EvictionOrder};
731 : use rstest::rstest;
732 : use utils::{id::NodeId, serde_percent::Percent};
733 :
734 : use super::PageServerConf;
735 :
736 : #[test]
737 1 : fn test_minimal_config_toml_is_valid() {
738 : // The minimal valid config for running a pageserver:
739 : // - control_plane_api is mandatory, as pageservers cannot run in isolation
740 : // - we use Default impl of everything else in this situation
741 1 : let input = r#"
742 1 : control_plane_api = "http://localhost:6666"
743 1 : "#;
744 1 : let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(input)
745 1 : .expect("empty config is valid");
746 1 : let workdir = Utf8PathBuf::from("/nonexistent");
747 1 : PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir)
748 1 : .expect("parse_and_validate");
749 1 : }
750 :
751 : #[test]
752 1 : fn test_config_tracing_endpoint_is_invalid() {
753 1 : let input = r#"
754 1 : control_plane_api = "http://localhost:6666"
755 1 :
756 1 : [tracing]
757 1 :
758 1 : sampling_ratio = { numerator = 1, denominator = 0 }
759 1 :
760 1 : [tracing.export_config]
761 1 : endpoint = "localhost:4317"
762 1 : protocol = "http-binary"
763 1 : timeout = "1ms"
764 1 : "#;
765 1 : let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(input)
766 1 : .expect("config has valid fields");
767 1 : let workdir = Utf8PathBuf::from("/nonexistent");
768 1 : PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir)
769 1 : .expect_err("parse_and_validate should fail for endpoint without scheme");
770 1 : }
771 :
772 : #[rstest]
773 : #[case(32, 32, true)]
774 : #[case(64, 32, false)]
775 : #[case(64, 64, true)]
776 : #[case(128, 128, true)]
777 : fn test_config_max_batch_size_is_valid(
778 : #[case] max_batch_size: usize,
779 : #[case] max_get_vectored_keys: usize,
780 : #[case] is_valid: bool,
781 : ) {
782 : let input = format!(
783 : r#"
784 : control_plane_api = "http://localhost:6666"
785 : max_get_vectored_keys = {max_get_vectored_keys}
786 : page_service_pipelining = {{ mode="pipelined", execution="concurrent-futures", max_batch_size={max_batch_size}, batching="uniform-lsn" }}
787 : "#,
788 : );
789 : let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(&input)
790 : .expect("config has valid fields");
791 : let workdir = Utf8PathBuf::from("/nonexistent");
792 : let result = PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir);
793 : assert_eq!(result.is_ok(), is_valid);
794 : }
795 :
796 : #[test]
797 1 : fn test_config_posthog_config_is_valid() {
798 1 : let input = r#"
799 1 : control_plane_api = "http://localhost:6666"
800 1 :
801 1 : [posthog_config]
802 1 : server_api_key = "phs_AAA"
803 1 : client_api_key = "phc_BBB"
804 1 : project_id = "000"
805 1 : private_api_url = "https://us.posthog.com"
806 1 : public_api_url = "https://us.i.posthog.com"
807 1 : "#;
808 1 : let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(input)
809 1 : .expect("posthogconfig is valid");
810 1 : let workdir = Utf8PathBuf::from("/nonexistent");
811 1 : PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir)
812 1 : .expect("parse_and_validate");
813 1 : }
814 :
815 : #[test]
816 1 : fn test_config_posthog_incomplete_config_is_valid() {
817 1 : let input = r#"
818 1 : control_plane_api = "http://localhost:6666"
819 1 :
820 1 : [posthog_config]
821 1 : server_api_key = "phs_AAA"
822 1 : private_api_url = "https://us.posthog.com"
823 1 : public_api_url = "https://us.i.posthog.com"
824 1 : "#;
825 1 : let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(input)
826 1 : .expect("posthogconfig is valid");
827 1 : let workdir = Utf8PathBuf::from("/nonexistent");
828 1 : PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir)
829 1 : .expect("parse_and_validate");
830 1 : }
831 :
832 : #[rstest]
833 : #[
834 : case::omit_the_whole_config(
835 : DiskUsageEvictionTaskConfig {
836 : max_usage_pct: Percent::new(80).unwrap(),
837 : min_avail_bytes: 2_000_000_000,
838 : period: Duration::from_secs(60),
839 : eviction_order: Default::default(),
840 : #[cfg(feature = "testing")]
841 : mock_statvfs: None,
842 : enabled: true,
843 : },
844 : r#"
845 : control_plane_api = "http://localhost:6666"
846 : "#,
847 : )]
848 : #[
849 : case::omit_enabled_field(
850 : DiskUsageEvictionTaskConfig {
851 : max_usage_pct: Percent::new(80).unwrap(),
852 : min_avail_bytes: 1_000_000_000,
853 : period: Duration::from_secs(60),
854 : eviction_order: EvictionOrder::RelativeAccessed {
855 : highest_layer_count_loses_first: true,
856 : },
857 : #[cfg(feature = "testing")]
858 : mock_statvfs: None,
859 : enabled: true,
860 : },
861 : r#"
862 : control_plane_api = "http://localhost:6666"
863 : disk_usage_based_eviction = { max_usage_pct = 80, min_avail_bytes = 1000000000, period = "60s" }
864 : "#,
865 : )]
866 : #[case::disabled(
867 : DiskUsageEvictionTaskConfig {
868 : max_usage_pct: Percent::new(80).unwrap(),
869 : min_avail_bytes: 2_000_000_000,
870 : period: Duration::from_secs(60),
871 : eviction_order: EvictionOrder::RelativeAccessed {
872 : highest_layer_count_loses_first: true,
873 : },
874 : #[cfg(feature = "testing")]
875 : mock_statvfs: None,
876 : enabled: false,
877 : },
878 : r#"
879 : control_plane_api = "http://localhost:6666"
880 : disk_usage_based_eviction = { enabled = false }
881 : "#
882 : )]
883 : fn test_config_disk_usage_based_eviction_is_valid(
884 : #[case] expected_disk_usage_based_eviction: DiskUsageEvictionTaskConfig,
885 : #[case] input: &str,
886 : ) {
887 : let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(input)
888 : .expect("disk_usage_based_eviction is valid");
889 : let workdir = Utf8PathBuf::from("/nonexistent");
890 : let config = PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir).unwrap();
891 : let disk_usage_based_eviction = config.disk_usage_based_eviction;
892 : assert_eq!(
893 : expected_disk_usage_based_eviction,
894 : disk_usage_based_eviction
895 : );
896 : }
897 : }
|