Line data Source code
1 : //! Code to manage compute endpoints
2 : //!
3 : //! In the local test environment, the data for each endpoint is stored in
4 : //!
5 : //! ```text
6 : //! .neon/endpoints/<endpoint id>
7 : //! ```
8 : //!
9 : //! Some basic information about the endpoint, like the tenant and timeline IDs,
10 : //! are stored in the `endpoint.json` file. The `endpoint.json` file is created
11 : //! when the endpoint is created, and doesn't change afterwards.
12 : //!
13 : //! The endpoint is managed by the `compute_ctl` binary. When an endpoint is
14 : //! started, we launch `compute_ctl` It synchronizes the safekeepers, downloads
15 : //! the basebackup from the pageserver to initialize the data directory, and
16 : //! finally launches the PostgreSQL process. It watches the PostgreSQL process
17 : //! until it exits.
18 : //!
19 : //! When an endpoint is created, a `postgresql.conf` file is also created in
20 : //! the endpoint's directory. The file can be modified before starting PostgreSQL.
21 : //! However, the `postgresql.conf` file in the endpoint directory is not used directly
22 : //! by PostgreSQL. It is passed to `compute_ctl`, and `compute_ctl` writes another
23 : //! copy of it in the data directory.
24 : //!
25 : //! Directory contents:
26 : //!
27 : //! ```text
28 : //! .neon/endpoints/main/
29 : //! compute.log - log output of `compute_ctl` and `postgres`
30 : //! endpoint.json - serialized `EndpointConf` struct
31 : //! postgresql.conf - postgresql settings
32 : //! spec.json - passed to `compute_ctl`
33 : //! pgdata/
34 : //! postgresql.conf - copy of postgresql.conf created by `compute_ctl`
35 : //! zenith.signal
36 : //! <other PostgreSQL files>
37 : //! ```
38 : //!
39 : use std::collections::BTreeMap;
40 : use std::net::SocketAddr;
41 : use std::net::TcpStream;
42 : use std::path::PathBuf;
43 : use std::process::Command;
44 : use std::str::FromStr;
45 : use std::sync::Arc;
46 : use std::time::Duration;
47 :
48 : use anyhow::{anyhow, bail, Context, Result};
49 : use compute_api::spec::Database;
50 : use compute_api::spec::PgIdent;
51 : use compute_api::spec::RemoteExtSpec;
52 : use compute_api::spec::Role;
53 : use nix::sys::signal::kill;
54 : use nix::sys::signal::Signal;
55 : use pageserver_api::shard::ShardStripeSize;
56 : use serde::{Deserialize, Serialize};
57 : use url::Host;
58 : use utils::id::{NodeId, TenantId, TimelineId};
59 :
60 : use crate::local_env::LocalEnv;
61 : use crate::postgresql_conf::PostgresConf;
62 : use crate::storage_controller::StorageController;
63 :
64 : use compute_api::responses::{ComputeState, ComputeStatus};
65 : use compute_api::spec::{Cluster, ComputeFeature, ComputeMode, ComputeSpec};
66 :
67 : // contents of a endpoint.json file
68 0 : #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
69 : pub struct EndpointConf {
70 : endpoint_id: String,
71 : tenant_id: TenantId,
72 : timeline_id: TimelineId,
73 : mode: ComputeMode,
74 : pg_port: u16,
75 : http_port: u16,
76 : pg_version: u32,
77 : skip_pg_catalog_updates: bool,
78 : features: Vec<ComputeFeature>,
79 : }
80 :
81 : //
82 : // ComputeControlPlane
83 : //
84 : pub struct ComputeControlPlane {
85 : base_port: u16,
86 :
87 : // endpoint ID is the key
88 : pub endpoints: BTreeMap<String, Arc<Endpoint>>,
89 :
90 : env: LocalEnv,
91 : }
92 :
93 : impl ComputeControlPlane {
94 : // Load current endpoints from the endpoints/ subdirectories
95 0 : pub fn load(env: LocalEnv) -> Result<ComputeControlPlane> {
96 0 : let mut endpoints = BTreeMap::default();
97 0 : for endpoint_dir in std::fs::read_dir(env.endpoints_path())
98 0 : .with_context(|| format!("failed to list {}", env.endpoints_path().display()))?
99 : {
100 0 : let ep = Endpoint::from_dir_entry(endpoint_dir?, &env)?;
101 0 : endpoints.insert(ep.endpoint_id.clone(), Arc::new(ep));
102 : }
103 :
104 0 : Ok(ComputeControlPlane {
105 0 : base_port: 55431,
106 0 : endpoints,
107 0 : env,
108 0 : })
109 0 : }
110 :
111 0 : fn get_port(&mut self) -> u16 {
112 0 : 1 + self
113 0 : .endpoints
114 0 : .values()
115 0 : .map(|ep| std::cmp::max(ep.pg_address.port(), ep.http_address.port()))
116 0 : .max()
117 0 : .unwrap_or(self.base_port)
118 0 : }
119 :
120 : #[allow(clippy::too_many_arguments)]
121 0 : pub fn new_endpoint(
122 0 : &mut self,
123 0 : endpoint_id: &str,
124 0 : tenant_id: TenantId,
125 0 : timeline_id: TimelineId,
126 0 : pg_port: Option<u16>,
127 0 : http_port: Option<u16>,
128 0 : pg_version: u32,
129 0 : mode: ComputeMode,
130 0 : skip_pg_catalog_updates: bool,
131 0 : ) -> Result<Arc<Endpoint>> {
132 0 : let pg_port = pg_port.unwrap_or_else(|| self.get_port());
133 0 : let http_port = http_port.unwrap_or_else(|| self.get_port() + 1);
134 0 : let ep = Arc::new(Endpoint {
135 0 : endpoint_id: endpoint_id.to_owned(),
136 0 : pg_address: SocketAddr::new("127.0.0.1".parse().unwrap(), pg_port),
137 0 : http_address: SocketAddr::new("127.0.0.1".parse().unwrap(), http_port),
138 0 : env: self.env.clone(),
139 0 : timeline_id,
140 0 : mode,
141 0 : tenant_id,
142 0 : pg_version,
143 0 : // We don't setup roles and databases in the spec locally, so we don't need to
144 0 : // do catalog updates. Catalog updates also include check availability
145 0 : // data creation. Yet, we have tests that check that size and db dump
146 0 : // before and after start are the same. So, skip catalog updates,
147 0 : // with this we basically test a case of waking up an idle compute, where
148 0 : // we also skip catalog updates in the cloud.
149 0 : skip_pg_catalog_updates,
150 0 : features: vec![],
151 0 : });
152 0 :
153 0 : ep.create_endpoint_dir()?;
154 : std::fs::write(
155 0 : ep.endpoint_path().join("endpoint.json"),
156 0 : serde_json::to_string_pretty(&EndpointConf {
157 0 : endpoint_id: endpoint_id.to_string(),
158 0 : tenant_id,
159 0 : timeline_id,
160 0 : mode,
161 0 : http_port,
162 0 : pg_port,
163 0 : pg_version,
164 0 : skip_pg_catalog_updates,
165 0 : features: vec![],
166 0 : })?,
167 0 : )?;
168 : std::fs::write(
169 0 : ep.endpoint_path().join("postgresql.conf"),
170 0 : ep.setup_pg_conf()?.to_string(),
171 0 : )?;
172 :
173 0 : self.endpoints
174 0 : .insert(ep.endpoint_id.clone(), Arc::clone(&ep));
175 0 :
176 0 : Ok(ep)
177 0 : }
178 :
179 0 : pub fn check_conflicting_endpoints(
180 0 : &self,
181 0 : mode: ComputeMode,
182 0 : tenant_id: TenantId,
183 0 : timeline_id: TimelineId,
184 0 : ) -> Result<()> {
185 0 : if matches!(mode, ComputeMode::Primary) {
186 : // this check is not complete, as you could have a concurrent attempt at
187 : // creating another primary, both reading the state before checking it here,
188 : // but it's better than nothing.
189 0 : let mut duplicates = self.endpoints.iter().filter(|(_k, v)| {
190 0 : v.tenant_id == tenant_id
191 0 : && v.timeline_id == timeline_id
192 0 : && v.mode == mode
193 0 : && v.status() != EndpointStatus::Stopped
194 0 : });
195 :
196 0 : if let Some((key, _)) = duplicates.next() {
197 0 : bail!("attempting to create a duplicate primary endpoint on tenant {tenant_id}, timeline {timeline_id}: endpoint {key:?} exists already. please don't do this, it is not supported.");
198 0 : }
199 0 : }
200 0 : Ok(())
201 0 : }
202 : }
203 :
204 : ///////////////////////////////////////////////////////////////////////////////
205 :
206 : #[derive(Debug)]
207 : pub struct Endpoint {
208 : /// used as the directory name
209 : endpoint_id: String,
210 : pub tenant_id: TenantId,
211 : pub timeline_id: TimelineId,
212 : pub mode: ComputeMode,
213 :
214 : // port and address of the Postgres server and `compute_ctl`'s HTTP API
215 : pub pg_address: SocketAddr,
216 : pub http_address: SocketAddr,
217 :
218 : // postgres major version in the format: 14, 15, etc.
219 : pg_version: u32,
220 :
221 : // These are not part of the endpoint as such, but the environment
222 : // the endpoint runs in.
223 : pub env: LocalEnv,
224 :
225 : // Optimizations
226 : skip_pg_catalog_updates: bool,
227 :
228 : // Feature flags
229 : features: Vec<ComputeFeature>,
230 : }
231 :
232 : #[derive(PartialEq, Eq)]
233 : pub enum EndpointStatus {
234 : Running,
235 : Stopped,
236 : Crashed,
237 : RunningNoPidfile,
238 : }
239 :
240 : impl std::fmt::Display for EndpointStatus {
241 0 : fn fmt(&self, writer: &mut std::fmt::Formatter) -> std::fmt::Result {
242 0 : let s = match self {
243 0 : Self::Running => "running",
244 0 : Self::Stopped => "stopped",
245 0 : Self::Crashed => "crashed",
246 0 : Self::RunningNoPidfile => "running, no pidfile",
247 : };
248 0 : write!(writer, "{}", s)
249 0 : }
250 : }
251 :
252 : impl Endpoint {
253 0 : fn from_dir_entry(entry: std::fs::DirEntry, env: &LocalEnv) -> Result<Endpoint> {
254 0 : if !entry.file_type()?.is_dir() {
255 0 : anyhow::bail!(
256 0 : "Endpoint::from_dir_entry failed: '{}' is not a directory",
257 0 : entry.path().display()
258 0 : );
259 0 : }
260 0 :
261 0 : // parse data directory name
262 0 : let fname = entry.file_name();
263 0 : let endpoint_id = fname.to_str().unwrap().to_string();
264 :
265 : // Read the endpoint.json file
266 0 : let conf: EndpointConf =
267 0 : serde_json::from_slice(&std::fs::read(entry.path().join("endpoint.json"))?)?;
268 :
269 0 : Ok(Endpoint {
270 0 : pg_address: SocketAddr::new("127.0.0.1".parse().unwrap(), conf.pg_port),
271 0 : http_address: SocketAddr::new("127.0.0.1".parse().unwrap(), conf.http_port),
272 0 : endpoint_id,
273 0 : env: env.clone(),
274 0 : timeline_id: conf.timeline_id,
275 0 : mode: conf.mode,
276 0 : tenant_id: conf.tenant_id,
277 0 : pg_version: conf.pg_version,
278 0 : skip_pg_catalog_updates: conf.skip_pg_catalog_updates,
279 0 : features: conf.features,
280 0 : })
281 0 : }
282 :
283 0 : fn create_endpoint_dir(&self) -> Result<()> {
284 0 : std::fs::create_dir_all(self.endpoint_path()).with_context(|| {
285 0 : format!(
286 0 : "could not create endpoint directory {}",
287 0 : self.endpoint_path().display()
288 0 : )
289 0 : })
290 0 : }
291 :
292 : // Generate postgresql.conf with default configuration
293 0 : fn setup_pg_conf(&self) -> Result<PostgresConf> {
294 0 : let mut conf = PostgresConf::new();
295 0 : conf.append("max_wal_senders", "10");
296 0 : conf.append("wal_log_hints", "off");
297 0 : conf.append("max_replication_slots", "10");
298 0 : conf.append("hot_standby", "on");
299 0 : conf.append("shared_buffers", "1MB");
300 0 : conf.append("fsync", "off");
301 0 : conf.append("max_connections", "100");
302 0 : conf.append("wal_level", "logical");
303 0 : // wal_sender_timeout is the maximum time to wait for WAL replication.
304 0 : // It also defines how often the walreciever will send a feedback message to the wal sender.
305 0 : conf.append("wal_sender_timeout", "5s");
306 0 : conf.append("listen_addresses", &self.pg_address.ip().to_string());
307 0 : conf.append("port", &self.pg_address.port().to_string());
308 0 : conf.append("wal_keep_size", "0");
309 0 : // walproposer panics when basebackup is invalid, it is pointless to restart in this case.
310 0 : conf.append("restart_after_crash", "off");
311 0 :
312 0 : // Load the 'neon' extension
313 0 : conf.append("shared_preload_libraries", "neon");
314 0 :
315 0 : conf.append_line("");
316 0 : // Replication-related configurations, such as WAL sending
317 0 : match &self.mode {
318 : ComputeMode::Primary => {
319 : // Configure backpressure
320 : // - Replication write lag depends on how fast the walreceiver can process incoming WAL.
321 : // This lag determines latency of get_page_at_lsn. Speed of applying WAL is about 10MB/sec,
322 : // so to avoid expiration of 1 minute timeout, this lag should not be larger than 600MB.
323 : // Actually latency should be much smaller (better if < 1sec). But we assume that recently
324 : // updates pages are not requested from pageserver.
325 : // - Replication flush lag depends on speed of persisting data by checkpointer (creation of
326 : // delta/image layers) and advancing disk_consistent_lsn. Safekeepers are able to
327 : // remove/archive WAL only beyond disk_consistent_lsn. Too large a lag can cause long
328 : // recovery time (in case of pageserver crash) and disk space overflow at safekeepers.
329 : // - Replication apply lag depends on speed of uploading changes to S3 by uploader thread.
330 : // To be able to restore database in case of pageserver node crash, safekeeper should not
331 : // remove WAL beyond this point. Too large lag can cause space exhaustion in safekeepers
332 : // (if they are not able to upload WAL to S3).
333 0 : conf.append("max_replication_write_lag", "15MB");
334 0 : conf.append("max_replication_flush_lag", "10GB");
335 0 :
336 0 : if !self.env.safekeepers.is_empty() {
337 0 : // Configure Postgres to connect to the safekeepers
338 0 : conf.append("synchronous_standby_names", "walproposer");
339 0 :
340 0 : let safekeepers = self
341 0 : .env
342 0 : .safekeepers
343 0 : .iter()
344 0 : .map(|sk| format!("localhost:{}", sk.get_compute_port()))
345 0 : .collect::<Vec<String>>()
346 0 : .join(",");
347 0 : conf.append("neon.safekeepers", &safekeepers);
348 0 : } else {
349 0 : // We only use setup without safekeepers for tests,
350 0 : // and don't care about data durability on pageserver,
351 0 : // so set more relaxed synchronous_commit.
352 0 : conf.append("synchronous_commit", "remote_write");
353 0 :
354 0 : // Configure the node to stream WAL directly to the pageserver
355 0 : // This isn't really a supported configuration, but can be useful for
356 0 : // testing.
357 0 : conf.append("synchronous_standby_names", "pageserver");
358 0 : }
359 : }
360 0 : ComputeMode::Static(lsn) => {
361 0 : conf.append("recovery_target_lsn", &lsn.to_string());
362 0 : }
363 : ComputeMode::Replica => {
364 0 : assert!(!self.env.safekeepers.is_empty());
365 :
366 : // TODO: use future host field from safekeeper spec
367 : // Pass the list of safekeepers to the replica so that it can connect to any of them,
368 : // whichever is available.
369 0 : let sk_ports = self
370 0 : .env
371 0 : .safekeepers
372 0 : .iter()
373 0 : .map(|x| x.get_compute_port().to_string())
374 0 : .collect::<Vec<_>>()
375 0 : .join(",");
376 0 : let sk_hosts = vec!["localhost"; self.env.safekeepers.len()].join(",");
377 0 :
378 0 : let connstr = format!(
379 0 : "host={} port={} options='-c timeline_id={} tenant_id={}' application_name=replica replication=true",
380 0 : sk_hosts,
381 0 : sk_ports,
382 0 : &self.timeline_id.to_string(),
383 0 : &self.tenant_id.to_string(),
384 0 : );
385 0 :
386 0 : let slot_name = format!("repl_{}_", self.timeline_id);
387 0 : conf.append("primary_conninfo", connstr.as_str());
388 0 : conf.append("primary_slot_name", slot_name.as_str());
389 0 : conf.append("hot_standby", "on");
390 0 : // prefetching of blocks referenced in WAL doesn't make sense for us
391 0 : // Neon hot standby ignores pages that are not in the shared_buffers
392 0 : if self.pg_version >= 15 {
393 0 : conf.append("recovery_prefetch", "off");
394 0 : }
395 : }
396 : }
397 :
398 0 : Ok(conf)
399 0 : }
400 :
401 0 : pub fn endpoint_path(&self) -> PathBuf {
402 0 : self.env.endpoints_path().join(&self.endpoint_id)
403 0 : }
404 :
405 0 : pub fn pgdata(&self) -> PathBuf {
406 0 : self.endpoint_path().join("pgdata")
407 0 : }
408 :
409 0 : pub fn status(&self) -> EndpointStatus {
410 0 : let timeout = Duration::from_millis(300);
411 0 : let has_pidfile = self.pgdata().join("postmaster.pid").exists();
412 0 : let can_connect = TcpStream::connect_timeout(&self.pg_address, timeout).is_ok();
413 0 :
414 0 : match (has_pidfile, can_connect) {
415 0 : (true, true) => EndpointStatus::Running,
416 0 : (false, false) => EndpointStatus::Stopped,
417 0 : (true, false) => EndpointStatus::Crashed,
418 0 : (false, true) => EndpointStatus::RunningNoPidfile,
419 : }
420 0 : }
421 :
422 0 : fn pg_ctl(&self, args: &[&str], auth_token: &Option<String>) -> Result<()> {
423 0 : let pg_ctl_path = self.env.pg_bin_dir(self.pg_version)?.join("pg_ctl");
424 0 : let mut cmd = Command::new(&pg_ctl_path);
425 0 : cmd.args(
426 0 : [
427 0 : &[
428 0 : "-D",
429 0 : self.pgdata().to_str().unwrap(),
430 0 : "-w", //wait till pg_ctl actually does what was asked
431 0 : ],
432 0 : args,
433 0 : ]
434 0 : .concat(),
435 0 : )
436 0 : .env_clear()
437 0 : .env(
438 0 : "LD_LIBRARY_PATH",
439 0 : self.env.pg_lib_dir(self.pg_version)?.to_str().unwrap(),
440 0 : )
441 0 : .env(
442 0 : "DYLD_LIBRARY_PATH",
443 0 : self.env.pg_lib_dir(self.pg_version)?.to_str().unwrap(),
444 : );
445 :
446 : // Pass authentication token used for the connections to pageserver and safekeepers
447 0 : if let Some(token) = auth_token {
448 0 : cmd.env("NEON_AUTH_TOKEN", token);
449 0 : }
450 :
451 0 : let pg_ctl = cmd
452 0 : .output()
453 0 : .context(format!("{} failed", pg_ctl_path.display()))?;
454 0 : if !pg_ctl.status.success() {
455 0 : anyhow::bail!(
456 0 : "pg_ctl failed, exit code: {}, stdout: {}, stderr: {}",
457 0 : pg_ctl.status,
458 0 : String::from_utf8_lossy(&pg_ctl.stdout),
459 0 : String::from_utf8_lossy(&pg_ctl.stderr),
460 0 : );
461 0 : }
462 0 :
463 0 : Ok(())
464 0 : }
465 :
466 0 : fn wait_for_compute_ctl_to_exit(&self, send_sigterm: bool) -> Result<()> {
467 0 : // TODO use background_process::stop_process instead: https://github.com/neondatabase/neon/pull/6482
468 0 : let pidfile_path = self.endpoint_path().join("compute_ctl.pid");
469 0 : let pid: u32 = std::fs::read_to_string(pidfile_path)?.parse()?;
470 0 : let pid = nix::unistd::Pid::from_raw(pid as i32);
471 0 : if send_sigterm {
472 0 : kill(pid, Signal::SIGTERM).ok();
473 0 : }
474 0 : crate::background_process::wait_until_stopped("compute_ctl", pid)?;
475 0 : Ok(())
476 0 : }
477 :
478 0 : fn read_postgresql_conf(&self) -> Result<String> {
479 0 : // Slurp the endpoints/<endpoint id>/postgresql.conf file into
480 0 : // memory. We will include it in the spec file that we pass to
481 0 : // `compute_ctl`, and `compute_ctl` will write it to the postgresql.conf
482 0 : // in the data directory.
483 0 : let postgresql_conf_path = self.endpoint_path().join("postgresql.conf");
484 0 : match std::fs::read(&postgresql_conf_path) {
485 0 : Ok(content) => Ok(String::from_utf8(content)?),
486 0 : Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok("".to_string()),
487 0 : Err(e) => Err(anyhow::Error::new(e).context(format!(
488 0 : "failed to read config file in {}",
489 0 : postgresql_conf_path.to_str().unwrap()
490 0 : ))),
491 : }
492 0 : }
493 :
494 0 : fn build_pageserver_connstr(pageservers: &[(Host, u16)]) -> String {
495 0 : pageservers
496 0 : .iter()
497 0 : .map(|(host, port)| format!("postgresql://no_user@{host}:{port}"))
498 0 : .collect::<Vec<_>>()
499 0 : .join(",")
500 0 : }
501 :
502 : /// Map safekeepers ids to the actual connection strings.
503 0 : fn build_safekeepers_connstrs(&self, sk_ids: Vec<NodeId>) -> Result<Vec<String>> {
504 0 : let mut safekeeper_connstrings = Vec::new();
505 0 : if self.mode == ComputeMode::Primary {
506 0 : for sk_id in sk_ids {
507 0 : let sk = self
508 0 : .env
509 0 : .safekeepers
510 0 : .iter()
511 0 : .find(|node| node.id == sk_id)
512 0 : .ok_or_else(|| anyhow!("safekeeper {sk_id} does not exist"))?;
513 0 : safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.get_compute_port()));
514 : }
515 0 : }
516 0 : Ok(safekeeper_connstrings)
517 0 : }
518 :
519 0 : pub async fn start(
520 0 : &self,
521 0 : auth_token: &Option<String>,
522 0 : safekeepers: Vec<NodeId>,
523 0 : pageservers: Vec<(Host, u16)>,
524 0 : remote_ext_config: Option<&String>,
525 0 : shard_stripe_size: usize,
526 0 : create_test_user: bool,
527 0 : ) -> Result<()> {
528 0 : if self.status() == EndpointStatus::Running {
529 0 : anyhow::bail!("The endpoint is already running");
530 0 : }
531 :
532 0 : let postgresql_conf = self.read_postgresql_conf()?;
533 :
534 : // We always start the compute node from scratch, so if the Postgres
535 : // data dir exists from a previous launch, remove it first.
536 0 : if self.pgdata().exists() {
537 0 : std::fs::remove_dir_all(self.pgdata())?;
538 0 : }
539 :
540 0 : let pageserver_connstring = Self::build_pageserver_connstr(&pageservers);
541 0 : assert!(!pageserver_connstring.is_empty());
542 :
543 0 : let safekeeper_connstrings = self.build_safekeepers_connstrs(safekeepers)?;
544 :
545 : // check for file remote_extensions_spec.json
546 : // if it is present, read it and pass to compute_ctl
547 0 : let remote_extensions_spec_path = self.endpoint_path().join("remote_extensions_spec.json");
548 0 : let remote_extensions_spec = std::fs::File::open(remote_extensions_spec_path);
549 : let remote_extensions: Option<RemoteExtSpec>;
550 :
551 0 : if let Ok(spec_file) = remote_extensions_spec {
552 0 : remote_extensions = serde_json::from_reader(spec_file).ok();
553 0 : } else {
554 0 : remote_extensions = None;
555 0 : };
556 :
557 : // Create spec file
558 0 : let spec = ComputeSpec {
559 0 : skip_pg_catalog_updates: self.skip_pg_catalog_updates,
560 0 : format_version: 1.0,
561 0 : operation_uuid: None,
562 0 : features: self.features.clone(),
563 0 : swap_size_bytes: None,
564 0 : cluster: Cluster {
565 0 : cluster_id: None, // project ID: not used
566 0 : name: None, // project name: not used
567 0 : state: None,
568 0 : roles: if create_test_user {
569 0 : vec![Role {
570 0 : name: PgIdent::from_str("test").unwrap(),
571 0 : encrypted_password: None,
572 0 : options: None,
573 0 : }]
574 : } else {
575 0 : Vec::new()
576 : },
577 0 : databases: if create_test_user {
578 0 : vec![Database {
579 0 : name: PgIdent::from_str("neondb").unwrap(),
580 0 : owner: PgIdent::from_str("test").unwrap(),
581 0 : options: None,
582 0 : restrict_conn: false,
583 0 : invalid: false,
584 0 : }]
585 : } else {
586 0 : Vec::new()
587 : },
588 0 : settings: None,
589 0 : postgresql_conf: Some(postgresql_conf),
590 0 : },
591 0 : delta_operations: None,
592 0 : tenant_id: Some(self.tenant_id),
593 0 : timeline_id: Some(self.timeline_id),
594 0 : mode: self.mode,
595 0 : pageserver_connstring: Some(pageserver_connstring),
596 0 : safekeeper_connstrings,
597 0 : storage_auth_token: auth_token.clone(),
598 0 : remote_extensions,
599 0 : pgbouncer_settings: None,
600 0 : shard_stripe_size: Some(shard_stripe_size),
601 0 : };
602 0 : let spec_path = self.endpoint_path().join("spec.json");
603 0 : std::fs::write(spec_path, serde_json::to_string_pretty(&spec)?)?;
604 :
605 : // Open log file. We'll redirect the stdout and stderr of `compute_ctl` to it.
606 0 : let logfile = std::fs::OpenOptions::new()
607 0 : .create(true)
608 0 : .append(true)
609 0 : .open(self.endpoint_path().join("compute.log"))?;
610 :
611 : // Launch compute_ctl
612 0 : let conn_str = self.connstr("cloud_admin", "postgres");
613 0 : println!("Starting postgres node at '{}'", conn_str);
614 0 : if create_test_user {
615 0 : let conn_str = self.connstr("test", "neondb");
616 0 : println!("Also at '{}'", conn_str);
617 0 : }
618 0 : let mut cmd = Command::new(self.env.neon_distrib_dir.join("compute_ctl"));
619 0 : cmd.args(["--http-port", &self.http_address.port().to_string()])
620 0 : .args(["--pgdata", self.pgdata().to_str().unwrap()])
621 0 : .args(["--connstr", &conn_str])
622 0 : .args([
623 0 : "--spec-path",
624 0 : self.endpoint_path().join("spec.json").to_str().unwrap(),
625 0 : ])
626 0 : .args([
627 0 : "--pgbin",
628 0 : self.env
629 0 : .pg_bin_dir(self.pg_version)?
630 0 : .join("postgres")
631 0 : .to_str()
632 0 : .unwrap(),
633 0 : ])
634 0 : .stdin(std::process::Stdio::null())
635 0 : .stderr(logfile.try_clone()?)
636 0 : .stdout(logfile);
637 :
638 0 : if let Some(remote_ext_config) = remote_ext_config {
639 0 : cmd.args(["--remote-ext-config", remote_ext_config]);
640 0 : }
641 :
642 0 : let child = cmd.spawn()?;
643 : // set up a scopeguard to kill & wait for the child in case we panic or bail below
644 0 : let child = scopeguard::guard(child, |mut child| {
645 0 : println!("SIGKILL & wait the started process");
646 0 : (|| {
647 0 : // TODO: use another signal that can be caught by the child so it can clean up any children it spawned
648 0 : child.kill().context("SIGKILL child")?;
649 0 : child.wait().context("wait() for child process")?;
650 0 : anyhow::Ok(())
651 0 : })()
652 0 : .with_context(|| format!("scopeguard kill&wait child {child:?}"))
653 0 : .unwrap();
654 0 : });
655 0 :
656 0 : // Write down the pid so we can wait for it when we want to stop
657 0 : // TODO use background_process::start_process instead: https://github.com/neondatabase/neon/pull/6482
658 0 : let pid = child.id();
659 0 : let pidfile_path = self.endpoint_path().join("compute_ctl.pid");
660 0 : std::fs::write(pidfile_path, pid.to_string())?;
661 :
662 : // Wait for it to start
663 0 : let mut attempt = 0;
664 : const ATTEMPT_INTERVAL: Duration = Duration::from_millis(100);
665 : const MAX_ATTEMPTS: u32 = 10 * 90; // Wait up to 1.5 min
666 : loop {
667 0 : attempt += 1;
668 0 : match self.get_status().await {
669 0 : Ok(state) => {
670 0 : match state.status {
671 : ComputeStatus::Init => {
672 0 : if attempt == MAX_ATTEMPTS {
673 0 : bail!("compute startup timed out; still in Init state");
674 0 : }
675 : // keep retrying
676 : }
677 : ComputeStatus::Running => {
678 : // All good!
679 0 : break;
680 : }
681 : ComputeStatus::Failed => {
682 0 : bail!(
683 0 : "compute startup failed: {}",
684 0 : state
685 0 : .error
686 0 : .as_deref()
687 0 : .unwrap_or("<no error from compute_ctl>")
688 0 : );
689 : }
690 : ComputeStatus::Empty
691 : | ComputeStatus::ConfigurationPending
692 : | ComputeStatus::Configuration
693 : | ComputeStatus::TerminationPending
694 : | ComputeStatus::Terminated => {
695 0 : bail!("unexpected compute status: {:?}", state.status)
696 : }
697 : }
698 : }
699 0 : Err(e) => {
700 0 : if attempt == MAX_ATTEMPTS {
701 0 : return Err(e).context("timed out waiting to connect to compute_ctl HTTP");
702 0 : }
703 : }
704 : }
705 0 : std::thread::sleep(ATTEMPT_INTERVAL);
706 : }
707 :
708 : // disarm the scopeguard, let the child outlive this function (and neon_local invoction)
709 0 : drop(scopeguard::ScopeGuard::into_inner(child));
710 0 :
711 0 : Ok(())
712 0 : }
713 :
714 : // Call the /status HTTP API
715 0 : pub async fn get_status(&self) -> Result<ComputeState> {
716 0 : let client = reqwest::Client::new();
717 :
718 0 : let response = client
719 0 : .request(
720 0 : reqwest::Method::GET,
721 0 : format!(
722 0 : "http://{}:{}/status",
723 0 : self.http_address.ip(),
724 0 : self.http_address.port()
725 0 : ),
726 0 : )
727 0 : .send()
728 0 : .await?;
729 :
730 : // Interpret the response
731 0 : let status = response.status();
732 0 : if !(status.is_client_error() || status.is_server_error()) {
733 0 : Ok(response.json().await?)
734 : } else {
735 : // reqwest does not export its error construction utility functions, so let's craft the message ourselves
736 0 : let url = response.url().to_owned();
737 0 : let msg = match response.text().await {
738 0 : Ok(err_body) => format!("Error: {}", err_body),
739 0 : Err(_) => format!("Http error ({}) at {}.", status.as_u16(), url),
740 : };
741 0 : Err(anyhow::anyhow!(msg))
742 : }
743 0 : }
744 :
745 0 : pub async fn reconfigure(
746 0 : &self,
747 0 : mut pageservers: Vec<(Host, u16)>,
748 0 : stripe_size: Option<ShardStripeSize>,
749 0 : safekeepers: Option<Vec<NodeId>>,
750 0 : ) -> Result<()> {
751 0 : let mut spec: ComputeSpec = {
752 0 : let spec_path = self.endpoint_path().join("spec.json");
753 0 : let file = std::fs::File::open(spec_path)?;
754 0 : serde_json::from_reader(file)?
755 : };
756 :
757 0 : let postgresql_conf = self.read_postgresql_conf()?;
758 0 : spec.cluster.postgresql_conf = Some(postgresql_conf);
759 0 :
760 0 : // If we weren't given explicit pageservers, query the storage controller
761 0 : if pageservers.is_empty() {
762 0 : let storage_controller = StorageController::from_env(&self.env);
763 0 : let locate_result = storage_controller.tenant_locate(self.tenant_id).await?;
764 0 : pageservers = locate_result
765 0 : .shards
766 0 : .into_iter()
767 0 : .map(|shard| {
768 0 : (
769 0 : Host::parse(&shard.listen_pg_addr)
770 0 : .expect("Storage controller reported bad hostname"),
771 0 : shard.listen_pg_port,
772 0 : )
773 0 : })
774 0 : .collect::<Vec<_>>();
775 0 : }
776 :
777 0 : let pageserver_connstr = Self::build_pageserver_connstr(&pageservers);
778 0 : assert!(!pageserver_connstr.is_empty());
779 0 : spec.pageserver_connstring = Some(pageserver_connstr);
780 0 : if stripe_size.is_some() {
781 0 : spec.shard_stripe_size = stripe_size.map(|s| s.0 as usize);
782 0 : }
783 :
784 : // If safekeepers are not specified, don't change them.
785 0 : if let Some(safekeepers) = safekeepers {
786 0 : let safekeeper_connstrings = self.build_safekeepers_connstrs(safekeepers)?;
787 0 : spec.safekeeper_connstrings = safekeeper_connstrings;
788 0 : }
789 :
790 0 : let client = reqwest::Client::builder()
791 0 : .timeout(Duration::from_secs(30))
792 0 : .build()
793 0 : .unwrap();
794 0 : let response = client
795 0 : .post(format!(
796 0 : "http://{}:{}/configure",
797 0 : self.http_address.ip(),
798 0 : self.http_address.port()
799 0 : ))
800 0 : .body(format!(
801 0 : "{{\"spec\":{}}}",
802 0 : serde_json::to_string_pretty(&spec)?
803 : ))
804 0 : .send()
805 0 : .await?;
806 :
807 0 : let status = response.status();
808 0 : if !(status.is_client_error() || status.is_server_error()) {
809 0 : Ok(())
810 : } else {
811 0 : let url = response.url().to_owned();
812 0 : let msg = match response.text().await {
813 0 : Ok(err_body) => format!("Error: {}", err_body),
814 0 : Err(_) => format!("Http error ({}) at {}.", status.as_u16(), url),
815 : };
816 0 : Err(anyhow::anyhow!(msg))
817 : }
818 0 : }
819 :
820 0 : pub fn stop(&self, mode: &str, destroy: bool) -> Result<()> {
821 0 : self.pg_ctl(&["-m", mode, "stop"], &None)?;
822 :
823 : // Also wait for the compute_ctl process to die. It might have some
824 : // cleanup work to do after postgres stops, like syncing safekeepers,
825 : // etc.
826 : //
827 : // If destroying or stop mode is immediate, send it SIGTERM before
828 : // waiting. Sometimes we do *not* want this cleanup: tests intentionally
829 : // do stop when majority of safekeepers is down, so sync-safekeepers
830 : // would hang otherwise. This could be a separate flag though.
831 0 : let send_sigterm = destroy || mode == "immediate";
832 0 : self.wait_for_compute_ctl_to_exit(send_sigterm)?;
833 0 : if destroy {
834 0 : println!(
835 0 : "Destroying postgres data directory '{}'",
836 0 : self.pgdata().to_str().unwrap()
837 0 : );
838 0 : std::fs::remove_dir_all(self.endpoint_path())?;
839 0 : }
840 0 : Ok(())
841 0 : }
842 :
843 0 : pub fn connstr(&self, user: &str, db_name: &str) -> String {
844 0 : format!(
845 0 : "postgresql://{}@{}:{}/{}",
846 0 : user,
847 0 : self.pg_address.ip(),
848 0 : self.pg_address.port(),
849 0 : db_name
850 0 : )
851 0 : }
852 : }
|