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