Skip to main content

risingwave_common_proc_macro/
session_config.rs

1// Copyright 2023 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 bae::FromAttributes;
16use proc_macro_error::{OptionExt, ResultExt, abort};
17use proc_macro2::TokenStream;
18use quote::{format_ident, quote, quote_spanned};
19use syn::DeriveInput;
20
21#[derive(FromAttributes)]
22struct Parameter {
23    pub rename: Option<syn::LitStr>,
24    pub alias: Option<syn::Expr>,
25    pub default: syn::Expr,
26    pub flags: Option<syn::LitStr>,
27    pub check_hook: Option<syn::Expr>,
28    pub deprecated: Option<syn::LitStr>,
29}
30
31pub(crate) fn derive_config(input: DeriveInput) -> TokenStream {
32    let syn::Data::Struct(syn::DataStruct { fields, .. }) = input.data else {
33        abort!(input, "Only struct is supported");
34    };
35
36    let mut default_fields = vec![];
37    let mut struct_impl_set = vec![];
38    let mut struct_impl_get = vec![];
39    let mut struct_impl_reset = vec![];
40    let mut set_match_branches = vec![];
41    let mut get_match_branches = vec![];
42    let mut reset_match_branches = vec![];
43    let mut show_all_list = vec![];
44    let mut list_all_list = vec![];
45    let mut alias_to_entry_name_branches = vec![];
46    let mut entry_name_flags = vec![];
47    // Fields and entries for the generated `SessionInitConfig`, i.e. parameters flagged with
48    // `SESSION_INIT` that can be seeded from `[session_init]` in `risingwave.toml`.
49    let mut session_init_fields = vec![];
50    let mut session_init_entries = vec![];
51
52    for field in fields {
53        let field_ident = field.ident.expect_or_abort("Field need to be named");
54        let ty = field.ty;
55
56        let mut doc_list = vec![];
57        for attr in &field.attrs {
58            if attr.path.is_ident("doc") {
59                let meta = attr.parse_meta().expect_or_abort("Failed to parse meta");
60                if let syn::Meta::NameValue(val) = meta
61                    && let syn::Lit::Str(desc) = val.lit
62                {
63                    doc_list.push(desc.value().trim().to_owned());
64                }
65            }
66        }
67
68        let description: TokenStream = format!("r#\"{}\"#", doc_list.join(" ")).parse().unwrap();
69
70        let attr =
71            Parameter::from_attributes(&field.attrs).expect_or_abort("Failed to parse attribute");
72        let Parameter {
73            rename,
74            alias,
75            default,
76            flags,
77            check_hook: check_hook_name,
78            deprecated,
79        } = attr;
80        let deprecated_attr = deprecated
81            .as_ref()
82            .map(|notice| quote! { #[deprecated(note = #notice)] })
83            .unwrap_or_else(|| quote! {});
84
85        let entry_name = if let Some(rename) = rename {
86            if !(rename.value().is_ascii() && rename.value().to_ascii_lowercase() == rename.value())
87            {
88                abort!(rename, "Expect `rename` to be an ascii lower case string");
89            }
90            quote! {#rename}
91        } else {
92            let ident = format_ident!("{}", field_ident.to_string().to_lowercase());
93            quote! {stringify!(#ident)}
94        };
95
96        if let Some(alias) = alias {
97            alias_to_entry_name_branches.push(quote! {
98                #alias => #entry_name.to_string(),
99            })
100        }
101
102        let flags = flags.map(|f| f.value()).unwrap_or_default();
103        let flags: Vec<_> = flags.split('|').map(|str| str.trim()).collect();
104
105        default_fields.push(quote_spanned! {
106            field_ident.span()=>
107            #field_ident: #default.into(),
108        });
109
110        let set_func_name = format_ident!("set_{}_str", field_ident);
111        let set_t_func_name = format_ident!("set_{}", field_ident);
112        let set_t_inner_func_name = format_ident!("set_{}_inner", field_ident);
113        let set_t_func_doc: TokenStream =
114            format!("r#\"Set parameter {} by a typed value.\"#", entry_name)
115                .parse()
116                .unwrap();
117        let set_func_doc: TokenStream = format!("r#\"Set parameter {} by a string.\"#", entry_name)
118            .parse()
119            .unwrap();
120
121        let gen_set_func_name = if flags.contains(&"SETTER") {
122            set_t_inner_func_name.clone()
123        } else {
124            set_t_func_name.clone()
125        };
126
127        let check_hook = if let Some(check_hook_name) = check_hook_name {
128            quote! {
129                #check_hook_name(&val).map_err(|e| {
130                    SessionConfigError::InvalidValue {
131                        entry: #entry_name,
132                        value: val.to_string(),
133                        source: anyhow::anyhow!(e),
134                    }
135                })?;
136            }
137        } else {
138            quote! {}
139        };
140
141        let report_hook = if flags.contains(&"REPORT") {
142            quote! {
143                if self.#field_ident != val {
144                    reporter.report_status(#entry_name, val.to_string());
145                }
146            }
147        } else {
148            quote! {}
149        };
150
151        // An easy way to check if the type is bool and use a different parse function.
152        let parse = if quote!(#ty).to_string() == "bool" {
153            quote!(risingwave_common::cast::str_to_bool)
154        } else {
155            quote!(<#ty as ::std::str::FromStr>::from_str)
156        };
157
158        struct_impl_set.push(quote! {
159            #deprecated_attr
160            #[doc = #set_func_doc]
161            pub fn #set_func_name(
162                &mut self,
163                val: &str,
164                reporter: &mut impl ConfigReporter
165            ) -> SessionConfigResult<String> {
166                let val_t = #parse(val).map_err(|e| {
167                    SessionConfigError::InvalidValue {
168                        entry: #entry_name,
169                        value: val.to_string(),
170                        source: anyhow::anyhow!(e),
171                    }
172                })?;
173
174                self.#set_t_func_name(val_t, reporter).map(|val| val.to_string())
175            }
176
177            #deprecated_attr
178            #[doc = #set_t_func_doc]
179            pub fn #gen_set_func_name(
180                &mut self,
181                val: #ty,
182                reporter: &mut impl ConfigReporter
183            ) -> SessionConfigResult<#ty> {
184                #check_hook
185                #report_hook
186
187                self.#field_ident = val.clone();
188                Ok(val)
189            }
190
191        });
192
193        let reset_func_name = format_ident!("reset_{}", field_ident);
194        struct_impl_reset.push(quote! {
195
196        #deprecated_attr
197        #[allow(clippy::useless_conversion)]
198        pub fn #reset_func_name(&mut self, reporter: &mut impl ConfigReporter) -> String {
199                let val = #default;
200                #report_hook
201                self.#field_ident = val.into();
202                self.#field_ident.to_string()
203            }
204        });
205
206        let get_func_name = format_ident!("{}_str", field_ident);
207        let get_t_func_name = format_ident!("{}", field_ident);
208        let get_func_doc: TokenStream =
209            format!("r#\"Get a value string of parameter {} \"#", entry_name)
210                .parse()
211                .unwrap();
212        let get_t_func_doc: TokenStream =
213            format!("r#\"Get a typed value of parameter {} \"#", entry_name)
214                .parse()
215                .unwrap();
216
217        struct_impl_get.push(quote! {
218            #deprecated_attr
219            #[doc = #get_func_doc]
220            pub fn #get_func_name(&self) -> String {
221                self.#get_t_func_name().to_string()
222            }
223
224            #deprecated_attr
225            #[doc = #get_t_func_doc]
226            pub fn #get_t_func_name(&self) -> #ty {
227                self.#field_ident.clone()
228            }
229
230        });
231
232        get_match_branches.push(quote! {
233            #entry_name => Ok(self.#get_func_name()),
234        });
235
236        set_match_branches.push(quote! {
237            #entry_name => self.#set_func_name(&value, reporter),
238        });
239
240        reset_match_branches.push(quote! {
241            #entry_name => Ok(self.#reset_func_name(reporter)),
242        });
243
244        let var_info = quote! {
245            VariableInfo {
246                name: #entry_name.to_string(),
247                setting: self.#field_ident.to_string(),
248                description : #description.to_string(),
249            },
250        };
251        list_all_list.push(var_info.clone());
252
253        let no_show_all = flags.contains(&"NO_SHOW_ALL");
254        let no_show_all_flag: TokenStream = no_show_all.to_string().parse().unwrap();
255        if !no_show_all {
256            show_all_list.push(var_info);
257        }
258
259        let no_alter_sys_flag: TokenStream =
260            flags.contains(&"NO_ALTER_SYS").to_string().parse().unwrap();
261        let deprecated_notice = deprecated
262            .map(|notice| quote! { Some(#notice) })
263            .unwrap_or_else(|| quote! { None });
264
265        entry_name_flags.push(quote! {
266            (#entry_name, ParamFlags {
267                no_show_all: #no_show_all_flag,
268                no_alter_sys: #no_alter_sys_flag,
269                deprecated_notice: #deprecated_notice,
270            })
271        });
272
273        // Parameters flagged with `SESSION_INIT` become a field in the generated
274        // `SessionInitConfig`. The value is kept as a raw `Option<String>` so that a parameter
275        // omitted from `risingwave.toml` (`None`) can be distinguished from one explicitly set to
276        // its logical default such as `"default"` (`Some("default")`).
277        if flags.contains(&"SESSION_INIT") {
278            let doc_string = doc_list.join(" ");
279            session_init_fields.push(quote! {
280                #[doc = #doc_string]
281                #[serde(default, with = "crate::config::none_as_empty_string")]
282                pub #field_ident: Option<String>,
283            });
284            session_init_entries.push(quote! {
285                (#entry_name, &self.#field_ident),
286            });
287        }
288    }
289
290    let struct_ident = input.ident;
291    quote! {
292        /// The section `[session_init]` in `risingwave.toml`, generated from the `SESSION_INIT`-flagged
293        /// fields of [`SessionConfig`].
294        ///
295        /// It seeds the corresponding persisted session parameters into the meta store during
296        /// **cluster bootstrap only**. The precedence is:
297        ///
298        /// 1. Persisted value in the meta store (`session_parameter`)
299        /// 2. Explicit value in `[session_init]`
300        /// 3. Built-in `SessionConfig::default()`
301        ///
302        /// Editing `[session_init]` after a cluster has been bootstrapped does not change the
303        /// effective value of an already-persisted parameter. See the RFC for details.
304        #[derive(Clone, Debug, Default, Serialize, Deserialize, ConfigDoc, PartialEq)]
305        #[serde(deny_unknown_fields)]
306        pub struct SessionInitConfig {
307            #(#session_init_fields)*
308        }
309
310        impl SessionInitConfig {
311            /// Returns the explicitly-configured `(session parameter entry name, value)` pairs.
312            /// Parameters omitted from `[session_init]` are not included.
313            pub fn entries(&self) -> Vec<(&'static str, &str)> {
314                [
315                    #(#session_init_entries)*
316                ]
317                .into_iter()
318                .filter_map(|(name, value): (&'static str, &Option<String>)| {
319                    value.as_deref().map(|value| (name, value))
320                })
321                .collect()
322            }
323        }
324
325        use std::collections::HashMap;
326        use std::sync::LazyLock;
327        static PARAM_NAME_FLAGS: LazyLock<HashMap<&'static str, ParamFlags>> = LazyLock::new(|| HashMap::from([#(#entry_name_flags, )*]));
328
329        struct ParamFlags {
330            no_show_all: bool,
331            no_alter_sys: bool,
332            deprecated_notice: Option<&'static str>,
333        }
334
335        impl Default for #struct_ident {
336            #[allow(clippy::useless_conversion, deprecated)]
337            fn default() -> Self {
338                Self {
339                    #(#default_fields)*
340                }
341            }
342        }
343
344        #[allow(deprecated)]
345        impl #struct_ident {
346            fn new() -> Self {
347                Default::default()
348            }
349
350            pub fn alias_to_entry_name(key_name: &str) -> String {
351                let key_name = key_name.to_ascii_lowercase();
352                match key_name.as_str() {
353                    #(#alias_to_entry_name_branches)*
354                    _ => key_name,
355                }
356            }
357
358            #(#struct_impl_get)*
359
360            #(#struct_impl_set)*
361
362            #(#struct_impl_reset)*
363
364            /// Set a parameter given it's name and value string.
365            pub fn set(&mut self, key_name: &str, value: String, reporter: &mut impl ConfigReporter) -> SessionConfigResult<String> {
366                let key_name = Self::alias_to_entry_name(key_name);
367                match key_name.as_ref() {
368                    #(#set_match_branches)*
369                    _ => Err(SessionConfigError::UnrecognizedEntry(key_name.to_string())),
370                }
371            }
372
373            /// Get a parameter by it's name.
374            pub fn get(&self, key_name: &str) -> SessionConfigResult<String> {
375                let key_name = Self::alias_to_entry_name(key_name);
376                match key_name.as_ref() {
377                    #(#get_match_branches)*
378                    _ => Err(SessionConfigError::UnrecognizedEntry(key_name.to_string())),
379                }
380            }
381
382            /// Reset a parameter by it's name.
383            pub fn reset(&mut self, key_name: &str, reporter: &mut impl ConfigReporter) -> SessionConfigResult<String> {
384                let key_name = Self::alias_to_entry_name(key_name);
385                match key_name.as_ref() {
386                    #(#reset_match_branches)*
387                    _ => Err(SessionConfigError::UnrecognizedEntry(key_name.to_string())),
388                }
389            }
390
391            /// Show all parameters except those specified `NO_SHOW_ALL`.
392            pub fn show_all(&self) -> Vec<VariableInfo> {
393                vec![
394                    #(#show_all_list)*
395                ]
396            }
397
398            /// List all parameters
399            pub fn list_all(&self) -> Vec<VariableInfo> {
400                vec![
401                    #(#list_all_list)*
402                ]
403            }
404
405            /// Check if `SessionConfig` has a parameter.
406            pub fn contains_param(key_name: &str) -> bool {
407                let key_name = Self::alias_to_entry_name(key_name);
408                PARAM_NAME_FLAGS.contains_key(key_name.as_str())
409            }
410
411            /// Check if `SessionConfig` has a parameter.
412            pub fn check_no_alter_sys(key_name: &str) -> SessionConfigResult<bool> {
413                let key_name = Self::alias_to_entry_name(key_name);
414                let flags = PARAM_NAME_FLAGS.get(key_name.as_str()).ok_or_else(|| SessionConfigError::UnrecognizedEntry(key_name.to_string()))?;
415                Ok(flags.no_alter_sys)
416            }
417
418            /// Returns a user-facing notice for deprecated parameters.
419            pub fn deprecated_notice(key_name: &str) -> SessionConfigResult<Option<&'static str>> {
420                let key_name = Self::alias_to_entry_name(key_name);
421                let flags = PARAM_NAME_FLAGS.get(key_name.as_str()).ok_or_else(|| SessionConfigError::UnrecognizedEntry(key_name.to_string()))?;
422                Ok(flags.deprecated_notice)
423            }
424        }
425    }
426}