risingwave_meta/controller/
session_params.rs1use 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
40pub struct SessionParamsController {
43 db: DatabaseConnection,
44 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 let mut init_params = SessionConfig::default();
62
63 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 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(¶m.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(¶m.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(¶m.name)?)
101 }
102 SessionConfigError::UnrecognizedEntry(_) => {
103 tracing::error!(error = %e.as_report(), "failed to set parameter from meta database")
104 }
105 }
106 }
107 }
108 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 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 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 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 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 let new_params = session_param_ctl
222 .set_param("rw_implicit_flush", Some("true".into()))
223 .await
224 .unwrap();
225
226 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 let session_param_ctl = SessionParamsController::new(
239 meta_store.clone(),
240 env.notification_manager_ref(),
241 SessionInitConfig::default(),
242 )
243 .await
244 .unwrap();
245 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 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 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 #[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 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 assert_eq!(
306 params.get("streaming_parallelism_for_table").unwrap(),
307 "default"
308 );
309
310 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 #[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 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 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 #[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 SessionParamsController::new(
362 meta_store.clone(),
363 env.notification_manager_ref(),
364 SessionInitConfig::default(),
365 )
366 .await
367 .unwrap();
368 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 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}