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 std::env;
8 : use std::num::NonZeroUsize;
9 : use std::sync::Arc;
10 : use std::time::Duration;
11 :
12 : use anyhow::{Context, bail, ensure};
13 : use camino::{Utf8Path, Utf8PathBuf};
14 : use once_cell::sync::OnceCell;
15 : use pageserver_api::config::{DiskUsageEvictionTaskConfig, MaxVectoredReadBytes};
16 : use pageserver_api::models::ImageCompressionAlgorithm;
17 : use pageserver_api::shard::TenantShardId;
18 : use postgres_backend::AuthType;
19 : use remote_storage::{RemotePath, RemoteStorageConfig};
20 : use reqwest::Url;
21 : use storage_broker::Uri;
22 : use utils::id::{NodeId, TimelineId};
23 : use utils::logging::{LogFormat, SecretString};
24 : use utils::postgres_client::PostgresClientProtocol;
25 :
26 : use crate::tenant::storage_layer::inmemory_layer::IndexEntry;
27 : use crate::tenant::{TENANTS_SEGMENT_NAME, TIMELINES_SEGMENT_NAME};
28 : use crate::virtual_file::io_engine;
29 : use crate::{TENANT_HEATMAP_BASENAME, TENANT_LOCATION_CONFIG_NAME, virtual_file};
30 :
31 : /// Global state of pageserver.
32 : ///
33 : /// It's mostly immutable configuration, but some semaphores and the
34 : /// like crept in over time and the name stuck.
35 : ///
36 : /// Instantiated by deserializing `pageserver.toml` into [`pageserver_api::config::ConfigToml`]
37 : /// and passing that to [`PageServerConf::parse_and_validate`].
38 : ///
39 : /// # Adding a New Field
40 : ///
41 : /// 1. Add the field to `pageserver_api::config::ConfigToml`.
42 : /// 2. Fix compiler errors (exhaustive destructuring will guide you).
43 : ///
44 : /// For fields that require additional validation or filling in of defaults at runtime,
45 : /// check for examples in the [`PageServerConf::parse_and_validate`] method.
46 : #[derive(Debug, Clone, PartialEq, Eq)]
47 : pub struct PageServerConf {
48 : // Identifier of that particular pageserver so e g safekeepers
49 : // can safely distinguish different pageservers
50 : pub id: NodeId,
51 :
52 : /// Example (default): 127.0.0.1:64000
53 : pub listen_pg_addr: String,
54 : /// Example (default): 127.0.0.1:9898
55 : pub listen_http_addr: String,
56 : /// Example: 127.0.0.1:9899
57 : pub listen_https_addr: Option<String>,
58 :
59 : pub ssl_key_file: Utf8PathBuf,
60 : pub ssl_cert_file: Utf8PathBuf,
61 :
62 : /// Current availability zone. Used for traffic metrics.
63 : pub availability_zone: Option<String>,
64 :
65 : // Timeout when waiting for WAL receiver to catch up to an LSN given in a GetPage@LSN call.
66 : pub wait_lsn_timeout: Duration,
67 : // How long to wait for WAL redo to complete.
68 : pub wal_redo_timeout: Duration,
69 :
70 : pub superuser: String,
71 : pub locale: String,
72 :
73 : pub page_cache_size: usize,
74 : pub max_file_descriptors: usize,
75 :
76 : // Repository directory, relative to current working directory.
77 : // Normally, the page server changes the current working directory
78 : // to the repository, and 'workdir' is always '.'. But we don't do
79 : // that during unit testing, because the current directory is global
80 : // to the process but different unit tests work on different
81 : // repositories.
82 : pub workdir: Utf8PathBuf,
83 :
84 : pub pg_distrib_dir: Utf8PathBuf,
85 :
86 : // Authentication
87 : /// authentication method for the HTTP mgmt API
88 : pub http_auth_type: AuthType,
89 : /// authentication method for libpq connections from compute
90 : pub pg_auth_type: AuthType,
91 : /// Path to a file or directory containing public key(s) for verifying JWT tokens.
92 : /// Used for both mgmt and compute auth, if enabled.
93 : pub auth_validation_public_key_path: Option<Utf8PathBuf>,
94 :
95 : pub remote_storage_config: Option<RemoteStorageConfig>,
96 :
97 : pub default_tenant_conf: crate::tenant::config::TenantConf,
98 :
99 : /// Storage broker endpoints to connect to.
100 : pub broker_endpoint: Uri,
101 : pub broker_keepalive_interval: Duration,
102 :
103 : pub log_format: LogFormat,
104 :
105 : /// Number of tenants which will be concurrently loaded from remote storage proactively on startup or attach.
106 : ///
107 : /// A lower value implicitly deprioritizes loading such tenants, vs. other work in the system.
108 : pub concurrent_tenant_warmup: ConfigurableSemaphore,
109 :
110 : /// Number of concurrent [`Tenant::gather_size_inputs`](crate::tenant::Tenant::gather_size_inputs) allowed.
111 : pub concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore,
112 : /// Limit of concurrent [`Tenant::gather_size_inputs`] issued by module `eviction_task`.
113 : /// The number of permits is the same as `concurrent_tenant_size_logical_size_queries`.
114 : /// See the comment in `eviction_task` for details.
115 : ///
116 : /// [`Tenant::gather_size_inputs`]: crate::tenant::Tenant::gather_size_inputs
117 : pub eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore,
118 :
119 : // How often to collect metrics and send them to the metrics endpoint.
120 : pub metric_collection_interval: Duration,
121 : // How often to send unchanged cached metrics to the metrics endpoint.
122 : pub metric_collection_endpoint: Option<Url>,
123 : pub metric_collection_bucket: Option<RemoteStorageConfig>,
124 : pub synthetic_size_calculation_interval: Duration,
125 :
126 : pub disk_usage_based_eviction: Option<DiskUsageEvictionTaskConfig>,
127 :
128 : pub test_remote_failures: u64,
129 :
130 : pub ondemand_download_behavior_treat_error_as_warn: bool,
131 :
132 : /// How long will background tasks be delayed at most after initial load of tenants.
133 : ///
134 : /// Our largest initialization completions are in the range of 100-200s, so perhaps 10s works
135 : /// as we now isolate initial loading, initial logical size calculation and background tasks.
136 : /// Smaller nodes will have background tasks "not running" for this long unless every timeline
137 : /// has it's initial logical size calculated. Not running background tasks for some seconds is
138 : /// not terrible.
139 : pub background_task_maximum_delay: Duration,
140 :
141 : pub control_plane_api: Option<Url>,
142 :
143 : /// JWT token for use with the control plane API.
144 : pub control_plane_api_token: Option<SecretString>,
145 :
146 : pub import_pgdata_upcall_api: Option<Url>,
147 : pub import_pgdata_upcall_api_token: Option<SecretString>,
148 : pub import_pgdata_aws_endpoint_url: Option<Url>,
149 :
150 : /// If true, pageserver will make best-effort to operate without a control plane: only
151 : /// for use in major incidents.
152 : pub control_plane_emergency_mode: bool,
153 :
154 : /// How many heatmap uploads may be done concurrency: lower values implicitly deprioritize
155 : /// heatmap uploads vs. other remote storage operations.
156 : pub heatmap_upload_concurrency: usize,
157 :
158 : /// How many remote storage downloads may be done for secondary tenants concurrently. Implicitly
159 : /// deprioritises secondary downloads vs. remote storage operations for attached tenants.
160 : pub secondary_download_concurrency: usize,
161 :
162 : /// Maximum number of WAL records to be ingested and committed at the same time
163 : pub ingest_batch_size: u64,
164 :
165 : pub virtual_file_io_engine: virtual_file::IoEngineKind,
166 :
167 : pub max_vectored_read_bytes: MaxVectoredReadBytes,
168 :
169 : pub image_compression: ImageCompressionAlgorithm,
170 :
171 : /// Whether to offload archived timelines automatically
172 : pub timeline_offloading: bool,
173 :
174 : /// How many bytes of ephemeral layer content will we allow per kilobyte of RAM. When this
175 : /// is exceeded, we start proactively closing ephemeral layers to limit the total amount
176 : /// of ephemeral data.
177 : ///
178 : /// Setting this to zero disables limits on total ephemeral layer size.
179 : pub ephemeral_bytes_per_memory_kb: usize,
180 :
181 : pub l0_flush: crate::l0_flush::L0FlushConfig,
182 :
183 : /// Direct IO settings
184 : pub virtual_file_io_mode: virtual_file::IoMode,
185 :
186 : /// Optionally disable disk syncs (unsafe!)
187 : pub no_sync: bool,
188 :
189 : pub wal_receiver_protocol: PostgresClientProtocol,
190 :
191 : pub page_service_pipelining: pageserver_api::config::PageServicePipeliningConfig,
192 :
193 : pub get_vectored_concurrent_io: pageserver_api::config::GetVectoredConcurrentIo,
194 :
195 : /// Enable read path debugging. If enabled, read key errors will print a backtrace of the layer
196 : /// files read.
197 : pub enable_read_path_debugging: bool,
198 :
199 : /// Interpreted protocol feature: if enabled, validate that the logical WAL received from
200 : /// safekeepers does not have gaps.
201 : pub validate_wal_contiguity: bool,
202 :
203 : /// When set, the previously written to disk heatmap is loaded on tenant attach and used
204 : /// to avoid clobbering the heatmap from new, cold, attached locations.
205 : pub load_previous_heatmap: bool,
206 :
207 : /// When set, include visible layers in the next uploaded heatmaps of an unarchived timeline.
208 : pub generate_unarchival_heatmap: bool,
209 : }
210 :
211 : /// Token for authentication to safekeepers
212 : ///
213 : /// We do not want to store this in a PageServerConf because the latter may be logged
214 : /// and/or serialized at a whim, while the token is secret. Currently this token is the
215 : /// same for accessing all tenants/timelines, but may become per-tenant/per-timeline in
216 : /// the future, more tokens and auth may arrive for storage broker, completely changing the logic.
217 : /// Hence, we resort to a global variable for now instead of passing the token from the
218 : /// startup code to the connection code through a dozen layers.
219 : pub static SAFEKEEPER_AUTH_TOKEN: OnceCell<Arc<String>> = OnceCell::new();
220 :
221 : impl PageServerConf {
222 : //
223 : // Repository paths, relative to workdir.
224 : //
225 :
226 15508 : pub fn tenants_path(&self) -> Utf8PathBuf {
227 15508 : self.workdir.join(TENANTS_SEGMENT_NAME)
228 15508 : }
229 :
230 144 : pub fn deletion_prefix(&self) -> Utf8PathBuf {
231 144 : self.workdir.join("deletion")
232 144 : }
233 :
234 0 : pub fn metadata_path(&self) -> Utf8PathBuf {
235 0 : self.workdir.join("metadata.json")
236 0 : }
237 :
238 56 : pub fn deletion_list_path(&self, sequence: u64) -> Utf8PathBuf {
239 : // Encode a version in the filename, so that if we ever switch away from JSON we can
240 : // increment this.
241 : const VERSION: u8 = 1;
242 :
243 56 : self.deletion_prefix()
244 56 : .join(format!("{sequence:016x}-{VERSION:02x}.list"))
245 56 : }
246 :
247 48 : pub fn deletion_header_path(&self) -> Utf8PathBuf {
248 : // Encode a version in the filename, so that if we ever switch away from JSON we can
249 : // increment this.
250 : const VERSION: u8 = 1;
251 :
252 48 : self.deletion_prefix().join(format!("header-{VERSION:02x}"))
253 48 : }
254 :
255 15404 : pub fn tenant_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
256 15404 : self.tenants_path().join(tenant_shard_id.to_string())
257 15404 : }
258 :
259 : /// Points to a place in pageserver's local directory,
260 : /// where certain tenant's LocationConf be stored.
261 0 : pub(crate) fn tenant_location_config_path(
262 0 : &self,
263 0 : tenant_shard_id: &TenantShardId,
264 0 : ) -> Utf8PathBuf {
265 0 : self.tenant_path(tenant_shard_id)
266 0 : .join(TENANT_LOCATION_CONFIG_NAME)
267 0 : }
268 :
269 452 : pub(crate) fn tenant_heatmap_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
270 452 : self.tenant_path(tenant_shard_id)
271 452 : .join(TENANT_HEATMAP_BASENAME)
272 452 : }
273 :
274 14492 : pub fn timelines_path(&self, tenant_shard_id: &TenantShardId) -> Utf8PathBuf {
275 14492 : self.tenant_path(tenant_shard_id)
276 14492 : .join(TIMELINES_SEGMENT_NAME)
277 14492 : }
278 :
279 13576 : pub fn timeline_path(
280 13576 : &self,
281 13576 : tenant_shard_id: &TenantShardId,
282 13576 : timeline_id: &TimelineId,
283 13576 : ) -> Utf8PathBuf {
284 13576 : self.timelines_path(tenant_shard_id)
285 13576 : .join(timeline_id.to_string())
286 13576 : }
287 :
288 : /// Turns storage remote path of a file into its local path.
289 0 : pub fn local_path(&self, remote_path: &RemotePath) -> Utf8PathBuf {
290 0 : remote_path.with_base(&self.workdir)
291 0 : }
292 :
293 : //
294 : // Postgres distribution paths
295 : //
296 40 : pub fn pg_distrib_dir(&self, pg_version: u32) -> anyhow::Result<Utf8PathBuf> {
297 40 : let path = self.pg_distrib_dir.clone();
298 40 :
299 40 : #[allow(clippy::manual_range_patterns)]
300 40 : match pg_version {
301 40 : 14 | 15 | 16 | 17 => Ok(path.join(format!("v{pg_version}"))),
302 0 : _ => bail!("Unsupported postgres version: {}", pg_version),
303 : }
304 40 : }
305 :
306 20 : pub fn pg_bin_dir(&self, pg_version: u32) -> anyhow::Result<Utf8PathBuf> {
307 20 : Ok(self.pg_distrib_dir(pg_version)?.join("bin"))
308 20 : }
309 20 : pub fn pg_lib_dir(&self, pg_version: u32) -> anyhow::Result<Utf8PathBuf> {
310 20 : Ok(self.pg_distrib_dir(pg_version)?.join("lib"))
311 20 : }
312 :
313 : /// Parse a configuration file (pageserver.toml) into a PageServerConf struct,
314 : /// validating the input and failing on errors.
315 : ///
316 : /// This leaves any options not present in the file in the built-in defaults.
317 488 : pub fn parse_and_validate(
318 488 : id: NodeId,
319 488 : config_toml: pageserver_api::config::ConfigToml,
320 488 : workdir: &Utf8Path,
321 488 : ) -> anyhow::Result<Self> {
322 488 : let pageserver_api::config::ConfigToml {
323 488 : listen_pg_addr,
324 488 : listen_http_addr,
325 488 : listen_https_addr,
326 488 : ssl_key_file,
327 488 : ssl_cert_file,
328 488 : availability_zone,
329 488 : wait_lsn_timeout,
330 488 : wal_redo_timeout,
331 488 : superuser,
332 488 : locale,
333 488 : page_cache_size,
334 488 : max_file_descriptors,
335 488 : pg_distrib_dir,
336 488 : http_auth_type,
337 488 : pg_auth_type,
338 488 : auth_validation_public_key_path,
339 488 : remote_storage,
340 488 : broker_endpoint,
341 488 : broker_keepalive_interval,
342 488 : log_format,
343 488 : metric_collection_interval,
344 488 : metric_collection_endpoint,
345 488 : metric_collection_bucket,
346 488 : synthetic_size_calculation_interval,
347 488 : disk_usage_based_eviction,
348 488 : test_remote_failures,
349 488 : ondemand_download_behavior_treat_error_as_warn,
350 488 : background_task_maximum_delay,
351 488 : control_plane_api,
352 488 : control_plane_api_token,
353 488 : control_plane_emergency_mode,
354 488 : import_pgdata_upcall_api,
355 488 : import_pgdata_upcall_api_token,
356 488 : import_pgdata_aws_endpoint_url,
357 488 : heatmap_upload_concurrency,
358 488 : secondary_download_concurrency,
359 488 : ingest_batch_size,
360 488 : max_vectored_read_bytes,
361 488 : image_compression,
362 488 : timeline_offloading,
363 488 : ephemeral_bytes_per_memory_kb,
364 488 : l0_flush,
365 488 : virtual_file_io_mode,
366 488 : concurrent_tenant_warmup,
367 488 : concurrent_tenant_size_logical_size_queries,
368 488 : virtual_file_io_engine,
369 488 : tenant_config,
370 488 : no_sync,
371 488 : wal_receiver_protocol,
372 488 : page_service_pipelining,
373 488 : get_vectored_concurrent_io,
374 488 : enable_read_path_debugging,
375 488 : validate_wal_contiguity,
376 488 : load_previous_heatmap,
377 488 : generate_unarchival_heatmap,
378 488 : } = config_toml;
379 :
380 488 : let mut conf = PageServerConf {
381 : // ------------------------------------------------------------
382 : // fields that are already fully validated by the ConfigToml Deserialize impl
383 : // ------------------------------------------------------------
384 488 : listen_pg_addr,
385 488 : listen_http_addr,
386 488 : listen_https_addr,
387 488 : ssl_key_file,
388 488 : ssl_cert_file,
389 488 : availability_zone,
390 488 : wait_lsn_timeout,
391 488 : wal_redo_timeout,
392 488 : superuser,
393 488 : locale,
394 488 : page_cache_size,
395 488 : max_file_descriptors,
396 488 : http_auth_type,
397 488 : pg_auth_type,
398 488 : auth_validation_public_key_path,
399 488 : remote_storage_config: remote_storage,
400 488 : broker_endpoint,
401 488 : broker_keepalive_interval,
402 488 : log_format,
403 488 : metric_collection_interval,
404 488 : metric_collection_endpoint,
405 488 : metric_collection_bucket,
406 488 : synthetic_size_calculation_interval,
407 488 : disk_usage_based_eviction,
408 488 : test_remote_failures,
409 488 : ondemand_download_behavior_treat_error_as_warn,
410 488 : background_task_maximum_delay,
411 488 : control_plane_api,
412 488 : control_plane_emergency_mode,
413 488 : heatmap_upload_concurrency,
414 488 : secondary_download_concurrency,
415 488 : ingest_batch_size,
416 488 : max_vectored_read_bytes,
417 488 : image_compression,
418 488 : timeline_offloading,
419 488 : ephemeral_bytes_per_memory_kb,
420 488 : import_pgdata_upcall_api,
421 488 : import_pgdata_upcall_api_token: import_pgdata_upcall_api_token.map(SecretString::from),
422 488 : import_pgdata_aws_endpoint_url,
423 488 : wal_receiver_protocol,
424 488 : page_service_pipelining,
425 488 : get_vectored_concurrent_io,
426 488 :
427 488 : // ------------------------------------------------------------
428 488 : // fields that require additional validation or custom handling
429 488 : // ------------------------------------------------------------
430 488 : workdir: workdir.to_owned(),
431 488 : pg_distrib_dir: pg_distrib_dir.unwrap_or_else(|| {
432 4 : std::env::current_dir()
433 4 : .expect("current_dir() failed")
434 4 : .try_into()
435 4 : .expect("current_dir() is not a valid Utf8Path")
436 488 : }),
437 488 : control_plane_api_token: control_plane_api_token.map(SecretString::from),
438 488 : id,
439 488 : default_tenant_conf: tenant_config,
440 488 : concurrent_tenant_warmup: ConfigurableSemaphore::new(concurrent_tenant_warmup),
441 488 : concurrent_tenant_size_logical_size_queries: ConfigurableSemaphore::new(
442 488 : concurrent_tenant_size_logical_size_queries,
443 488 : ),
444 488 : eviction_task_immitated_concurrent_logical_size_queries: ConfigurableSemaphore::new(
445 488 : // re-use `concurrent_tenant_size_logical_size_queries`
446 488 : concurrent_tenant_size_logical_size_queries,
447 488 : ),
448 488 : virtual_file_io_engine: match virtual_file_io_engine {
449 0 : Some(v) => v,
450 488 : None => match crate::virtual_file::io_engine_feature_test()
451 488 : .context("auto-detect virtual_file_io_engine")?
452 : {
453 488 : io_engine::FeatureTestResult::PlatformPreferred(v) => v, // make no noise
454 0 : io_engine::FeatureTestResult::Worse { engine, remark } => {
455 0 : // TODO: bubble this up to the caller so we can tracing::warn! it.
456 0 : eprintln!(
457 0 : "auto-detected IO engine is not platform-preferred: engine={engine:?} remark={remark:?}"
458 0 : );
459 0 : engine
460 : }
461 : },
462 : },
463 488 : l0_flush: l0_flush
464 488 : .map(crate::l0_flush::L0FlushConfig::from)
465 488 : .unwrap_or_default(),
466 488 : virtual_file_io_mode: virtual_file_io_mode.unwrap_or(virtual_file::IoMode::preferred()),
467 488 : no_sync: no_sync.unwrap_or(false),
468 488 : enable_read_path_debugging: enable_read_path_debugging.unwrap_or(false),
469 488 : validate_wal_contiguity: validate_wal_contiguity.unwrap_or(false),
470 488 : load_previous_heatmap: load_previous_heatmap.unwrap_or(true),
471 488 : generate_unarchival_heatmap: generate_unarchival_heatmap.unwrap_or(true),
472 488 : };
473 488 :
474 488 : // ------------------------------------------------------------
475 488 : // custom validation code that covers more than one field in isolation
476 488 : // ------------------------------------------------------------
477 488 :
478 488 : if conf.http_auth_type == AuthType::NeonJWT || conf.pg_auth_type == AuthType::NeonJWT {
479 0 : let auth_validation_public_key_path = conf
480 0 : .auth_validation_public_key_path
481 0 : .get_or_insert_with(|| workdir.join("auth_public_key.pem"));
482 0 : ensure!(
483 0 : auth_validation_public_key_path.exists(),
484 0 : format!(
485 0 : "Can't find auth_validation_public_key at '{auth_validation_public_key_path}'",
486 0 : )
487 : );
488 488 : }
489 :
490 488 : IndexEntry::validate_checkpoint_distance(conf.default_tenant_conf.checkpoint_distance)
491 488 : .map_err(anyhow::Error::msg)
492 488 : .with_context(|| {
493 0 : format!(
494 0 : "effective checkpoint distance is unsupported: {}",
495 0 : conf.default_tenant_conf.checkpoint_distance
496 0 : )
497 488 : })?;
498 :
499 488 : Ok(conf)
500 488 : }
501 :
502 : #[cfg(test)]
503 488 : pub fn test_repo_dir(test_name: &str) -> Utf8PathBuf {
504 488 : let test_output_dir = std::env::var("TEST_OUTPUT").unwrap_or("../tmp_check".into());
505 488 :
506 488 : let test_id = uuid::Uuid::new_v4();
507 488 : Utf8PathBuf::from(format!("{test_output_dir}/test_{test_name}_{test_id}"))
508 488 : }
509 :
510 484 : pub fn dummy_conf(repo_dir: Utf8PathBuf) -> Self {
511 484 : let pg_distrib_dir = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../pg_install");
512 484 :
513 484 : let config_toml = pageserver_api::config::ConfigToml {
514 484 : wait_lsn_timeout: Duration::from_secs(60),
515 484 : wal_redo_timeout: Duration::from_secs(60),
516 484 : pg_distrib_dir: Some(pg_distrib_dir),
517 484 : metric_collection_interval: Duration::from_secs(60),
518 484 : synthetic_size_calculation_interval: Duration::from_secs(60),
519 484 : background_task_maximum_delay: Duration::ZERO,
520 484 : load_previous_heatmap: Some(true),
521 484 : generate_unarchival_heatmap: Some(true),
522 484 : ..Default::default()
523 484 : };
524 484 : PageServerConf::parse_and_validate(NodeId(0), config_toml, &repo_dir).unwrap()
525 484 : }
526 : }
527 :
528 0 : #[derive(serde::Deserialize, serde::Serialize)]
529 : #[serde(deny_unknown_fields)]
530 : pub struct PageserverIdentity {
531 : pub id: NodeId,
532 : }
533 :
534 : /// Configurable semaphore permits setting.
535 : ///
536 : /// Does not allow semaphore permits to be zero, because at runtime initially zero permits and empty
537 : /// semaphore cannot be distinguished, leading any feature using these to await forever (or until
538 : /// new permits are added).
539 : #[derive(Debug, Clone)]
540 : pub struct ConfigurableSemaphore {
541 : initial_permits: NonZeroUsize,
542 : inner: std::sync::Arc<tokio::sync::Semaphore>,
543 : }
544 :
545 : impl ConfigurableSemaphore {
546 : /// Initializse using a non-zero amount of permits.
547 : ///
548 : /// Require a non-zero initial permits, because using permits == 0 is a crude way to disable a
549 : /// feature such as [`Tenant::gather_size_inputs`]. Otherwise any semaphore using future will
550 : /// behave like [`futures::future::pending`], just waiting until new permits are added.
551 : ///
552 : /// [`Tenant::gather_size_inputs`]: crate::tenant::Tenant::gather_size_inputs
553 1464 : pub fn new(initial_permits: NonZeroUsize) -> Self {
554 1464 : ConfigurableSemaphore {
555 1464 : initial_permits,
556 1464 : inner: std::sync::Arc::new(tokio::sync::Semaphore::new(initial_permits.get())),
557 1464 : }
558 1464 : }
559 :
560 : /// Returns the configured amount of permits.
561 0 : pub fn initial_permits(&self) -> NonZeroUsize {
562 0 : self.initial_permits
563 0 : }
564 : }
565 :
566 : impl PartialEq for ConfigurableSemaphore {
567 0 : fn eq(&self, other: &Self) -> bool {
568 0 : // the number of permits can be increased at runtime, so we cannot really fulfill the
569 0 : // PartialEq value equality otherwise
570 0 : self.initial_permits == other.initial_permits
571 0 : }
572 : }
573 :
574 : impl Eq for ConfigurableSemaphore {}
575 :
576 : impl ConfigurableSemaphore {
577 0 : pub fn inner(&self) -> &std::sync::Arc<tokio::sync::Semaphore> {
578 0 : &self.inner
579 0 : }
580 : }
581 :
582 : #[cfg(test)]
583 : mod tests {
584 :
585 : use camino::Utf8PathBuf;
586 : use utils::id::NodeId;
587 :
588 : use super::PageServerConf;
589 :
590 : #[test]
591 4 : fn test_empty_config_toml_is_valid() {
592 4 : // we use Default impl of everything in this situation
593 4 : let input = r#"
594 4 : "#;
595 4 : let config_toml = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(input)
596 4 : .expect("empty config is valid");
597 4 : let workdir = Utf8PathBuf::from("/nonexistent");
598 4 : PageServerConf::parse_and_validate(NodeId(0), config_toml, &workdir)
599 4 : .expect("parse_and_validate");
600 4 : }
601 :
602 : /// If there's a typo in the pageserver config, we'd rather catch that typo
603 : /// and fail pageserver startup than silently ignoring the typo, leaving whoever
604 : /// made it in the believe that their config change is effective.
605 : ///
606 : /// The default in serde is to allow unknown fields, so, we rely
607 : /// on developer+review discipline to add `deny_unknown_fields` when adding
608 : /// new structs to the config, and these tests here as a regression test.
609 : ///
610 : /// The alternative to all of this would be to allow unknown fields in the config.
611 : /// To catch them, we could have a config check tool or mgmt API endpoint that
612 : /// compares the effective config with the TOML on disk and makes sure that
613 : /// the on-disk TOML is a strict subset of the effective config.
614 : mod unknown_fields_handling {
615 : macro_rules! test {
616 : ($short_name:ident, $input:expr) => {
617 : #[test]
618 20 : fn $short_name() {
619 20 : let input = $input;
620 20 : let err = toml_edit::de::from_str::<pageserver_api::config::ConfigToml>(&input)
621 20 : .expect_err("some_invalid_field is an invalid field");
622 20 : dbg!(&err);
623 20 : assert!(err.to_string().contains("some_invalid_field"));
624 20 : }
625 : };
626 : }
627 : use indoc::indoc;
628 :
629 : test!(
630 : toplevel,
631 : indoc! {r#"
632 : some_invalid_field = 23
633 : "#}
634 : );
635 :
636 : test!(
637 : toplevel_nested,
638 : indoc! {r#"
639 : [some_invalid_field]
640 : foo = 23
641 : "#}
642 : );
643 :
644 : test!(
645 : disk_usage_based_eviction,
646 : indoc! {r#"
647 : [disk_usage_based_eviction]
648 : some_invalid_field = 23
649 : "#}
650 : );
651 :
652 : test!(
653 : tenant_config,
654 : indoc! {r#"
655 : [tenant_config]
656 : some_invalid_field = 23
657 : "#}
658 : );
659 :
660 : test!(
661 : l0_flush,
662 : indoc! {r#"
663 : [l0_flush]
664 : mode = "direct"
665 : some_invalid_field = 23
666 : "#}
667 : );
668 :
669 : // TODO: fix this => https://github.com/neondatabase/neon/issues/8915
670 : // test!(
671 : // remote_storage_config,
672 : // indoc! {r#"
673 : // [remote_storage_config]
674 : // local_path = "/nonexistent"
675 : // some_invalid_field = 23
676 : // "#}
677 : // );
678 : }
679 : }
|