Skip to main content

risingwave_meta/controller/
session_params.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use itertools::Itertools;
19use risingwave_common::config::SessionInitConfig;
20use risingwave_common::session_config::{SessionConfig, SessionConfigError};
21use risingwave_meta_model::prelude::SessionParameter;
22use risingwave_meta_model::session_parameter;
23use risingwave_pb::meta::SetSessionParamRequest;
24use risingwave_pb::meta::subscribe_response::{Info, Operation};
25use sea_orm::ActiveValue::Set;
26use sea_orm::{DatabaseConnection, EntityTrait, TransactionTrait};
27use thiserror_ext::AsReport;
28use tokio::sync::RwLock;
29use tracing::info;
30
31use crate::controller::SqlMetaStore;
32use crate::manager::{LocalNotification, NotificationManagerRef};
33use crate::{MetaError, MetaResult};
34
35pub type SessionParamsControllerRef = Arc<SessionParamsController>;
36
37const ENABLE_LOCALITY_BACKFILL_PARAM: &str = "enable_locality_backfill";
38const LOCALITY_BACKFILL_MODE_PARAM: &str = "locality_backfill_mode";
39
40/// Manages the global default session params on meta.
41/// Note that the session params in each session will be initialized from the default value here.
42pub struct SessionParamsController {
43    db: DatabaseConnection,
44    // Cached parameters.
45    params: RwLock<SessionConfig>,
46    notification_manager: NotificationManagerRef,
47}
48
49impl SessionParamsController {
50    pub async fn new(
51        sql_meta_store: SqlMetaStore,
52        notification_manager: NotificationManagerRef,
53        session_init: SessionInitConfig,
54    ) -> MetaResult<Self> {
55        let db = sql_meta_store.conn;
56
57        // Precedence (high to low):
58        // 1. Persisted value in Meta store (`session_parameter`)
59        // 2. Explicit value in `[session_init]`
60        // 3. Built-in `SessionConfig::default()`
61        let mut init_params = SessionConfig::default();
62
63        // Apply the explicitly-configured `[session_init]` values onto the built-in defaults.
64        // Record the normalized value of each so we can later detect when a persisted value
65        // takes precedence and warn the operator.
66        let mut session_init_values: HashMap<String, String> = HashMap::new();
67        for (name, value) in session_init.entries() {
68            let normalized = init_params.set(name, value.to_owned(), &mut ())?;
69            session_init_values.insert(name.to_owned(), normalized);
70        }
71        if !session_init_values.is_empty() {
72            info!(
73                "[session_init] seeds session parameters during cluster bootstrap only; \
74                 persisted values in the meta store take precedence on existing clusters"
75            );
76        }
77
78        // Persisted values take precedence over `[session_init]`.
79        let params = SessionParameter::find().all(&db).await?;
80        let has_locality_backfill_mode = params
81            .iter()
82            .any(|param| param.name == LOCALITY_BACKFILL_MODE_PARAM);
83        let has_legacy_locality_backfill = params
84            .iter()
85            .any(|param| param.name == ENABLE_LOCALITY_BACKFILL_PARAM);
86        for param in params {
87            if let Some(configured) = session_init_values.get(&param.name)
88                && *configured != param.value
89            {
90                tracing::warn!(
91                    parameter = %param.name,
92                    session_init_value = %configured,
93                    persisted_value = %param.value,
94                    "session_init value differs from persisted session parameter, using persisted value"
95                );
96            }
97            if let Err(e) = init_params.set(&param.name, param.value, &mut ()) {
98                match e {
99                    SessionConfigError::InvalidValue { .. } => {
100                        tracing::error!(error = %e.as_report(), "failed to set parameter from meta database, using default value {}", init_params.get(&param.name)?)
101                    }
102                    SessionConfigError::UnrecognizedEntry(_) => {
103                        tracing::error!(error = %e.as_report(), "failed to set parameter from meta database")
104                    }
105                }
106            }
107        }
108        // Before the mode existed, enabling locality backfill always skipped the size check.
109        if !has_locality_backfill_mode && has_legacy_locality_backfill {
110            init_params.set(LOCALITY_BACKFILL_MODE_PARAM, "always".to_owned(), &mut ())?;
111        }
112
113        info!(?init_params, "session parameters");
114
115        let ctl = Self {
116            db,
117            params: RwLock::new(init_params.clone()),
118            notification_manager,
119        };
120        // flush to db.
121        ctl.flush_params().await?;
122
123        Ok(ctl)
124    }
125
126    pub async fn get_params(&self) -> SessionConfig {
127        self.params.read().await.clone()
128    }
129
130    async fn flush_params(&self) -> MetaResult<()> {
131        let params = self.params.read().await.list_all();
132        let models = params
133            .into_iter()
134            .map(|param| session_parameter::ActiveModel {
135                name: Set(param.name),
136                value: Set(param.setting),
137                description: Set(Some(param.description)),
138            })
139            .collect_vec();
140        let txn = self.db.begin().await?;
141        // delete all params first and then insert all params. It follows the same logic
142        // as the old code, we'd better change it to another way later to keep consistency.
143        SessionParameter::delete_many().exec(&txn).await?;
144        SessionParameter::insert_many(models).exec(&txn).await?;
145        txn.commit().await?;
146        Ok(())
147    }
148
149    pub async fn set_param(&self, name: &str, value: Option<String>) -> MetaResult<String> {
150        let mut params_guard = self.params.write().await;
151        let name = SessionConfig::alias_to_entry_name(name);
152        let Some(param) = SessionParameter::find_by_id(name.clone())
153            .one(&self.db)
154            .await?
155        else {
156            return Err(MetaError::system_params(format!(
157                "unrecognized session parameter {:?}",
158                name
159            )));
160        };
161        let old_batch_parallelism = params_guard.batch_parallelism();
162        // FIXME: use a real reporter
163        let reporter = &mut ();
164        let new_param = if let Some(value) = value {
165            params_guard.set(&name, value, reporter)?
166        } else {
167            params_guard.reset(&name, reporter)?
168        };
169
170        let mut param: session_parameter::ActiveModel = param.into();
171        param.value = Set(new_param.clone());
172        SessionParameter::update(param).exec(&self.db).await?;
173        let new_batch_parallelism = params_guard.batch_parallelism();
174        self.notify_workers(name.clone(), new_param.clone());
175        if old_batch_parallelism != new_batch_parallelism {
176            self.notification_manager
177                .notify_local_subscribers(LocalNotification::BatchParallelismChange);
178        }
179
180        Ok(new_param)
181    }
182
183    pub fn notify_workers(&self, name: String, value: String) {
184        self.notification_manager.notify_frontend_without_version(
185            Operation::Update,
186            Info::SessionParam(SetSessionParamRequest {
187                param: name,
188                value: Some(value),
189            }),
190        );
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use sea_orm::ColumnTrait;
197
198    use super::*;
199    use crate::manager::MetaSrvEnv;
200
201    #[tokio::test]
202    async fn test_session_params() {
203        use sea_orm::QueryFilter;
204
205        let env = MetaSrvEnv::for_test().await;
206        let meta_store = env.meta_store_ref();
207        let init_params = SessionConfig::default();
208
209        // init system parameter controller as first launch.
210        let session_param_ctl = SessionParamsController::new(
211            meta_store.clone(),
212            env.notification_manager_ref(),
213            SessionInitConfig::default(),
214        )
215        .await
216        .unwrap();
217        let params = session_param_ctl.get_params().await;
218        assert_eq!(params, init_params);
219
220        // set parameter.
221        let new_params = session_param_ctl
222            .set_param("rw_implicit_flush", Some("true".into()))
223            .await
224            .unwrap();
225
226        // insert deprecated params.
227        let deprecated_param = session_parameter::ActiveModel {
228            name: Set("deprecated_param".into()),
229            value: Set("foo".into()),
230            description: Set(None),
231        };
232        SessionParameter::insert(deprecated_param)
233            .exec(&session_param_ctl.db)
234            .await
235            .unwrap();
236
237        // init system parameter controller as not first launch.
238        let session_param_ctl = SessionParamsController::new(
239            meta_store.clone(),
240            env.notification_manager_ref(),
241            SessionInitConfig::default(),
242        )
243        .await
244        .unwrap();
245        // check deprecated params are cleaned up.
246        assert!(
247            SessionParameter::find_by_id("deprecated_param".to_owned())
248                .one(&session_param_ctl.db)
249                .await
250                .unwrap()
251                .is_none()
252        );
253        // check new params are set.
254        let params = session_param_ctl.get_params().await;
255        assert_eq!(params.get("rw_implicit_flush").unwrap(), new_params);
256        assert_eq!(
257            params.get("rw_implicit_flush").unwrap(),
258            params.get("implicit_flush").unwrap()
259        );
260        // check db consistency.
261        // rw_implicit_flush is alias to implicit_flush <https://github.com/risingwavelabs/risingwave/pull/18769>
262        let models = SessionParameter::find()
263            .filter(session_parameter::Column::Name.eq("rw_implicit_flush"))
264            .one(&session_param_ctl.db)
265            .await
266            .unwrap();
267        assert!(models.is_none());
268        let models = SessionParameter::find()
269            .filter(session_parameter::Column::Name.eq("implicit_flush"))
270            .one(&session_param_ctl.db)
271            .await
272            .unwrap()
273            .unwrap();
274        assert_eq!(models.value, params.get("rw_implicit_flush").unwrap());
275    }
276
277    /// Scenario 1: on a new cluster, `[session_init]` seeds values into the meta store, including
278    /// the `default` placeholder which must be persisted verbatim.
279    #[tokio::test]
280    async fn test_session_init_bootstrap() {
281        let env = MetaSrvEnv::for_test().await;
282        let meta_store = env.meta_store_ref().clone();
283        // Simulate an empty store: drop the rows seeded while constructing the test env.
284        SessionParameter::delete_many()
285            .exec(&meta_store.conn)
286            .await
287            .unwrap();
288
289        let session_init = SessionInitConfig {
290            streaming_parallelism: Some("bounded(8)".to_owned()),
291            streaming_parallelism_for_table: Some("default".to_owned()),
292            ..Default::default()
293        };
294        let ctl = SessionParamsController::new(
295            meta_store.clone(),
296            env.notification_manager_ref(),
297            session_init,
298        )
299        .await
300        .unwrap();
301
302        let params = ctl.get_params().await;
303        assert_eq!(params.get("streaming_parallelism").unwrap(), "bounded(8)");
304        // `default` must be persisted as-is, not materialized into a concrete value.
305        assert_eq!(
306            params.get("streaming_parallelism_for_table").unwrap(),
307            "default"
308        );
309
310        // Values are persisted to the meta store.
311        let persisted = SessionParameter::find_by_id("streaming_parallelism".to_owned())
312            .one(&meta_store.conn)
313            .await
314            .unwrap()
315            .unwrap();
316        assert_eq!(persisted.value, "bounded(8)");
317    }
318
319    /// Scenario 2: on an existing cluster, a persisted value takes precedence over `[session_init]`.
320    #[tokio::test]
321    async fn test_session_init_persisted_takes_precedence() {
322        let env = MetaSrvEnv::for_test().await;
323        let meta_store = env.meta_store_ref().clone();
324
325        // First launch: seed the store and then mimic an `ALTER SYSTEM SET`.
326        let ctl = SessionParamsController::new(
327            meta_store.clone(),
328            env.notification_manager_ref(),
329            SessionInitConfig::default(),
330        )
331        .await
332        .unwrap();
333        ctl.set_param("streaming_parallelism", Some("ratio(0.5)".to_owned()))
334            .await
335            .unwrap();
336
337        // Restart with a conflicting `[session_init]`: the persisted value must win.
338        let session_init = SessionInitConfig {
339            streaming_parallelism: Some("bounded(8)".to_owned()),
340            ..Default::default()
341        };
342        let ctl = SessionParamsController::new(
343            meta_store.clone(),
344            env.notification_manager_ref(),
345            session_init,
346        )
347        .await
348        .unwrap();
349        let params = ctl.get_params().await;
350        assert_eq!(params.get("streaming_parallelism").unwrap(), "ratio(0.5)");
351    }
352
353    /// Scenario 3: a newly supported field missing from the persisted `session_parameter` table is
354    /// seeded from `[session_init]` on restart.
355    #[tokio::test]
356    async fn test_session_init_seeds_missing_field() {
357        let env = MetaSrvEnv::for_test().await;
358        let meta_store = env.meta_store_ref().clone();
359
360        // First launch seeds and persists defaults for all fields.
361        SessionParamsController::new(
362            meta_store.clone(),
363            env.notification_manager_ref(),
364            SessionInitConfig::default(),
365        )
366        .await
367        .unwrap();
368        // Simulate an older cluster that has no row for this field.
369        SessionParameter::delete_by_id("streaming_parallelism_for_materialized_view".to_owned())
370            .exec(&meta_store.conn)
371            .await
372            .unwrap();
373
374        let session_init = SessionInitConfig {
375            streaming_parallelism_for_materialized_view: Some("bounded(4)".to_owned()),
376            ..Default::default()
377        };
378        let ctl = SessionParamsController::new(
379            meta_store.clone(),
380            env.notification_manager_ref(),
381            session_init,
382        )
383        .await
384        .unwrap();
385        let params = ctl.get_params().await;
386        assert_eq!(
387            params
388                .get("streaming_parallelism_for_materialized_view")
389                .unwrap(),
390            "bounded(4)"
391        );
392    }
393
394    #[tokio::test]
395    async fn test_session_init_invalid_value_fails_without_persisting() {
396        let env = MetaSrvEnv::for_test().await;
397        let meta_store = env.meta_store_ref().clone();
398        // Simulate an empty store: drop the rows seeded while constructing the test env.
399        SessionParameter::delete_many()
400            .exec(&meta_store.conn)
401            .await
402            .unwrap();
403
404        let session_init = SessionInitConfig {
405            streaming_parallelism_for_backfill: Some("bounded(2)".to_owned()),
406            ..Default::default()
407        };
408        let err = match SessionParamsController::new(
409            meta_store.clone(),
410            env.notification_manager_ref(),
411            session_init,
412        )
413        .await
414        {
415            Ok(_) => panic!("invalid [session_init] should fail"),
416            Err(err) => err,
417        };
418        assert!(err
419            .to_string()
420            .contains("Session parameters error: Invalid value `bounded(2)` for `streaming_parallelism_for_backfill`"));
421
422        let persisted = SessionParameter::find()
423            .all(&meta_store.conn)
424            .await
425            .unwrap();
426        assert!(persisted.is_empty());
427    }
428}