From 9a7471603f7b44ff7d64e70844696d14c732e0cc Mon Sep 17 00:00:00 2001 From: tabVersion Date: Mon, 19 Aug 2024 17:56:53 +0800 Subject: [PATCH 1/2] test Signed-off-by: tabVersion --- src/common/src/lib.rs | 1 + src/common/src/paid_feature.rs | 170 ++++++++++++++++++ src/connector/src/parser/mod.rs | 2 +- src/connector/src/sink/big_query.rs | 2 +- src/connector/src/sink/clickhouse.rs | 2 +- src/connector/src/sink/dynamodb.rs | 2 +- src/connector/src/sink/remote.rs | 2 +- src/connector/src/sink/snowflake.rs | 2 +- src/connector/src/sink/sqlserver.rs | 2 +- src/expr/impl/src/scalar/test_license.rs | 2 +- src/frontend/src/handler/create_secret.rs | 2 +- src/frontend/src/handler/create_source.rs | 2 +- src/frontend/src/handler/create_table.rs | 2 +- src/frontend/src/handler/drop_secret.rs | 2 +- src/frontend/src/optimizer/plan_node/utils.rs | 2 +- src/license/src/feature.rs | 125 ------------- src/license/src/lib.rs | 1 - src/license/src/manager.rs | 4 +- 18 files changed, 186 insertions(+), 141 deletions(-) create mode 100644 src/common/src/paid_feature.rs diff --git a/src/common/src/lib.rs b/src/common/src/lib.rs index 8d47d0c62164..93b59434d28d 100644 --- a/src/common/src/lib.rs +++ b/src/common/src/lib.rs @@ -84,6 +84,7 @@ pub use { }; pub mod lru; pub mod opts; +pub mod paid_feature; pub mod range; pub mod row; pub mod sequence; diff --git a/src/common/src/paid_feature.rs b/src/common/src/paid_feature.rs new file mode 100644 index 000000000000..7f75dc18e13e --- /dev/null +++ b/src/common/src/paid_feature.rs @@ -0,0 +1,170 @@ +// Copyright 2024 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use risingwave_license::{License, LicenseKeyError, LicenseManager, Tier}; +use risingwave_pb::telemetry::PbTelemetryEventStage; +use thiserror::Error; + +use crate::telemetry::report::report_event_common; + +/// Define all features that are available based on the tier of the license. +/// +/// # Define a new feature +/// +/// To add a new feature, add a new entry below following the same pattern as the existing ones. +/// +/// Check the definition of [`Tier`] for all available tiers. Note that normally there's no need to +/// add a feature with the minimum tier of `Free`, as you can directly write the code without +/// gating it with a feature check. +/// +/// # Check the availability of a feature +/// +/// To check the availability of a feature during runtime, call the method +/// [`check_available`](Feature::check_available) on the feature. If the feature is not available, +/// an error of type [`FeatureNotAvailable`] will be returned and you should handle it properly, +/// generally by returning an error to the user. +/// +/// # Feature availability in tests +/// +/// In tests with `debug_assertions` enabled, a license key of the paid (maximum) tier is set by +/// default. As a result, all features are available in tests. To test the behavior when a feature +/// is not available, you can manually set a license key with a lower tier. Check the e2e test cases +/// under `error_ui` for examples. +macro_rules! for_all_features { + ($macro:ident) => { + $macro! { + // name min tier doc + { TestPaid, Paid, "A dummy feature that's only available on paid tier for testing purposes." }, + { TimeTravel, Paid, "Query historical data within the retention period."}, + { GlueSchemaRegistry, Paid, "Use Schema Registry from AWS Glue rather than Confluent." }, + { SnowflakeSink, Paid, "Delivering data to SnowFlake." }, + { DynamoDbSink, Paid, "Delivering data to DynamoDb." }, + { OpenSearchSink, Paid, "Delivering data to OpenSearch." }, + { BigQuerySink, Paid, "Delivering data to BigQuery." }, + { ClickHouseSharedEngine,Paid, "Delivering data to Shared tree on clickhouse cloud"}, + { SecretManagement, Paid, "Secret management." }, + { CdcTableSchemaMap, Paid, "Automatically map upstream schema to CDC Table."}, + { SqlServerSink, Paid, "Sink data from RisingWave to SQL Server." }, + { SqlServerCdcSource, Paid, "CDC source connector for Sql Server." }, + } + }; +} + +macro_rules! def_feature { + ($({ $name:ident, $min_tier:ident, $doc:literal },)*) => { + /// A set of features that are available based on the tier of the license. + /// + /// To define a new feature, add a new entry in the macro [`for_all_features`]. + #[derive(Clone, Copy, Debug)] + pub enum Feature { + $( + #[doc = concat!($doc, "\n\nAvailable for tier `", stringify!($min_tier), "` and above.")] + $name, + )* + } + + impl Feature { + /// Minimum tier required to use this feature. + fn min_tier(self) -> Tier { + match self { + $( + Self::$name => Tier::$min_tier, + )* + } + } + + fn get_feature_name(&self) -> &'static str { + match &self { + $( + Self::$name => stringify!($name), + )* + } + } + } + }; +} + +for_all_features!(def_feature); + +/// The error type for feature not available due to license. +#[derive(Debug, Error)] +pub enum FeatureNotAvailable { + #[error( + "feature {:?} is only available for tier {:?} and above, while the current tier is {:?}\n\n\ + Hint: You may want to set a license key with `ALTER SYSTEM SET license_key = '...';` command.", + feature, feature.min_tier(), current_tier, + )] + InsufficientTier { + feature: Feature, + current_tier: Tier, + }, + + #[error("feature {feature:?} is not available due to license error")] + LicenseError { + feature: Feature, + source: LicenseKeyError, + }, +} + +impl Feature { + /// Check whether the feature is available based on the current license. + pub fn check_available(self) -> Result<(), FeatureNotAvailable> { + let check_res = match LicenseManager::get().license() { + Ok(license) => { + if license.tier >= self.min_tier() { + Ok(()) + } else { + Err(FeatureNotAvailable::InsufficientTier { + feature: self, + current_tier: license.tier, + }) + } + } + Err(error) => { + // If there's a license key error, we still try against the default license first + // to see if the feature is available for free. + if License::default().tier >= self.min_tier() { + Ok(()) + } else { + Err(FeatureNotAvailable::LicenseError { + feature: self, + source: error, + }) + } + } + }; + if let feature_name = Self::get_feature_name(&self) + && !feature_name.eq_ignore_ascii_case("TestPaid") + { + let mut attr_builder = jsonbb::Builder::>::new(); + attr_builder.begin_object(); + attr_builder.add_string("success"); + attr_builder.add_value(jsonbb::ValueRef::Bool(check_res.is_ok())); + attr_builder.end_object(); + let attr = attr_builder.finish(); + + report_event_common( + PbTelemetryEventStage::Unspecified, + feature_name, + 0, + None, + None, + Some(attr), + "paywall".to_string(), + ); + } + + check_res + } +} diff --git a/src/connector/src/parser/mod.rs b/src/connector/src/parser/mod.rs index 6cd843c0384b..01ca43f5272e 100644 --- a/src/connector/src/parser/mod.rs +++ b/src/connector/src/parser/mod.rs @@ -1233,7 +1233,7 @@ impl SpecificParserConfig { config.schema_location = if let Some(schema_arn) = format_encode_options_with_secret.get(AWS_GLUE_SCHEMA_ARN_KEY) { - risingwave_common::license::Feature::GlueSchemaRegistry + risingwave_common::paid_feature::Feature::GlueSchemaRegistry .check_available() .map_err(anyhow::Error::from)?; SchemaLocation::Glue { diff --git a/src/connector/src/sink/big_query.rs b/src/connector/src/sink/big_query.rs index 22146e86d0d1..6d42c8b5405c 100644 --- a/src/connector/src/sink/big_query.rs +++ b/src/connector/src/sink/big_query.rs @@ -358,7 +358,7 @@ impl Sink for BigQuerySink { } async fn validate(&self) -> Result<()> { - risingwave_common::license::Feature::BigQuerySink + risingwave_common::paid_feature::Feature::BigQuerySink .check_available() .map_err(|e| anyhow::anyhow!(e))?; if !self.is_append_only && self.pk_indices.is_empty() { diff --git a/src/connector/src/sink/clickhouse.rs b/src/connector/src/sink/clickhouse.rs index 4337f2b9d76b..f6099235c1a5 100644 --- a/src/connector/src/sink/clickhouse.rs +++ b/src/connector/src/sink/clickhouse.rs @@ -530,7 +530,7 @@ impl Sink for ClickHouseSink { let (clickhouse_column, clickhouse_engine) = query_column_engine_from_ck(client, &self.config).await?; if clickhouse_engine.is_shared_tree() { - risingwave_common::license::Feature::ClickHouseSharedEngine + risingwave_common::paid_feature::Feature::ClickHouseSharedEngine .check_available() .map_err(|e| anyhow::anyhow!(e))?; } diff --git a/src/connector/src/sink/dynamodb.rs b/src/connector/src/sink/dynamodb.rs index 6d73bf2d478c..64fbe0ccfb2e 100644 --- a/src/connector/src/sink/dynamodb.rs +++ b/src/connector/src/sink/dynamodb.rs @@ -88,7 +88,7 @@ impl Sink for DynamoDbSink { const SINK_NAME: &'static str = DYNAMO_DB_SINK; async fn validate(&self) -> Result<()> { - risingwave_common::license::Feature::DynamoDbSink + risingwave_common::paid_feature::Feature::DynamoDbSink .check_available() .map_err(|e| anyhow::anyhow!(e))?; let client = (self.config.build_client().await) diff --git a/src/connector/src/sink/remote.rs b/src/connector/src/sink/remote.rs index 606965a8424d..6239d5653085 100644 --- a/src/connector/src/sink/remote.rs +++ b/src/connector/src/sink/remote.rs @@ -166,7 +166,7 @@ impl Sink for RemoteSink { async fn validate_remote_sink(param: &SinkParam, sink_name: &str) -> ConnectorResult<()> { if sink_name == OpenSearchSink::SINK_NAME { - risingwave_common::license::Feature::OpenSearchSink + risingwave_common::paid_feature::Feature::OpenSearchSink .check_available() .map_err(|e| anyhow::anyhow!(e))?; } diff --git a/src/connector/src/sink/snowflake.rs b/src/connector/src/sink/snowflake.rs index d87072a2502e..10aff0593006 100644 --- a/src/connector/src/sink/snowflake.rs +++ b/src/connector/src/sink/snowflake.rs @@ -118,7 +118,7 @@ impl Sink for SnowflakeSink { } async fn validate(&self) -> Result<()> { - risingwave_common::license::Feature::SnowflakeSink + risingwave_common::paid_feature::Feature::SnowflakeSink .check_available() .map_err(|e| anyhow::anyhow!(e))?; if !self.is_append_only { diff --git a/src/connector/src/sink/sqlserver.rs b/src/connector/src/sink/sqlserver.rs index d40e2a2647b2..66be48bf6865 100644 --- a/src/connector/src/sink/sqlserver.rs +++ b/src/connector/src/sink/sqlserver.rs @@ -144,7 +144,7 @@ impl Sink for SqlServerSink { const SINK_NAME: &'static str = SQLSERVER_SINK; async fn validate(&self) -> Result<()> { - risingwave_common::license::Feature::SqlServerSink + risingwave_common::paid_feature::Feature::SqlServerSink .check_available() .map_err(|e| anyhow::anyhow!(e))?; diff --git a/src/expr/impl/src/scalar/test_license.rs b/src/expr/impl/src/scalar/test_license.rs index de1afd5ed616..ea4f99ace651 100644 --- a/src/expr/impl/src/scalar/test_license.rs +++ b/src/expr/impl/src/scalar/test_license.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use risingwave_common::license::Feature; +use risingwave_common::paid_feature::Feature; use risingwave_expr::{function, ExprError, Result}; /// A function that checks if the `TestPaid` feature is available. diff --git a/src/frontend/src/handler/create_secret.rs b/src/frontend/src/handler/create_secret.rs index 9810751361be..20846e804afb 100644 --- a/src/frontend/src/handler/create_secret.rs +++ b/src/frontend/src/handler/create_secret.rs @@ -15,7 +15,7 @@ use pgwire::pg_response::{PgResponse, StatementType}; use prost::Message; use risingwave_common::bail_not_implemented; -use risingwave_common::license::Feature; +use risingwave_common::paid_feature::Feature; use risingwave_sqlparser::ast::{CreateSecretStatement, SqlOption, Value}; use crate::error::{ErrorCode, Result}; diff --git a/src/frontend/src/handler/create_source.rs b/src/frontend/src/handler/create_source.rs index 9c7a264816a6..f1910f17a6bf 100644 --- a/src/frontend/src/handler/create_source.rs +++ b/src/frontend/src/handler/create_source.rs @@ -27,7 +27,7 @@ use risingwave_common::catalog::{ debug_assert_column_ids_distinct, ColumnCatalog, ColumnDesc, ColumnId, Schema, TableId, INITIAL_SOURCE_VERSION_ID, KAFKA_TIMESTAMP_COLUMN_NAME, }; -use risingwave_common::license::Feature; +use risingwave_common::paid_feature::Feature; use risingwave_common::secret::LocalSecretManager; use risingwave_common::types::DataType; use risingwave_connector::parser::additional_columns::{ diff --git a/src/frontend/src/handler/create_table.rs b/src/frontend/src/handler/create_table.rs index 6e57ab52c87f..41ae72700e9a 100644 --- a/src/frontend/src/handler/create_table.rs +++ b/src/frontend/src/handler/create_table.rs @@ -26,7 +26,7 @@ use risingwave_common::catalog::{ CdcTableDesc, ColumnCatalog, ColumnDesc, TableId, TableVersionId, DEFAULT_SCHEMA_NAME, INITIAL_TABLE_VERSION_ID, }; -use risingwave_common::license::Feature; +use risingwave_common::paid_feature::Feature; use risingwave_common::util::sort_util::{ColumnOrder, OrderType}; use risingwave_common::util::value_encoding::DatumToProtoExt; use risingwave_connector::source::cdc::external::{ diff --git a/src/frontend/src/handler/drop_secret.rs b/src/frontend/src/handler/drop_secret.rs index eff4b35224b8..feed5fa9186c 100644 --- a/src/frontend/src/handler/drop_secret.rs +++ b/src/frontend/src/handler/drop_secret.rs @@ -13,7 +13,7 @@ // limitations under the License. use pgwire::pg_response::StatementType; -use risingwave_common::license::Feature; +use risingwave_common::paid_feature::Feature; use risingwave_sqlparser::ast::ObjectName; use crate::catalog::root_catalog::SchemaPath; diff --git a/src/frontend/src/optimizer/plan_node/utils.rs b/src/frontend/src/optimizer/plan_node/utils.rs index 2ac317d597ba..c6e323987414 100644 --- a/src/frontend/src/optimizer/plan_node/utils.rs +++ b/src/frontend/src/optimizer/plan_node/utils.rs @@ -320,7 +320,7 @@ macro_rules! plan_node_name { }; } pub(crate) use plan_node_name; -use risingwave_common::license::Feature; +use risingwave_common::paid_feature::Feature; use risingwave_common::types::{DataType, Interval}; use risingwave_expr::aggregate::PbAggKind; use risingwave_pb::plan_common::as_of::AsOfType; diff --git a/src/license/src/feature.rs b/src/license/src/feature.rs index e434e3709ac2..691a35b9d982 100644 --- a/src/license/src/feature.rs +++ b/src/license/src/feature.rs @@ -11,128 +11,3 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - -use thiserror::Error; - -use super::{License, LicenseKeyError, LicenseManager, Tier}; - -/// Define all features that are available based on the tier of the license. -/// -/// # Define a new feature -/// -/// To add a new feature, add a new entry below following the same pattern as the existing ones. -/// -/// Check the definition of [`Tier`] for all available tiers. Note that normally there's no need to -/// add a feature with the minimum tier of `Free`, as you can directly write the code without -/// gating it with a feature check. -/// -/// # Check the availability of a feature -/// -/// To check the availability of a feature during runtime, call the method -/// [`check_available`](Feature::check_available) on the feature. If the feature is not available, -/// an error of type [`FeatureNotAvailable`] will be returned and you should handle it properly, -/// generally by returning an error to the user. -/// -/// # Feature availability in tests -/// -/// In tests with `debug_assertions` enabled, a license key of the paid (maximum) tier is set by -/// default. As a result, all features are available in tests. To test the behavior when a feature -/// is not available, you can manually set a license key with a lower tier. Check the e2e test cases -/// under `error_ui` for examples. -macro_rules! for_all_features { - ($macro:ident) => { - $macro! { - // name min tier doc - { TestPaid, Paid, "A dummy feature that's only available on paid tier for testing purposes." }, - { TimeTravel, Paid, "Query historical data within the retention period."}, - { GlueSchemaRegistry, Paid, "Use Schema Registry from AWS Glue rather than Confluent." }, - { SnowflakeSink, Paid, "Delivering data to SnowFlake." }, - { DynamoDbSink, Paid, "Delivering data to DynamoDb." }, - { OpenSearchSink, Paid, "Delivering data to OpenSearch." }, - { BigQuerySink, Paid, "Delivering data to BigQuery." }, - { ClickHouseSharedEngine,Paid, "Delivering data to Shared tree on clickhouse cloud"}, - { SecretManagement, Paid, "Secret management." }, - { CdcTableSchemaMap, Paid, "Automatically map upstream schema to CDC Table."}, - { SqlServerSink, Paid, "Sink data from RisingWave to SQL Server." }, - { SqlServerCdcSource, Paid, "CDC source connector for Sql Server." }, - } - }; -} - -macro_rules! def_feature { - ($({ $name:ident, $min_tier:ident, $doc:literal },)*) => { - /// A set of features that are available based on the tier of the license. - /// - /// To define a new feature, add a new entry in the macro [`for_all_features`]. - #[derive(Clone, Copy, Debug)] - pub enum Feature { - $( - #[doc = concat!($doc, "\n\nAvailable for tier `", stringify!($min_tier), "` and above.")] - $name, - )* - } - - impl Feature { - /// Minimum tier required to use this feature. - fn min_tier(self) -> Tier { - match self { - $( - Self::$name => Tier::$min_tier, - )* - } - } - } - }; -} - -for_all_features!(def_feature); - -/// The error type for feature not available due to license. -#[derive(Debug, Error)] -pub enum FeatureNotAvailable { - #[error( - "feature {:?} is only available for tier {:?} and above, while the current tier is {:?}\n\n\ - Hint: You may want to set a license key with `ALTER SYSTEM SET license_key = '...';` command.", - feature, feature.min_tier(), current_tier, - )] - InsufficientTier { - feature: Feature, - current_tier: Tier, - }, - - #[error("feature {feature:?} is not available due to license error")] - LicenseError { - feature: Feature, - source: LicenseKeyError, - }, -} - -impl Feature { - /// Check whether the feature is available based on the current license. - pub fn check_available(self) -> Result<(), FeatureNotAvailable> { - match LicenseManager::get().license() { - Ok(license) => { - if license.tier >= self.min_tier() { - Ok(()) - } else { - Err(FeatureNotAvailable::InsufficientTier { - feature: self, - current_tier: license.tier, - }) - } - } - Err(error) => { - // If there's a license key error, we still try against the default license first - // to see if the feature is available for free. - if License::default().tier >= self.min_tier() { - Ok(()) - } else { - Err(FeatureNotAvailable::LicenseError { - feature: self, - source: error, - }) - } - } - } - } -} diff --git a/src/license/src/lib.rs b/src/license/src/lib.rs index bdcac9044104..7c907e46db8d 100644 --- a/src/license/src/lib.rs +++ b/src/license/src/lib.rs @@ -16,6 +16,5 @@ mod feature; mod key; mod manager; -pub use feature::*; pub use key::*; pub use manager::*; diff --git a/src/license/src/manager.rs b/src/license/src/manager.rs index cac51105358a..2f04545f7bee 100644 --- a/src/license/src/manager.rs +++ b/src/license/src/manager.rs @@ -60,7 +60,7 @@ pub enum Issuer { // TODO(license): Shall we add a version field? #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "snake_case")] -pub(super) struct License { +pub struct License { /// Subject of the license. /// /// See . @@ -166,7 +166,7 @@ impl LicenseManager { /// Get the current license if it is valid. /// /// Since the license can expire, the returned license should not be cached by the caller. - pub(super) fn license(&self) -> Result { + pub fn license(&self) -> Result { let license = self.inner.read().unwrap().license.clone()?; // Check the expiration time additionally. From 3b99a39bd51a5af734b03bfb8d1f9efeec6c4940 Mon Sep 17 00:00:00 2001 From: tabVersion Date: Mon, 19 Aug 2024 18:06:10 +0800 Subject: [PATCH 2/2] stash Signed-off-by: tabVersion --- src/common/src/paid_feature.rs | 7 ++++--- src/license/src/feature.rs | 13 ------------- src/license/src/lib.rs | 1 - 3 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 src/license/src/feature.rs diff --git a/src/common/src/paid_feature.rs b/src/common/src/paid_feature.rs index 7f75dc18e13e..46cb0a1d314f 100644 --- a/src/common/src/paid_feature.rs +++ b/src/common/src/paid_feature.rs @@ -144,9 +144,10 @@ impl Feature { } } }; - if let feature_name = Self::get_feature_name(&self) - && !feature_name.eq_ignore_ascii_case("TestPaid") - { + + // Report the event to telemetry + let feature_name = Self::get_feature_name(&self); + if !feature_name.eq_ignore_ascii_case("TestPaid") { let mut attr_builder = jsonbb::Builder::>::new(); attr_builder.begin_object(); attr_builder.add_string("success"); diff --git a/src/license/src/feature.rs b/src/license/src/feature.rs deleted file mode 100644 index 691a35b9d982..000000000000 --- a/src/license/src/feature.rs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2024 RisingWave Labs -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. diff --git a/src/license/src/lib.rs b/src/license/src/lib.rs index 7c907e46db8d..fc9680743978 100644 --- a/src/license/src/lib.rs +++ b/src/license/src/lib.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod feature; mod key; mod manager;