From 092635f71beeaefd720f7a376d73377507154633 Mon Sep 17 00:00:00 2001 From: shikhar Date: Sat, 5 Sep 2026 19:17:26 -0700 Subject: [PATCH 1/3] feat: add Native storage class to shared interfaces --- README.md | 2 ++ api/src/v1/config.rs | 10 ++++++- cli/schema.json | 3 ++- cli/src/apply.rs | 3 +++ cli/src/types.rs | 3 +++ common/src/config.rs | 2 ++ lite/src/backend/basins.rs | 4 ++- lite/src/backend/error.rs | 4 +++ lite/src/backend/mod.rs | 11 +++++++- lite/src/backend/streams.rs | 8 ++++++ lite/src/handlers/v1/error.rs | 2 ++ lite/tests/backend/control_plane/basin.rs | 26 ++++++++++++++++++ lite/tests/backend/control_plane/stream.rs | 31 ++++++++++++++++++++++ resource-spec/src/lib.rs | 4 ++- sdk/src/types.rs | 22 ++++++++++++--- 15 files changed, 126 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9c3eb14e..8473f368 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ headless host that has no credential store. It uses [SlateDB](https://slatedb.io) as its storage engine, which relies entirely on object storage for durability. +Lite accepts `standard` and `express` storage-class settings for API compatibility; both use the configured SlateDB backend. The `native` storage class is not supported and is rejected in stream configurations and basin defaults. + It is easy to run `s2 lite` against object stores like AWS S3 and Tigris. It is a single-node binary with no other external dependencies. You can also simply not specify a `--bucket`, which makes it operate entirely in-memory (or use `--local-root` to persist to local disk instead). diff --git a/api/src/v1/config.rs b/api/src/v1/config.rs index d48c9a87..6727f94d 100644 --- a/api/src/v1/config.rs +++ b/api/src/v1/config.rs @@ -13,6 +13,8 @@ pub enum StorageClass { Standard, /// Append tail latency under 40 milliseconds with s2.dev. Express, + /// Native storage class. Not supported by S2 Lite. + Native, } impl From for s2_common::config::StorageClass { @@ -20,6 +22,7 @@ impl From for s2_common::config::StorageClass { match value { StorageClass::Express => Self::Express, StorageClass::Standard => Self::Standard, + StorageClass::Native => Self::Native, } } } @@ -29,6 +32,7 @@ impl From for StorageClass { match value { s2_common::config::StorageClass::Express => Self::Express, s2_common::config::StorageClass::Standard => Self::Standard, + s2_common::config::StorageClass::Native => Self::Native, } } } @@ -598,7 +602,11 @@ mod tests { use super::*; fn gen_storage_class() -> impl Strategy { - prop_oneof![Just(StorageClass::Standard), Just(StorageClass::Express)] + prop_oneof![ + Just(StorageClass::Standard), + Just(StorageClass::Express), + Just(StorageClass::Native), + ] } fn gen_timestamping_mode() -> impl Strategy { diff --git a/cli/schema.json b/cli/schema.json index c6f9c997..3659ddcb 100644 --- a/cli/schema.json +++ b/cli/schema.json @@ -139,7 +139,8 @@ "type": "string", "enum": [ "standard", - "express" + "express", + "native" ] }, "RetentionPolicy": { diff --git a/cli/src/apply.rs b/cli/src/apply.rs index 7235508d..0855ccce 100644 --- a/cli/src/apply.rs +++ b/cli/src/apply.rs @@ -49,6 +49,7 @@ fn storage_class_from_sdk(storage_class: s2_sdk::types::StorageClass) -> Storage match storage_class { s2_sdk::types::StorageClass::Standard => StorageClass::Standard, s2_sdk::types::StorageClass::Express => StorageClass::Express, + s2_sdk::types::StorageClass::Native => StorageClass::Native, } } @@ -127,6 +128,7 @@ fn storage_class_to_sdk( match storage_class { s2_resource_spec::StorageClass::Standard => s2_sdk::types::StorageClass::Standard, s2_resource_spec::StorageClass::Express => s2_sdk::types::StorageClass::Express, + s2_resource_spec::StorageClass::Native => s2_sdk::types::StorageClass::Native, } } @@ -311,6 +313,7 @@ fn format_storage_class(sc: StorageClass) -> &'static str { match sc { StorageClass::Standard => "standard", StorageClass::Express => "express", + StorageClass::Native => "native", } } diff --git a/cli/src/types.rs b/cli/src/types.rs index 699a04b9..3acd5534 100644 --- a/cli/src/types.rs +++ b/cli/src/types.rs @@ -208,6 +208,7 @@ pub use sdk::types::LocationName; pub enum StorageClass { Standard, Express, + Native, } #[derive(ValueEnum, Debug, Clone, Serialize)] @@ -322,6 +323,7 @@ impl From for sdk::types::StorageClass { match class { StorageClass::Standard => sdk::types::StorageClass::Standard, StorageClass::Express => sdk::types::StorageClass::Express, + StorageClass::Native => sdk::types::StorageClass::Native, } } } @@ -331,6 +333,7 @@ impl From for StorageClass { match class { sdk::types::StorageClass::Standard => StorageClass::Standard, sdk::types::StorageClass::Express => StorageClass::Express, + sdk::types::StorageClass::Native => StorageClass::Native, } } } diff --git a/common/src/config.rs b/common/src/config.rs index 83526ec2..338a02e4 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -49,6 +49,8 @@ pub enum StorageClass { #[default] #[strum(serialize = "express")] Express = 2, + #[strum(serialize = "native")] + Native = 3, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/lite/src/backend/basins.rs b/lite/src/backend/basins.rs index c37511af..c737754f 100644 --- a/lite/src/backend/basins.rs +++ b/lite/src/backend/basins.rs @@ -11,7 +11,7 @@ use slatedb::{ }; use time::OffsetDateTime; -use super::{Backend, bgtasks::BgtaskTrigger, store::db_txn_get}; +use super::{Backend, bgtasks::BgtaskTrigger, store::db_txn_get, validate_storage_class}; use crate::backend::{ error::{ BasinAlreadyExistsError, BasinDeletionPendingError, BasinNotFoundError, DeleteBasinError, @@ -69,6 +69,7 @@ impl Backend { config: BasinConfig, mode: ProvisionMode, ) -> Result, ProvisionBasinError> { + validate_storage_class(config.default_stream_config.storage_class)?; let meta_key = kv::basin_meta::ser_key(&basin); let txn = self.db.begin(IsolationLevel::SerializableSnapshot).await?; @@ -176,6 +177,7 @@ impl Backend { } meta.config = meta.config.reconfigure(reconfig); + validate_storage_class(meta.config.default_stream_config.storage_class)?; txn.put(&meta_key, kv::basin_meta::ser_value(&meta))?; diff --git a/lite/src/backend/error.rs b/lite/src/backend/error.rs index 3c441a65..92bd484d 100644 --- a/lite/src/backend/error.rs +++ b/lite/src/backend/error.rs @@ -420,6 +420,8 @@ pub enum ProvisionBasinError { BasinAlreadyExists(#[from] BasinAlreadyExistsError), #[error(transparent)] BasinDeletionPending(#[from] BasinDeletionPendingError), + #[error(transparent)] + Validation(#[from] s2_common::ValidationError), } impl From for ProvisionBasinError { @@ -450,6 +452,8 @@ pub enum ReconfigureBasinError { BasinNotFound(#[from] BasinNotFoundError), #[error(transparent)] BasinDeletionPending(#[from] BasinDeletionPendingError), + #[error(transparent)] + Validation(#[from] s2_common::ValidationError), } impl From for ReconfigureBasinError { diff --git a/lite/src/backend/mod.rs b/lite/src/backend/mod.rs index be8db24f..a582f285 100644 --- a/lite/src/backend/mod.rs +++ b/lite/src/backend/mod.rs @@ -1,4 +1,4 @@ -use s2_common::encryption::EncryptionSpec; +use s2_common::{ValidationError, config::StorageClass, encryption::EncryptionSpec}; pub mod error; @@ -25,3 +25,12 @@ pub struct StreamHandle { } pub const FOLLOWER_MAX_LAG: usize = 25; + +fn validate_storage_class(storage_class: Option) -> Result<(), ValidationError> { + if storage_class == Some(StorageClass::Native) { + return Err(ValidationError( + "native storage class is not supported by S2 Lite".to_owned(), + )); + } + Ok(()) +} diff --git a/lite/src/backend/streams.rs b/lite/src/backend/streams.rs index b59d8032..7a23d929 100644 --- a/lite/src/backend/streams.rs +++ b/lite/src/backend/streams.rs @@ -17,6 +17,7 @@ use super::{ Backend, store::db_txn_get, streamer::{TerminalTrimCondition, TerminalTrimOutcome, doe_arm_delay}, + validate_storage_class, }; use crate::{ backend::{ @@ -102,6 +103,12 @@ impl Backend { return Err(BasinDeletionPendingError { basin }.into()); } + validate_storage_class( + config + .storage_class + .or(basin_meta.config.default_stream_config.storage_class), + )?; + let stream_meta_key = kv::stream_meta::ser_key(&basin, &stream); // Existence is decided from a Memory-level read; capture the row's @@ -314,6 +321,7 @@ impl Backend { meta.config = OptionalStreamConfig::from(meta.config) .reconfigure(reconfig) .merge(basin_meta.config.default_stream_config); + validate_storage_class(Some(meta.config.storage_class))?; txn.put(&meta_key, kv::stream_meta::ser_value(&meta))?; diff --git a/lite/src/handlers/v1/error.rs b/lite/src/handlers/v1/error.rs index b25e34b0..3a2e944d 100644 --- a/lite/src/handlers/v1/error.rs +++ b/lite/src/handlers/v1/error.rs @@ -108,6 +108,7 @@ impl ServiceError { ProvisionBasinError::BasinDeletionPending(e) => { standard(ErrorCode::BasinDeletionPending, e.to_string()) } + ProvisionBasinError::Validation(e) => standard(ErrorCode::Invalid, e.to_string()), }, ServiceError::GetBasinConfig(e) => match e { GetBasinConfigError::Storage(e) => standard(ErrorCode::Storage, e.to_string()), @@ -135,6 +136,7 @@ impl ServiceError { ReconfigureBasinError::BasinDeletionPending(e) => { standard(ErrorCode::BasinDeletionPending, e.to_string()) } + ReconfigureBasinError::Validation(e) => standard(ErrorCode::Invalid, e.to_string()), }, ServiceError::ListStreams(e) => match e { ListStreamsError::Storage(e) => standard(ErrorCode::Storage, e.to_string()), diff --git a/lite/tests/backend/control_plane/basin.rs b/lite/tests/backend/control_plane/basin.rs index f5d09f22..59742fea 100644 --- a/lite/tests/backend/control_plane/basin.rs +++ b/lite/tests/backend/control_plane/basin.rs @@ -298,6 +298,15 @@ async fn test_reconfigure_basin_updates_nested_defaults() { let mut initial_config = BasinConfig::default(); initial_config.default_stream_config.storage_class = Some(StorageClass::Standard); + let mut native_config = initial_config.clone(); + native_config.default_stream_config.storage_class = Some(StorageClass::Native); + assert!(matches!( + backend + .provision_basin(basin_name.clone(), native_config, ProvisionMode::Ensure) + .await, + Err(ProvisionBasinError::Validation(_)) + )); + backend .provision_basin( basin_name.clone(), @@ -347,6 +356,23 @@ async fn test_reconfigure_basin_updates_nested_defaults() { Some(TimestampingMode::Arrival) ); + assert!(matches!( + backend + .reconfigure_basin( + basin_name.clone(), + BasinReconfiguration { + default_stream_config: Maybe::from(Some(StreamReconfiguration { + storage_class: Maybe::from(Some(StorageClass::Native)), + ..Default::default() + })), + create_stream_on_append: Maybe::from(false), + ..Default::default() + }, + ) + .await, + Err(ReconfigureBasinError::Validation(_)) + )); + let fetched = backend .get_basin_config(basin_name) .await diff --git a/lite/tests/backend/control_plane/stream.rs b/lite/tests/backend/control_plane/stream.rs index a3ca62c5..d60c5335 100644 --- a/lite/tests/backend/control_plane/stream.rs +++ b/lite/tests/backend/control_plane/stream.rs @@ -531,6 +531,20 @@ async fn test_reconfigure_stream_updates_selected_fields() { .expect("Failed to create basin"); let stream_name = test_stream_name("stream-reconfigure"); + assert!(matches!( + backend + .provision_stream( + basin_name.clone(), + stream_name.clone(), + OptionalStreamConfig { + storage_class: Some(StorageClass::Native), + ..Default::default() + }, + ProvisionMode::Ensure, + ) + .await, + Err(ProvisionStreamError::Validation(_)) + )); let initial_config = OptionalStreamConfig { retention_policy: Some(RetentionPolicy::Age(Duration::from_secs(60))), timestamping: OptionalTimestampingConfig { @@ -573,6 +587,23 @@ async fn test_reconfigure_stream_updates_selected_fields() { assert_eq!(updated.timestamping.mode, TimestampingMode::Arrival); assert!(updated.timestamping.uncapped); + assert!(matches!( + backend + .reconfigure_stream( + basin_name.clone(), + stream_name.clone(), + StreamReconfiguration { + storage_class: Maybe::from(Some(StorageClass::Native)), + retention_policy: Maybe::from(Some(RetentionPolicy::Age(Duration::from_secs( + 60 + ),))), + ..Default::default() + }, + ) + .await, + Err(ReconfigureStreamError::Validation(_)) + )); + let fetched = backend .get_stream_config(basin_name, stream_name) .await diff --git a/resource-spec/src/lib.rs b/resource-spec/src/lib.rs index 7a0ea7fa..99531fe5 100644 --- a/resource-spec/src/lib.rs +++ b/resource-spec/src/lib.rs @@ -70,6 +70,7 @@ pub struct StreamConfig { pub enum StorageClass { Standard, Express, + Native, } impl schemars::JsonSchema for StorageClass { @@ -81,7 +82,7 @@ impl schemars::JsonSchema for StorageClass { schemars::json_schema!({ "type": "string", "description": "Storage class for recent writes.", - "enum": ["standard", "express"] + "enum": ["standard", "express", "native"] }) } } @@ -91,6 +92,7 @@ impl From for s2_common::config::StorageClass { match s { StorageClass::Standard => Self::Standard, StorageClass::Express => Self::Express, + StorageClass::Native => Self::Native, } } } diff --git a/sdk/src/types.rs b/sdk/src/types.rs index a66f8c44..5e57f59e 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -738,6 +738,8 @@ pub enum StorageClass { Standard, /// Express storage class that offers append latencies under `50ms`. Express, + /// Native storage class. Not supported by S2 Lite. + Native, } impl From for StorageClass { @@ -745,6 +747,7 @@ impl From for StorageClass { match value { api::config::StorageClass::Standard => StorageClass::Standard, api::config::StorageClass::Express => StorageClass::Express, + api::config::StorageClass::Native => StorageClass::Native, } } } @@ -754,6 +757,7 @@ impl From for api::config::StorageClass { match value { StorageClass::Standard => api::config::StorageClass::Standard, StorageClass::Express => api::config::StorageClass::Express, + StorageClass::Native => api::config::StorageClass::Native, } } } @@ -4115,11 +4119,21 @@ mod tests { // -- StorageClass -- #[rstest] - #[case::standard(StorageClass::Standard)] - #[case::express(StorageClass::Express)] - fn storage_class_roundtrip(#[case] sdk: StorageClass) { + #[case::standard(StorageClass::Standard, "standard", 1)] + #[case::express(StorageClass::Express, "express", 2)] + #[case::native(StorageClass::Native, "native", 3)] + fn storage_class_roundtrip(#[case] sdk: StorageClass, #[case] wire: &str, #[case] repr: u8) { let api: api::config::StorageClass = sdk.into(); - let back: StorageClass = api.into(); + assert_eq!(serde_json::to_value(api).unwrap(), wire); + let decoded: api::config::StorageClass = + serde_json::from_value(serde_json::json!(wire)).unwrap(); + let common: s2_common::config::StorageClass = decoded.into(); + assert_eq!(common as u8, repr); + assert_eq!( + s2_common::config::StorageClass::from_repr(repr), + Some(common) + ); + let back: StorageClass = api::config::StorageClass::from(common).into(); assert_eq!(back, sdk); } From 84089e77017bb6fd025375334494e1ed1e554919 Mon Sep 17 00:00:00 2001 From: shikhar Date: Wed, 9 Sep 2026 19:31:13 -0700 Subject: [PATCH 2/3] fix: keep Native storage settings compatible with Lite --- README.md | 2 -- api/src/v1/config.rs | 2 +- lite/src/backend/basins.rs | 4 +-- lite/src/backend/error.rs | 4 --- lite/src/backend/mod.rs | 11 +------- lite/src/backend/streams.rs | 8 ------ lite/src/handlers/v1/error.rs | 2 -- lite/tests/backend/control_plane/basin.rs | 26 ------------------ lite/tests/backend/control_plane/stream.rs | 31 ---------------------- sdk/src/types.rs | 2 +- 10 files changed, 4 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 8473f368..9c3eb14e 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,6 @@ headless host that has no credential store. It uses [SlateDB](https://slatedb.io) as its storage engine, which relies entirely on object storage for durability. -Lite accepts `standard` and `express` storage-class settings for API compatibility; both use the configured SlateDB backend. The `native` storage class is not supported and is rejected in stream configurations and basin defaults. - It is easy to run `s2 lite` against object stores like AWS S3 and Tigris. It is a single-node binary with no other external dependencies. You can also simply not specify a `--bucket`, which makes it operate entirely in-memory (or use `--local-root` to persist to local disk instead). diff --git a/api/src/v1/config.rs b/api/src/v1/config.rs index 6727f94d..95ffb3da 100644 --- a/api/src/v1/config.rs +++ b/api/src/v1/config.rs @@ -13,7 +13,7 @@ pub enum StorageClass { Standard, /// Append tail latency under 40 milliseconds with s2.dev. Express, - /// Native storage class. Not supported by S2 Lite. + /// Append tail latency under 4 ms with s2.dev. Native, } diff --git a/lite/src/backend/basins.rs b/lite/src/backend/basins.rs index c737754f..c37511af 100644 --- a/lite/src/backend/basins.rs +++ b/lite/src/backend/basins.rs @@ -11,7 +11,7 @@ use slatedb::{ }; use time::OffsetDateTime; -use super::{Backend, bgtasks::BgtaskTrigger, store::db_txn_get, validate_storage_class}; +use super::{Backend, bgtasks::BgtaskTrigger, store::db_txn_get}; use crate::backend::{ error::{ BasinAlreadyExistsError, BasinDeletionPendingError, BasinNotFoundError, DeleteBasinError, @@ -69,7 +69,6 @@ impl Backend { config: BasinConfig, mode: ProvisionMode, ) -> Result, ProvisionBasinError> { - validate_storage_class(config.default_stream_config.storage_class)?; let meta_key = kv::basin_meta::ser_key(&basin); let txn = self.db.begin(IsolationLevel::SerializableSnapshot).await?; @@ -177,7 +176,6 @@ impl Backend { } meta.config = meta.config.reconfigure(reconfig); - validate_storage_class(meta.config.default_stream_config.storage_class)?; txn.put(&meta_key, kv::basin_meta::ser_value(&meta))?; diff --git a/lite/src/backend/error.rs b/lite/src/backend/error.rs index 92bd484d..3c441a65 100644 --- a/lite/src/backend/error.rs +++ b/lite/src/backend/error.rs @@ -420,8 +420,6 @@ pub enum ProvisionBasinError { BasinAlreadyExists(#[from] BasinAlreadyExistsError), #[error(transparent)] BasinDeletionPending(#[from] BasinDeletionPendingError), - #[error(transparent)] - Validation(#[from] s2_common::ValidationError), } impl From for ProvisionBasinError { @@ -452,8 +450,6 @@ pub enum ReconfigureBasinError { BasinNotFound(#[from] BasinNotFoundError), #[error(transparent)] BasinDeletionPending(#[from] BasinDeletionPendingError), - #[error(transparent)] - Validation(#[from] s2_common::ValidationError), } impl From for ReconfigureBasinError { diff --git a/lite/src/backend/mod.rs b/lite/src/backend/mod.rs index a582f285..be8db24f 100644 --- a/lite/src/backend/mod.rs +++ b/lite/src/backend/mod.rs @@ -1,4 +1,4 @@ -use s2_common::{ValidationError, config::StorageClass, encryption::EncryptionSpec}; +use s2_common::encryption::EncryptionSpec; pub mod error; @@ -25,12 +25,3 @@ pub struct StreamHandle { } pub const FOLLOWER_MAX_LAG: usize = 25; - -fn validate_storage_class(storage_class: Option) -> Result<(), ValidationError> { - if storage_class == Some(StorageClass::Native) { - return Err(ValidationError( - "native storage class is not supported by S2 Lite".to_owned(), - )); - } - Ok(()) -} diff --git a/lite/src/backend/streams.rs b/lite/src/backend/streams.rs index 7a23d929..b59d8032 100644 --- a/lite/src/backend/streams.rs +++ b/lite/src/backend/streams.rs @@ -17,7 +17,6 @@ use super::{ Backend, store::db_txn_get, streamer::{TerminalTrimCondition, TerminalTrimOutcome, doe_arm_delay}, - validate_storage_class, }; use crate::{ backend::{ @@ -103,12 +102,6 @@ impl Backend { return Err(BasinDeletionPendingError { basin }.into()); } - validate_storage_class( - config - .storage_class - .or(basin_meta.config.default_stream_config.storage_class), - )?; - let stream_meta_key = kv::stream_meta::ser_key(&basin, &stream); // Existence is decided from a Memory-level read; capture the row's @@ -321,7 +314,6 @@ impl Backend { meta.config = OptionalStreamConfig::from(meta.config) .reconfigure(reconfig) .merge(basin_meta.config.default_stream_config); - validate_storage_class(Some(meta.config.storage_class))?; txn.put(&meta_key, kv::stream_meta::ser_value(&meta))?; diff --git a/lite/src/handlers/v1/error.rs b/lite/src/handlers/v1/error.rs index 3a2e944d..b25e34b0 100644 --- a/lite/src/handlers/v1/error.rs +++ b/lite/src/handlers/v1/error.rs @@ -108,7 +108,6 @@ impl ServiceError { ProvisionBasinError::BasinDeletionPending(e) => { standard(ErrorCode::BasinDeletionPending, e.to_string()) } - ProvisionBasinError::Validation(e) => standard(ErrorCode::Invalid, e.to_string()), }, ServiceError::GetBasinConfig(e) => match e { GetBasinConfigError::Storage(e) => standard(ErrorCode::Storage, e.to_string()), @@ -136,7 +135,6 @@ impl ServiceError { ReconfigureBasinError::BasinDeletionPending(e) => { standard(ErrorCode::BasinDeletionPending, e.to_string()) } - ReconfigureBasinError::Validation(e) => standard(ErrorCode::Invalid, e.to_string()), }, ServiceError::ListStreams(e) => match e { ListStreamsError::Storage(e) => standard(ErrorCode::Storage, e.to_string()), diff --git a/lite/tests/backend/control_plane/basin.rs b/lite/tests/backend/control_plane/basin.rs index 59742fea..f5d09f22 100644 --- a/lite/tests/backend/control_plane/basin.rs +++ b/lite/tests/backend/control_plane/basin.rs @@ -298,15 +298,6 @@ async fn test_reconfigure_basin_updates_nested_defaults() { let mut initial_config = BasinConfig::default(); initial_config.default_stream_config.storage_class = Some(StorageClass::Standard); - let mut native_config = initial_config.clone(); - native_config.default_stream_config.storage_class = Some(StorageClass::Native); - assert!(matches!( - backend - .provision_basin(basin_name.clone(), native_config, ProvisionMode::Ensure) - .await, - Err(ProvisionBasinError::Validation(_)) - )); - backend .provision_basin( basin_name.clone(), @@ -356,23 +347,6 @@ async fn test_reconfigure_basin_updates_nested_defaults() { Some(TimestampingMode::Arrival) ); - assert!(matches!( - backend - .reconfigure_basin( - basin_name.clone(), - BasinReconfiguration { - default_stream_config: Maybe::from(Some(StreamReconfiguration { - storage_class: Maybe::from(Some(StorageClass::Native)), - ..Default::default() - })), - create_stream_on_append: Maybe::from(false), - ..Default::default() - }, - ) - .await, - Err(ReconfigureBasinError::Validation(_)) - )); - let fetched = backend .get_basin_config(basin_name) .await diff --git a/lite/tests/backend/control_plane/stream.rs b/lite/tests/backend/control_plane/stream.rs index d60c5335..a3ca62c5 100644 --- a/lite/tests/backend/control_plane/stream.rs +++ b/lite/tests/backend/control_plane/stream.rs @@ -531,20 +531,6 @@ async fn test_reconfigure_stream_updates_selected_fields() { .expect("Failed to create basin"); let stream_name = test_stream_name("stream-reconfigure"); - assert!(matches!( - backend - .provision_stream( - basin_name.clone(), - stream_name.clone(), - OptionalStreamConfig { - storage_class: Some(StorageClass::Native), - ..Default::default() - }, - ProvisionMode::Ensure, - ) - .await, - Err(ProvisionStreamError::Validation(_)) - )); let initial_config = OptionalStreamConfig { retention_policy: Some(RetentionPolicy::Age(Duration::from_secs(60))), timestamping: OptionalTimestampingConfig { @@ -587,23 +573,6 @@ async fn test_reconfigure_stream_updates_selected_fields() { assert_eq!(updated.timestamping.mode, TimestampingMode::Arrival); assert!(updated.timestamping.uncapped); - assert!(matches!( - backend - .reconfigure_stream( - basin_name.clone(), - stream_name.clone(), - StreamReconfiguration { - storage_class: Maybe::from(Some(StorageClass::Native)), - retention_policy: Maybe::from(Some(RetentionPolicy::Age(Duration::from_secs( - 60 - ),))), - ..Default::default() - }, - ) - .await, - Err(ReconfigureStreamError::Validation(_)) - )); - let fetched = backend .get_stream_config(basin_name, stream_name) .await diff --git a/sdk/src/types.rs b/sdk/src/types.rs index 5e57f59e..c9da2792 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -738,7 +738,7 @@ pub enum StorageClass { Standard, /// Express storage class that offers append latencies under `50ms`. Express, - /// Native storage class. Not supported by S2 Lite. + /// Native storage class that offers append latencies under 4 ms. Native, } From c0f86857e6df9f438902d96e3122e5ae36070735 Mon Sep 17 00:00:00 2001 From: shikhar Date: Wed, 9 Sep 2026 20:34:41 -0700 Subject: [PATCH 3/3] docs: align storage class latency descriptions --- api/src/v1/config.rs | 4 ++-- sdk/src/types.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/v1/config.rs b/api/src/v1/config.rs index 95ffb3da..5ec7beba 100644 --- a/api/src/v1/config.rs +++ b/api/src/v1/config.rs @@ -9,9 +9,9 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "kebab-case")] pub enum StorageClass { - /// Append tail latency under 400 milliseconds with s2.dev. + /// Append tail latency under 400 ms with s2.dev. Standard, - /// Append tail latency under 40 milliseconds with s2.dev. + /// Append tail latency under 40 ms with s2.dev. Express, /// Append tail latency under 4 ms with s2.dev. Native, diff --git a/sdk/src/types.rs b/sdk/src/types.rs index c9da2792..1c0e3af2 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -734,11 +734,11 @@ impl Page { #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Storage class for recent appends. pub enum StorageClass { - /// Standard storage class that offers append latencies under `500ms`. + /// Append tail latency under 400 ms with s2.dev. Standard, - /// Express storage class that offers append latencies under `50ms`. + /// Append tail latency under 40 ms with s2.dev. Express, - /// Native storage class that offers append latencies under 4 ms. + /// Append tail latency under 4 ms with s2.dev. Native, }