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