common_options/plugin_options.rs
1// Copyright 2023 Greptime Team
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::sync::{Arc, Mutex};
16
17use serde::de::DeserializeOwned;
18use serde_json::Value;
19
20/// A trait for serializing Metasrv config to a JSON string.
21/// So it can be used in the metasrv's crate instead of depending on the plugins' crate.
22pub trait PluginOptionsSerializer: Send + Sync {
23 fn serialize(&self) -> Result<String, serde_json::Error>;
24}
25pub type PluginOptionsSerializerRef = Arc<dyn PluginOptionsSerializer>;
26
27/// A trait for deserializing Metasrv config from a JSON string.
28pub trait PluginOptionsDeserializer<T: DeserializeOwned>: Send + Sync {
29 fn deserialize(&self, payload: &str) -> Result<T, serde_json::Error>;
30}
31
32/// A flag for stating the standalone mode in the plugins.
33///
34/// The standalone build and start process calls `setup_frontend_plugins_pre_build` and `setup_datanode_plugins_pre_build`,
35/// so we add a flag to the plugins to indicate that the plugins are running in the standalone mode.
36#[derive(Clone, Copy, Debug)]
37pub struct StandaloneFlag;
38
39/// Buffer of plugin option tags that were dropped during config loading because
40/// they are not recognized by the current build.
41///
42/// Config loading typically happens *before* the global tracing subscriber is
43/// installed, so a `warn!` emitted at that point is silently lost. Instead we
44/// buffer the dropped tags here and let the server flush them (via
45/// [`take_dropped_plugin_warnings`]) once logging is initialized, so the warning
46/// actually reaches the configured log output.
47static DROPPED_PLUGIN_TAGS: Mutex<Vec<String>> = Mutex::new(Vec::new());
48
49/// Records a plugin option tag that was dropped because it is not recognized by
50/// this build (for example, an enterprise plugin option seen by an open-source
51/// build).
52pub fn record_dropped_plugin(tag: &str) {
53 if let Ok(mut tags) = DROPPED_PLUGIN_TAGS.lock() {
54 tags.push(tag.to_string());
55 }
56}
57
58/// Takes (and clears) the buffered dropped-plugin tags so they can be logged
59/// after the global logging is up.
60pub fn take_dropped_plugin_warnings() -> Vec<String> {
61 DROPPED_PLUGIN_TAGS
62 .lock()
63 .map(|mut tags| std::mem::take(&mut *tags))
64 .unwrap_or_default()
65}
66
67/// Deserializes a list of plugin options leniently.
68///
69/// Each entry is normally a single-key `{"tag": payload}` object, but a
70/// *multi-key* object is also accepted and split: every top-level key is
71/// treated as an independent plugin option. Keys naming a *known* variant are
72/// deserialized and kept; keys naming a variant this build doesn't recognize are
73/// dropped (and buffered via [`record_dropped_plugin`] for deferred logging).
74/// This lets a single config file be shared across builds that compile different
75/// sets of plugins — including a table that mixes a known plugin with unknown
76/// ones — without aborting startup.
77///
78/// A *known* variant whose payload is genuinely malformed still propagates its
79/// error, so a misconfigured plugin is never silently disabled; only genuinely
80/// unknown options are ignored. For privacy only the unrecognized variant *tag*
81/// is recorded, never the raw payload (which may contain secrets).
82///
83/// This is generic over `T` so the open-source `plugins` crate and the
84/// enterprise `ent-plugins` crate share a single implementation without either
85/// having to enumerate its variants (see [`is_unknown_variant_tag`]).
86pub fn filter_known_plugin_options<T: DeserializeOwned>(
87 values: Vec<Value>,
88) -> Result<Vec<T>, serde_json::Error> {
89 let mut out = Vec::with_capacity(values.len());
90 for value in values {
91 collect_from_entry::<T>(&value, &mut out)?;
92 }
93 Ok(out)
94}
95
96/// Extracts the recognized plugin options from one config entry.
97///
98/// A normal entry is a single-key `{"tag": payload}` object. We additionally
99/// accept a *multi-key* object and treat every top-level key as an independent
100/// plugin option: each key naming a *known* variant is deserialized (and a
101/// malformed payload for a known variant still errors, so a misconfigured plugin
102/// is never silently disabled), while keys naming unknown variants are dropped
103/// with a warning. A non-object entry (e.g. a bare number) is deserialized
104/// directly and surfaces any error.
105fn collect_from_entry<T: DeserializeOwned>(
106 value: &Value,
107 out: &mut Vec<T>,
108) -> Result<(), serde_json::Error> {
109 let Some(map) = value.as_object() else {
110 // Not a tagged object (e.g. a bare number); a bare string can still
111 // deserialize a unit variant. Surface any error.
112 out.push(T::deserialize(value)?);
113 return Ok(());
114 };
115 for (tag, payload) in map {
116 if is_unknown_variant_tag::<T>(tag) {
117 record_dropped_plugin(tag);
118 continue;
119 }
120 // Known variant: deserialize just `{"tag": payload}` so a malformed
121 // payload is reported against this variant rather than swallowed.
122 let mut single = serde_json::Map::new();
123 single.insert(tag.clone(), payload.clone());
124 out.push(T::deserialize(&Value::Object(single))?);
125 }
126 Ok(())
127}
128
129/// Decides whether `tag` names a variant unknown to `T`, *without* `T` having
130/// to enumerate its variants.
131///
132/// Two probe payloads (`{"tag": null}` and `{"tag": true}`) are re-deserialized
133/// for the same tag:
134/// - If `T` accepts either payload, the tag is recognized (a known variant).
135/// - If both are rejected, the tag is unknown iff the two errors are identical.
136/// An unknown variant yields a payload-independent "unknown variant" error for
137/// both probes, whereas a known variant rejects `null` and `bool` with
138/// *different* "invalid type" messages.
139fn is_unknown_variant_tag<T: DeserializeOwned>(tag: &str) -> bool {
140 let probe = |payload: Value| {
141 let mut map = serde_json::Map::new();
142 map.insert(tag.to_string(), payload);
143 Value::Object(map)
144 };
145 match (
146 T::deserialize(&probe(Value::Null)),
147 T::deserialize(&probe(Value::Bool(true))),
148 ) {
149 (Ok(_), _) | (_, Ok(_)) => false,
150 (Err(a), Err(b)) => a.to_string() == b.to_string(),
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use std::sync::{Mutex, OnceLock};
157
158 use serde::Deserialize;
159
160 use super::*;
161
162 // The dropped-tag buffer is process-global, so every test in this module
163 // serializes through this lock to keep buffer assertions deterministic.
164 fn lock() -> std::sync::MutexGuard<'static, ()> {
165 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
166 LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
167 }
168
169 #[derive(Debug, PartialEq, Deserialize)]
170 struct UnitPayload;
171
172 /// A stand-in for a real `PluginOptions` enum covering the variant shapes
173 /// that occur in practice (newtype-over-unit, struct, newtype-over-scalar).
174 #[derive(Debug, PartialEq, Deserialize)]
175 enum DummyPlugins {
176 Unit(UnitPayload),
177 Struct { x: u32 },
178 Newtype(String),
179 }
180
181 #[test]
182 fn detects_unknown_vs_known_tags() {
183 let _g = lock();
184 // `is_unknown_variant_tag` never touches the dropped-tag buffer.
185 for tag in ["Bogus", "DoesNotExist", "NotARealVariant"] {
186 assert!(
187 is_unknown_variant_tag::<DummyPlugins>(tag),
188 "{tag} should be unknown"
189 );
190 }
191 for tag in ["Unit", "Struct", "Newtype"] {
192 assert!(
193 !is_unknown_variant_tag::<DummyPlugins>(tag),
194 "{tag} should be known"
195 );
196 }
197 }
198
199 #[test]
200 fn filter_drops_unknown_keeps_known() {
201 let _g = lock();
202 let _ = take_dropped_plugin_warnings();
203 let values: Vec<Value> = serde_json::from_str(
204 r#"[
205 {"Unit": null},
206 {"Bogus": {"a": 1}},
207 {"AnotherUnknown": {}},
208 {"Struct": {"x": 7}},
209 {"Newtype": "hi"}
210 ]"#,
211 )
212 .unwrap();
213 let kept = filter_known_plugin_options::<DummyPlugins>(values).unwrap();
214 assert_eq!(
215 kept,
216 vec![
217 DummyPlugins::Unit(UnitPayload),
218 DummyPlugins::Struct { x: 7 },
219 DummyPlugins::Newtype("hi".to_string()),
220 ]
221 );
222 }
223
224 #[test]
225 fn filter_errors_on_malformed_known_variant() {
226 let _g = lock();
227 // struct/newtype variants cannot come from these payloads, so a *known*
228 // variant's malformed payload must error (not be silently dropped).
229 for bad in [
230 r#"[{"Struct": 5}]"#,
231 r#"[{"Struct": null}]"#,
232 r#"[{"Struct": "oops"}]"#,
233 r#"[{"Newtype": 7}]"#,
234 ] {
235 let values: Vec<Value> = serde_json::from_str(bad).unwrap();
236 assert!(
237 filter_known_plugin_options::<DummyPlugins>(values).is_err(),
238 "expected error for {bad}"
239 );
240 }
241 }
242
243 #[test]
244 fn filter_errors_on_non_tagged_shape() {
245 let _g = lock();
246 let values: Vec<Value> = serde_json::from_str(r#"[123]"#).unwrap();
247 assert!(filter_known_plugin_options::<DummyPlugins>(values).is_err());
248 }
249
250 #[test]
251 fn filter_drops_multi_key_all_unknown_entry() {
252 let _g = lock();
253 let _ = take_dropped_plugin_warnings();
254 // A single entry carrying several *unknown* keys must be dropped, not
255 // error with serde's "expected map with a single key".
256 let values: Vec<Value> =
257 serde_json::from_str(r#"[{"unknown_one": 1, "unknown_two": 2}]"#).unwrap();
258 let kept = filter_known_plugin_options::<DummyPlugins>(values).unwrap();
259 assert!(kept.is_empty());
260 let mut recorded = take_dropped_plugin_warnings();
261 recorded.sort();
262 assert_eq!(
263 recorded,
264 vec!["unknown_one".to_string(), "unknown_two".to_string()]
265 );
266 }
267
268 #[test]
269 fn filter_keeps_known_drops_unknown_in_mixed_entry() {
270 let _g = lock();
271 let _ = take_dropped_plugin_warnings();
272 // A table mixing a *known* plugin with unknown ones keeps the known
273 // plugin and drops (warns) the unknown ones — it must not error.
274 let values: Vec<Value> =
275 serde_json::from_str(r#"[{"Struct": {"x": 1}, "bogus": 2}]"#).unwrap();
276 let kept = filter_known_plugin_options::<DummyPlugins>(values).unwrap();
277 assert_eq!(kept, vec![DummyPlugins::Struct { x: 1 }]);
278 assert_eq!(take_dropped_plugin_warnings(), vec!["bogus".to_string()]);
279 }
280
281 #[test]
282 fn filter_errors_on_malformed_known_in_mixed_entry() {
283 let _g = lock();
284 let _ = take_dropped_plugin_warnings();
285 // A *known* variant with a genuinely malformed payload still errors,
286 // even when an unknown key sits next to it in the same table.
287 let values: Vec<Value> = serde_json::from_str(r#"[{"Struct": 5, "bogus": 2}]"#).unwrap();
288 assert!(filter_known_plugin_options::<DummyPlugins>(values).is_err());
289 // The malformed known variant aborted before the unknown was recorded.
290 assert!(take_dropped_plugin_warnings().is_empty());
291 }
292
293 #[test]
294 fn dropped_tags_are_buffered_with_tag_only() {
295 let _g = lock();
296 let _ = take_dropped_plugin_warnings();
297 // Privacy: only the tag is recorded, never the raw payload.
298 let values: Vec<Value> =
299 serde_json::from_str(r#"[{"SecretPlugin": {"token": "hunter2"}}]"#).unwrap();
300 filter_known_plugin_options::<DummyPlugins>(values).unwrap();
301 assert_eq!(
302 take_dropped_plugin_warnings(),
303 vec!["SecretPlugin".to_string()]
304 );
305 }
306}