mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-06 13:02:55 +00:00
storcon: make pageserver AZ id mandatory (#8856)
## Problem https://github.com/neondatabase/neon/pull/8852 introduced a new nullable column for the `nodes` table: `availability_zone_id` ## Summary of changes * Make neon local and the test suite always provide an az id * Make the az id field in the ps registration request mandatory * Migrate the column to non-nullable and adjust in memory state accordingly * Remove the code that was used to populate the az id for pre-existing nodes
This commit is contained in:
@@ -87,6 +87,7 @@ RUN mkdir -p /data/.neon/ && \
|
|||||||
"pg_distrib_dir='/usr/local/'\n" \
|
"pg_distrib_dir='/usr/local/'\n" \
|
||||||
"listen_pg_addr='0.0.0.0:6400'\n" \
|
"listen_pg_addr='0.0.0.0:6400'\n" \
|
||||||
"listen_http_addr='0.0.0.0:9898'\n" \
|
"listen_http_addr='0.0.0.0:9898'\n" \
|
||||||
|
"availability_zone='local'\n" \
|
||||||
> /data/.neon/pageserver.toml && \
|
> /data/.neon/pageserver.toml && \
|
||||||
chown -R neon:neon /data/.neon
|
chown -R neon:neon /data/.neon
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
listen_pg_port,
|
listen_pg_port,
|
||||||
listen_http_addr,
|
listen_http_addr,
|
||||||
listen_http_port,
|
listen_http_port,
|
||||||
availability_zone_id: Some(availability_zone_id),
|
availability_zone_id,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ pub struct NodeRegisterRequest {
|
|||||||
pub listen_http_addr: String,
|
pub listen_http_addr: String,
|
||||||
pub listen_http_port: u16,
|
pub listen_http_port: u16,
|
||||||
|
|
||||||
pub availability_zone_id: Option<String>,
|
pub availability_zone_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -141,10 +141,24 @@ impl ControlPlaneGenerationsApi for ControlPlaneClient {
|
|||||||
m.other
|
m.other
|
||||||
);
|
);
|
||||||
|
|
||||||
let az_id = m
|
let az_id = {
|
||||||
.other
|
let az_id_from_metadata = m
|
||||||
.get("availability_zone_id")
|
.other
|
||||||
.and_then(|jv| jv.as_str().map(|str| str.to_owned()));
|
.get("availability_zone_id")
|
||||||
|
.and_then(|jv| jv.as_str().map(|str| str.to_owned()));
|
||||||
|
|
||||||
|
match az_id_from_metadata {
|
||||||
|
Some(az_id) => Some(az_id),
|
||||||
|
None => {
|
||||||
|
tracing::warn!("metadata.json does not contain an 'availability_zone_id' field");
|
||||||
|
conf.availability_zone.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if az_id.is_none() {
|
||||||
|
panic!("Availablity zone id could not be inferred from metadata.json or pageserver config");
|
||||||
|
}
|
||||||
|
|
||||||
Some(NodeRegisterRequest {
|
Some(NodeRegisterRequest {
|
||||||
node_id: conf.id,
|
node_id: conf.id,
|
||||||
@@ -152,7 +166,7 @@ impl ControlPlaneGenerationsApi for ControlPlaneClient {
|
|||||||
listen_pg_port: m.postgres_port,
|
listen_pg_port: m.postgres_port,
|
||||||
listen_http_addr: m.http_host,
|
listen_http_addr: m.http_host,
|
||||||
listen_http_port: m.http_port,
|
listen_http_port: m.http_port,
|
||||||
availability_zone_id: az_id,
|
availability_zone_id: az_id.expect("Checked above"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE nodes ALTER availability_zone_id DROP NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE nodes ALTER availability_zone_id SET NOT NULL;
|
||||||
@@ -36,7 +36,7 @@ pub(crate) struct Node {
|
|||||||
listen_pg_addr: String,
|
listen_pg_addr: String,
|
||||||
listen_pg_port: u16,
|
listen_pg_port: u16,
|
||||||
|
|
||||||
availability_zone_id: Option<String>,
|
availability_zone_id: String,
|
||||||
|
|
||||||
// This cancellation token means "stop any RPCs in flight to this node, and don't start
|
// This cancellation token means "stop any RPCs in flight to this node, and don't start
|
||||||
// any more". It is not related to process shutdown.
|
// any more". It is not related to process shutdown.
|
||||||
@@ -63,8 +63,9 @@ impl Node {
|
|||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_availability_zone_id(&self) -> Option<&str> {
|
#[allow(unused)]
|
||||||
self.availability_zone_id.as_deref()
|
pub(crate) fn get_availability_zone_id(&self) -> &str {
|
||||||
|
self.availability_zone_id.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_scheduling(&self) -> NodeSchedulingPolicy {
|
pub(crate) fn get_scheduling(&self) -> NodeSchedulingPolicy {
|
||||||
@@ -78,22 +79,12 @@ impl Node {
|
|||||||
/// Does this registration request match `self`? This is used when deciding whether a registration
|
/// Does this registration request match `self`? This is used when deciding whether a registration
|
||||||
/// request should be allowed to update an existing record with the same node ID.
|
/// request should be allowed to update an existing record with the same node ID.
|
||||||
pub(crate) fn registration_match(&self, register_req: &NodeRegisterRequest) -> bool {
|
pub(crate) fn registration_match(&self, register_req: &NodeRegisterRequest) -> bool {
|
||||||
let az_ids_match = {
|
self.id == register_req.node_id
|
||||||
match (
|
|
||||||
self.availability_zone_id.as_deref(),
|
|
||||||
register_req.availability_zone_id.as_deref(),
|
|
||||||
) {
|
|
||||||
(Some(current_az), Some(register_req_az)) => current_az == register_req_az,
|
|
||||||
_ => true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
az_ids_match
|
|
||||||
&& self.id == register_req.node_id
|
|
||||||
&& self.listen_http_addr == register_req.listen_http_addr
|
&& self.listen_http_addr == register_req.listen_http_addr
|
||||||
&& self.listen_http_port == register_req.listen_http_port
|
&& self.listen_http_port == register_req.listen_http_port
|
||||||
&& self.listen_pg_addr == register_req.listen_pg_addr
|
&& self.listen_pg_addr == register_req.listen_pg_addr
|
||||||
&& self.listen_pg_port == register_req.listen_pg_port
|
&& self.listen_pg_port == register_req.listen_pg_port
|
||||||
|
&& self.availability_zone_id == register_req.availability_zone_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For a shard located on this node, populate a response object
|
/// For a shard located on this node, populate a response object
|
||||||
@@ -190,7 +181,7 @@ impl Node {
|
|||||||
listen_http_port: u16,
|
listen_http_port: u16,
|
||||||
listen_pg_addr: String,
|
listen_pg_addr: String,
|
||||||
listen_pg_port: u16,
|
listen_pg_port: u16,
|
||||||
availability_zone_id: Option<String>,
|
availability_zone_id: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ pub(crate) enum DatabaseOperation {
|
|||||||
ListMetadataHealthOutdated,
|
ListMetadataHealthOutdated,
|
||||||
GetLeader,
|
GetLeader,
|
||||||
UpdateLeader,
|
UpdateLeader,
|
||||||
SetNodeAzId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -325,31 +324,6 @@ impl Persistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn set_node_availability_zone_id(
|
|
||||||
&self,
|
|
||||||
input_node_id: NodeId,
|
|
||||||
input_az_id: String,
|
|
||||||
) -> DatabaseResult<()> {
|
|
||||||
use crate::schema::nodes::dsl::*;
|
|
||||||
let updated = self
|
|
||||||
.with_measured_conn(DatabaseOperation::SetNodeAzId, move |conn| {
|
|
||||||
let updated = diesel::update(nodes)
|
|
||||||
.filter(node_id.eq(input_node_id.0 as i64))
|
|
||||||
.set((availability_zone_id.eq(input_az_id.clone()),))
|
|
||||||
.execute(conn)?;
|
|
||||||
Ok(updated)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if updated != 1 {
|
|
||||||
Err(DatabaseError::Logical(format!(
|
|
||||||
"Node {node_id:?} not found for setting az id",
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// At startup, load the high level state for shards, such as their config + policy. This will
|
/// At startup, load the high level state for shards, such as their config + policy. This will
|
||||||
/// be enriched at runtime with state discovered on pageservers.
|
/// be enriched at runtime with state discovered on pageservers.
|
||||||
pub(crate) async fn list_tenant_shards(&self) -> DatabaseResult<Vec<TenantShardPersistence>> {
|
pub(crate) async fn list_tenant_shards(&self) -> DatabaseResult<Vec<TenantShardPersistence>> {
|
||||||
@@ -1110,7 +1084,7 @@ pub(crate) struct NodePersistence {
|
|||||||
pub(crate) listen_http_port: i32,
|
pub(crate) listen_http_port: i32,
|
||||||
pub(crate) listen_pg_addr: String,
|
pub(crate) listen_pg_addr: String,
|
||||||
pub(crate) listen_pg_port: i32,
|
pub(crate) listen_pg_port: i32,
|
||||||
pub(crate) availability_zone_id: Option<String>,
|
pub(crate) availability_zone_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tenant metadata health status that are stored durably.
|
/// Tenant metadata health status that are stored durably.
|
||||||
|
|||||||
@@ -528,7 +528,7 @@ pub(crate) mod test_utils {
|
|||||||
80 + i as u16,
|
80 + i as u16,
|
||||||
format!("pghost-{i}"),
|
format!("pghost-{i}"),
|
||||||
5432 + i as u16,
|
5432 + i as u16,
|
||||||
None,
|
"test-az".to_string(),
|
||||||
);
|
);
|
||||||
node.set_availability(NodeAvailability::Active(test_utilization::simple(0, 0)));
|
node.set_availability(NodeAvailability::Active(test_utilization::simple(0, 0)));
|
||||||
assert!(node.is_available());
|
assert!(node.is_available());
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ diesel::table! {
|
|||||||
listen_http_port -> Int4,
|
listen_http_port -> Int4,
|
||||||
listen_pg_addr -> Varchar,
|
listen_pg_addr -> Varchar,
|
||||||
listen_pg_port -> Int4,
|
listen_pg_port -> Int4,
|
||||||
availability_zone_id -> Nullable<Varchar>,
|
availability_zone_id -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1264,7 +1264,7 @@ impl Service {
|
|||||||
123,
|
123,
|
||||||
"".to_string(),
|
"".to_string(),
|
||||||
123,
|
123,
|
||||||
None,
|
"test_az".to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
scheduler.node_upsert(&node);
|
scheduler.node_upsert(&node);
|
||||||
@@ -4825,15 +4825,8 @@ impl Service {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if register_req.availability_zone_id.is_none() {
|
|
||||||
tracing::warn!(
|
|
||||||
"Node {} registering without specific availability zone id",
|
|
||||||
register_req.node_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RegistrationStatus {
|
enum RegistrationStatus {
|
||||||
Matched(Node),
|
Matched,
|
||||||
Mismatched,
|
Mismatched,
|
||||||
New,
|
New,
|
||||||
}
|
}
|
||||||
@@ -4842,7 +4835,7 @@ impl Service {
|
|||||||
let locked = self.inner.read().unwrap();
|
let locked = self.inner.read().unwrap();
|
||||||
if let Some(node) = locked.nodes.get(®ister_req.node_id) {
|
if let Some(node) = locked.nodes.get(®ister_req.node_id) {
|
||||||
if node.registration_match(®ister_req) {
|
if node.registration_match(®ister_req) {
|
||||||
RegistrationStatus::Matched(node.clone())
|
RegistrationStatus::Matched
|
||||||
} else {
|
} else {
|
||||||
RegistrationStatus::Mismatched
|
RegistrationStatus::Mismatched
|
||||||
}
|
}
|
||||||
@@ -4852,41 +4845,12 @@ impl Service {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match registration_status {
|
match registration_status {
|
||||||
RegistrationStatus::Matched(node) => {
|
RegistrationStatus::Matched => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Node {} re-registered with matching address",
|
"Node {} re-registered with matching address",
|
||||||
register_req.node_id
|
register_req.node_id
|
||||||
);
|
);
|
||||||
|
|
||||||
if node.get_availability_zone_id().is_none() {
|
|
||||||
if let Some(az_id) = register_req.availability_zone_id.clone() {
|
|
||||||
tracing::info!("Extracting availability zone id from registration request for node {}: {}",
|
|
||||||
register_req.node_id, az_id);
|
|
||||||
|
|
||||||
// Persist to the database and update in memory state. See comment below
|
|
||||||
// on ordering.
|
|
||||||
self.persistence
|
|
||||||
.set_node_availability_zone_id(register_req.node_id, az_id)
|
|
||||||
.await?;
|
|
||||||
let node_with_az = Node::new(
|
|
||||||
register_req.node_id,
|
|
||||||
register_req.listen_http_addr,
|
|
||||||
register_req.listen_http_port,
|
|
||||||
register_req.listen_pg_addr,
|
|
||||||
register_req.listen_pg_port,
|
|
||||||
register_req.availability_zone_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut locked = self.inner.write().unwrap();
|
|
||||||
let mut new_nodes = (*locked.nodes).clone();
|
|
||||||
|
|
||||||
locked.scheduler.node_upsert(&node_with_az);
|
|
||||||
new_nodes.insert(register_req.node_id, node_with_az);
|
|
||||||
|
|
||||||
locked.nodes = Arc::new(new_nodes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
RegistrationStatus::Mismatched => {
|
RegistrationStatus::Mismatched => {
|
||||||
|
|||||||
@@ -758,6 +758,9 @@ class NeonEnvBuilder:
|
|||||||
patch_script = ""
|
patch_script = ""
|
||||||
for ps in self.env.pageservers:
|
for ps in self.env.pageservers:
|
||||||
patch_script += f"UPDATE nodes SET listen_http_port={ps.service_port.http}, listen_pg_port={ps.service_port.pg} WHERE node_id = '{ps.id}';"
|
patch_script += f"UPDATE nodes SET listen_http_port={ps.service_port.http}, listen_pg_port={ps.service_port.pg} WHERE node_id = '{ps.id}';"
|
||||||
|
# This is a temporary to get the backward compat test happy
|
||||||
|
# since the compat snapshot was generated with an older version of neon local
|
||||||
|
patch_script += f"UPDATE nodes SET availability_zone_id='{ps.az_id}' WHERE node_id = '{ps.id}' AND availability_zone_id IS NULL;"
|
||||||
patch_script_path.write_text(patch_script)
|
patch_script_path.write_text(patch_script)
|
||||||
|
|
||||||
# Update the config with info about tenants and timelines
|
# Update the config with info about tenants and timelines
|
||||||
|
|||||||
Reference in New Issue
Block a user