diff --git a/compute_tools/src/compute.rs b/compute_tools/src/compute.rs index 28770acdcd..9969b2166c 100644 --- a/compute_tools/src/compute.rs +++ b/compute_tools/src/compute.rs @@ -22,7 +22,7 @@ use utils::id::{TenantId, TimelineId}; use utils::lsn::Lsn; use compute_api::responses::{ComputeMetrics, ComputeStatus}; -use compute_api::spec::{ComputeMode, ComputeSpec}; +use compute_api::spec::{ComputeFeature, ComputeMode, ComputeSpec}; use utils::measured_stream::MeasuredReader; use remote_storage::{DownloadError, RemotePath}; @@ -277,6 +277,17 @@ fn create_neon_superuser(spec: &ComputeSpec, client: &mut Client) -> Result<()> } impl ComputeNode { + /// Check that compute node has corresponding feature enabled. + pub fn has_feature(&self, feature: ComputeFeature) -> bool { + let state = self.state.lock().unwrap(); + + if let Some(s) = state.pspec.as_ref() { + s.spec.features.contains(&feature) + } else { + false + } + } + pub fn set_status(&self, status: ComputeStatus) { let mut state = self.state.lock().unwrap(); state.status = status; diff --git a/control_plane/src/endpoint.rs b/control_plane/src/endpoint.rs index 12b1250764..a566f03db9 100644 --- a/control_plane/src/endpoint.rs +++ b/control_plane/src/endpoint.rs @@ -519,6 +519,7 @@ impl Endpoint { skip_pg_catalog_updates: self.skip_pg_catalog_updates, format_version: 1.0, operation_uuid: None, + features: vec![], cluster: Cluster { cluster_id: None, // project ID: not used name: None, // project name: not used diff --git a/libs/compute_api/src/spec.rs b/libs/compute_api/src/spec.rs index 175b4461ac..d9c384a5d3 100644 --- a/libs/compute_api/src/spec.rs +++ b/libs/compute_api/src/spec.rs @@ -26,6 +26,13 @@ pub struct ComputeSpec { // but we don't use it for anything. Serde will ignore missing fields when // deserializing it. pub operation_uuid: Option, + + /// Compute features to enable. These feature flags are provided, when we + /// know all the details about client's compute, so they cannot be used + /// to change `Empty` compute behavior. + #[serde(default)] + pub features: Vec, + /// Expected cluster state at the end of transition process. pub cluster: Cluster, pub delta_operations: Option>, @@ -68,6 +75,19 @@ pub struct ComputeSpec { pub remote_extensions: Option, } +/// Feature flag to signal `compute_ctl` to enable certain experimental functionality. +#[derive(Serialize, Clone, Copy, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ComputeFeature { + // XXX: Add more feature flags here. + + // This is a special feature flag that is used to represent unknown feature flags. + // Basically all unknown to enum flags are represented as this one. See unit test + // `parse_unknown_features()` for more details. + #[serde(other)] + UnknownFeature, +} + #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct RemoteExtSpec { pub public_extensions: Option>, @@ -229,7 +249,10 @@ mod tests { #[test] fn parse_spec_file() { let file = File::open("tests/cluster_spec.json").unwrap(); - let _spec: ComputeSpec = serde_json::from_reader(file).unwrap(); + let spec: ComputeSpec = serde_json::from_reader(file).unwrap(); + + // Features list defaults to empty vector. + assert!(spec.features.is_empty()); } #[test] @@ -241,4 +264,22 @@ mod tests { ob.insert("unknown_field_123123123".into(), "hello".into()); let _spec: ComputeSpec = serde_json::from_value(json).unwrap(); } + + #[test] + fn parse_unknown_features() { + // Test that unknown feature flags do not cause any errors. + let file = File::open("tests/cluster_spec.json").unwrap(); + let mut json: serde_json::Value = serde_json::from_reader(file).unwrap(); + let ob = json.as_object_mut().unwrap(); + + // Add unknown feature flags. + let features = vec!["foo_bar_feature", "baz_feature"]; + ob.insert("features".into(), features.into()); + + let spec: ComputeSpec = serde_json::from_value(json).unwrap(); + + assert!(spec.features.len() == 2); + assert!(spec.features.contains(&ComputeFeature::UnknownFeature)); + assert_eq!(spec.features, vec![ComputeFeature::UnknownFeature; 2]); + } }