kumo_server_common/
lib.rs

1use bstr::ByteSlice;
2use config::{
3    any_err, decorate_callback_name, from_lua_value, get_or_create_module, load_config,
4    serialize_options, CallbackSignature,
5};
6use kumo_server_runtime::available_parallelism;
7use mlua::{Function, Lua, LuaSerdeExt, Value, Variadic};
8use mod_redis::RedisConnKey;
9use serde::{Deserialize, Serialize};
10use std::sync::atomic::AtomicUsize;
11
12pub mod acct;
13pub mod authn_authz;
14pub mod config_handle;
15pub mod diagnostic_logging;
16pub mod disk_space;
17pub mod hashable_weak;
18pub mod http_server;
19pub mod log;
20pub mod log_backlog;
21pub mod nodeid;
22pub mod panic;
23pub mod start;
24pub mod tls_helpers;
25
26pub fn register(lua: &Lua) -> anyhow::Result<()> {
27    for func in [
28        mod_redis::register,
29        data_loader::register,
30        mod_digest::register,
31        mod_encode::register,
32        mod_aws_sigv4::register,
33        cidr_map::register,
34        domain_map::register,
35        mod_amqp::register,
36        mod_filesystem::register,
37        mod_file_type::register,
38        mod_http::register,
39        mod_regex::register,
40        mod_serde::register,
41        mod_sqlite::register,
42        mod_crypto::register,
43        mod_smtp_response_normalize::register,
44        kumo_jsonl::lua::register,
45        mod_counter_series::register,
46        mod_string::register,
47        mod_time::register,
48        mod_dns_resolver::register,
49        mod_kafka::register,
50        mod_memoize::register,
51        mod_mimepart::register,
52        mod_mpsc::register,
53        mod_nats::register,
54        mod_uuid::register,
55        kumo_api_types::shaping::register,
56        regex_set_map::register,
57        crate::authn_authz::register,
58    ] {
59        func(lua)?;
60    }
61
62    let kumo_mod = get_or_create_module(lua, "kumo")?;
63    kumo_mod.set("version", version_info::kumo_version())?;
64
65    fn event_registrar_name(name: &str) -> String {
66        format!("kumomta-event-registrars-{name}")
67    }
68
69    // Record the call stack of the code calling kumo.on so that
70    // kumo.get_event_registrars can retrieve it later
71    fn register_event_caller(lua: &Lua, name: &str) -> mlua::Result<()> {
72        let decorated_name = event_registrar_name(name);
73        let mut call_stack = vec![];
74        for n in 1.. {
75            match lua.inspect_stack(n, |info| {
76                let source = info.source();
77                format!(
78                    "{}:{}",
79                    source
80                        .short_src
81                        .as_ref()
82                        .map(|b| b.to_string())
83                        .unwrap_or_else(String::new),
84                    info.current_line().unwrap_or(0)
85                )
86            }) {
87                Some(info) => {
88                    call_stack.push(info);
89                }
90                None => break,
91            }
92        }
93
94        let tbl: Value = lua.named_registry_value(&decorated_name)?;
95        match tbl {
96            Value::Nil => {
97                let tbl = lua.create_table()?;
98                tbl.set(1, call_stack)?;
99                lua.set_named_registry_value(&decorated_name, tbl)?;
100                Ok(())
101            }
102            Value::Table(tbl) => {
103                let len = tbl.raw_len();
104                tbl.set(len + 1, call_stack)?;
105                Ok(())
106            }
107            _ => Err(mlua::Error::external(format!(
108                "registry key for {decorated_name} has invalid type",
109            ))),
110        }
111    }
112
113    // Returns the list of call-stacks of the code that registered
114    // for a specific named event
115    kumo_mod.set(
116        "get_event_registrars",
117        lua.create_function(move |lua, name: String| {
118            let decorated_name = event_registrar_name(&name);
119            let value: Value = lua.named_registry_value(&decorated_name)?;
120            Ok(value)
121        })?,
122    )?;
123
124    kumo_mod.set(
125        "on",
126        lua.create_function(move |lua, (name, func): (String, Function)| {
127            let decorated_name = decorate_callback_name(&name);
128
129            if let Ok(current_event) = lua.globals().get::<String>("_KUMO_CURRENT_EVENT") {
130                if current_event != "main" {
131                    return Err(mlua::Error::external(format!(
132                        "Attempting to register an event handler via \
133                    `kumo.on('{name}', ...)` from within the event handler \
134                    '{current_event}'. You must move your event handler registration \
135                    so that it is setup directly when the policy is loaded \
136                    in order for it to consistently trigger and handle events."
137                    )));
138                }
139            }
140
141            register_event_caller(lua, &name)?;
142
143            if config::does_callback_allow_multiple(&name) {
144                let tbl: Value = lua.named_registry_value(&decorated_name)?;
145                return match tbl {
146                    Value::Nil => {
147                        let tbl = lua.create_table()?;
148                        tbl.set(1, func)?;
149                        lua.set_named_registry_value(&decorated_name, tbl)?;
150                        Ok(())
151                    }
152                    Value::Table(tbl) => {
153                        let len = tbl.raw_len();
154                        tbl.set(len + 1, func)?;
155                        Ok(())
156                    }
157                    _ => Err(mlua::Error::external(format!(
158                        "registry key for {decorated_name} has invalid type",
159                    ))),
160                };
161            }
162
163            let existing: Value = lua.named_registry_value(&decorated_name)?;
164            match existing {
165                Value::Nil => {}
166                Value::Function(func) => {
167                    let info = func.info();
168                    let src = info.source.unwrap_or_else(|| "?".into());
169                    let line = info.line_defined.unwrap_or(0);
170                    return Err(mlua::Error::external(format!(
171                        "{name} event already has a handler defined at {src}:{line}"
172                    )));
173                }
174                _ => {
175                    return Err(mlua::Error::external(format!(
176                        "{name} event already has a handler"
177                    )));
178                }
179            }
180
181            lua.set_named_registry_value(&decorated_name, func)?;
182            Ok(())
183        })?,
184    )?;
185
186    kumo_mod.set(
187        "set_diagnostic_log_filter",
188        lua.create_function(move |_, filter: String| {
189            diagnostic_logging::set_diagnostic_log_filter(&filter).map_err(any_err)
190        })?,
191    )?;
192
193    fn variadic_to_string(args: Variadic<Value>) -> String {
194        let mut output = String::new();
195        for (idx, item) in args.into_iter().enumerate() {
196            if idx > 0 {
197                output.push(' ');
198            }
199
200            match item {
201                Value::String(s) => {
202                    let bytes = s.as_bytes();
203                    for (start, end, c) in bytes.char_indices() {
204                        if c == std::char::REPLACEMENT_CHARACTER {
205                            let c_slice = &bytes[start..end];
206                            for &b in c_slice.iter() {
207                                output.push_str(&format!("\\x{b:02X}"));
208                            }
209                        } else {
210                            output.push(c);
211                        }
212                    }
213                }
214                item => match item.to_string() {
215                    Ok(s) => output.push_str(&s),
216                    Err(_) => output.push_str(&format!("{item:?}")),
217                },
218            }
219        }
220        output
221    }
222
223    fn get_caller(lua: &Lua) -> String {
224        match lua.inspect_stack(1, |info| {
225            let source = info.source();
226            let file_name = source
227                .short_src
228                .as_ref()
229                .map(|b| b.to_string())
230                .unwrap_or_else(String::new);
231            // Lua returns the somewhat obnoxious `[string "source.lua"]`
232            // Let's fix that up to be a bit nicer
233            let file_name = match file_name.strip_prefix("[string \"") {
234                Some(name) => name.strip_suffix("\"]").unwrap_or(name),
235                None => &file_name,
236            };
237
238            format!("{file_name}:{}", info.current_line().unwrap_or(0))
239        }) {
240            Some(info) => info,
241            None => "?".to_string(),
242        }
243    }
244
245    kumo_mod.set(
246        "log_error",
247        lua.create_function(move |lua, args: Variadic<Value>| {
248            if tracing::event_enabled!(target: "lua", tracing::Level::ERROR) {
249                let src = get_caller(lua);
250                tracing::error!(target: "lua", "{src}: {}", variadic_to_string(args));
251            }
252            Ok(())
253        })?,
254    )?;
255    kumo_mod.set(
256        "log_info",
257        lua.create_function(move |lua, args: Variadic<Value>| {
258            if tracing::event_enabled!(target: "lua", tracing::Level::INFO) {
259                let src = get_caller(lua);
260                tracing::info!(target: "lua", "{src}: {}", variadic_to_string(args));
261            }
262            Ok(())
263        })?,
264    )?;
265    kumo_mod.set(
266        "log_warn",
267        lua.create_function(move |lua, args: Variadic<Value>| {
268            if tracing::event_enabled!(target: "lua", tracing::Level::WARN) {
269                let src = get_caller(lua);
270                tracing::warn!(target: "lua", "{src}: {}", variadic_to_string(args));
271            }
272            Ok(())
273        })?,
274    )?;
275    kumo_mod.set(
276        "log_debug",
277        lua.create_function(move |lua, args: Variadic<Value>| {
278            if tracing::event_enabled!(target: "lua", tracing::Level::DEBUG) {
279                let src = get_caller(lua);
280                tracing::debug!(target: "lua", "{src}: {}", variadic_to_string(args));
281            }
282            Ok(())
283        })?,
284    )?;
285
286    kumo_mod.set(
287        "set_max_spare_lua_contexts",
288        lua.create_function(move |_, limit: usize| {
289            config::set_max_spare(limit);
290            Ok(())
291        })?,
292    )?;
293
294    kumo_mod.set(
295        "set_max_lua_context_use_count",
296        lua.create_function(move |_, limit: usize| {
297            config::set_max_use(limit);
298            Ok(())
299        })?,
300    )?;
301
302    kumo_mod.set(
303        "set_max_lua_context_age",
304        lua.create_function(move |_, limit: usize| {
305            config::set_max_age(limit);
306            Ok(())
307        })?,
308    )?;
309
310    kumo_mod.set(
311        "set_lua_gc_on_put",
312        lua.create_function(move |_, enable: u8| {
313            config::set_gc_on_put(enable);
314            Ok(())
315        })?,
316    )?;
317
318    kumo_mod.set(
319        "set_lruttl_cache_capacity",
320        lua.create_function(move |_, (name, capacity): (String, usize)| {
321            if lruttl::set_cache_capacity(&name, capacity) {
322                Ok(())
323            } else {
324                Err(mlua::Error::external(format!(
325                    "could not set capacity for cache {name} \
326                    as that is not a pre-defined lruttl cache name"
327                )))
328            }
329        })?,
330    )?;
331
332    kumo_mod.set(
333        "set_config_monitor_globs",
334        lua.create_function(move |_, globs: Vec<String>| {
335            config::epoch::set_globs(globs).map_err(any_err)?;
336            Ok(())
337        })?,
338    )?;
339    kumo_mod.set(
340        "eval_config_monitor_globs",
341        lua.create_async_function(|_, _: ()| async move {
342            config::epoch::eval_globs().await.map_err(any_err)
343        })?,
344    )?;
345    kumo_mod.set(
346        "bump_config_epoch",
347        lua.create_function(move |_, _: ()| {
348            config::epoch::bump_current_epoch();
349            Ok(())
350        })?,
351    )?;
352
353    kumo_mod.set(
354        "available_parallelism",
355        lua.create_function(move |_, _: ()| available_parallelism().map_err(any_err))?,
356    )?;
357
358    kumo_mod.set(
359        "set_memory_hard_limit",
360        lua.create_function(move |_, limit: usize| {
361            kumo_server_memory::set_hard_limit(limit);
362            Ok(())
363        })?,
364    )?;
365
366    kumo_mod.set(
367        "set_memory_low_thresh",
368        lua.create_function(move |_, limit: usize| {
369            kumo_server_memory::set_low_memory_thresh(limit);
370            Ok(())
371        })?,
372    )?;
373
374    kumo_mod.set(
375        "set_memory_soft_limit",
376        lua.create_function(move |_, limit: usize| {
377            kumo_server_memory::set_soft_limit(limit);
378            Ok(())
379        })?,
380    )?;
381
382    kumo_mod.set(
383        "get_memory_hard_limit",
384        lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_hard_limit()))?,
385    )?;
386
387    kumo_mod.set(
388        "get_memory_soft_limit",
389        lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_soft_limit()))?,
390    )?;
391
392    kumo_mod.set(
393        "get_memory_low_thresh",
394        lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_low_memory_thresh()))?,
395    )?;
396
397    kumo_mod.set(
398        "configure_redis_throttles",
399        lua.create_async_function(|lua, params: Value| async move {
400            let key: RedisConnKey = from_lua_value(&lua, params)?;
401            let conn = key.open().map_err(any_err)?;
402            conn.ping().await.map_err(any_err)?;
403            throttle::use_redis(conn).await.map_err(any_err)
404        })?,
405    )?;
406
407    kumo_mod.set(
408        "traceback",
409        lua.create_function(move |lua: &Lua, level: usize| {
410            #[derive(Debug, Serialize)]
411            struct Frame {
412                event: String,
413                name: Option<String>,
414                name_what: Option<String>,
415                source: Option<String>,
416                short_src: Option<String>,
417                line_defined: Option<usize>,
418                last_line_defined: Option<usize>,
419                what: &'static str,
420                curr_line: Option<usize>,
421                is_tail_call: bool,
422            }
423
424            let mut frames = vec![];
425            for n in level.. {
426                match lua.inspect_stack(n, |info| {
427                    let source = info.source();
428                    let names = info.names();
429                    Frame {
430                        curr_line: info.current_line(),
431                        is_tail_call: info.is_tail_call(),
432                        event: format!("{:?}", info.event()),
433                        last_line_defined: source.last_line_defined,
434                        line_defined: source.line_defined,
435                        name: names.name.as_ref().map(|b| b.to_string()),
436                        name_what: names.name_what.as_ref().map(|b| b.to_string()),
437                        source: source.source.as_ref().map(|b| b.to_string()),
438                        short_src: source.short_src.as_ref().map(|b| b.to_string()),
439                        what: source.what,
440                    }
441                }) {
442                    Some(frame) => {
443                        frames.push(frame);
444                    }
445                    None => break,
446                }
447            }
448
449            lua.to_value(&frames)
450        })?,
451    )?;
452
453    // TODO: options like restarting on error, delay between
454    // restarts and so on
455    #[derive(Deserialize, Debug)]
456    struct TaskParams {
457        event_name: String,
458        args: Vec<serde_json::Value>,
459    }
460
461    impl TaskParams {
462        async fn run(&self) -> anyhow::Result<()> {
463            let mut config = load_config().await?;
464
465            let sig = CallbackSignature::<Value, ()>::new(self.event_name.to_string());
466
467            config
468                .convert_args_and_call_callback(&sig, &self.args)
469                .await?;
470
471            config.put();
472
473            Ok(())
474        }
475    }
476
477    kumo_mod.set(
478        "spawn_task",
479        lua.create_function(|lua, params: Value| {
480            let params: TaskParams = lua.from_value(params)?;
481
482            if !config::is_validating() {
483                std::thread::Builder::new()
484                    .name(format!("spawned-task-{}", params.event_name))
485                    .spawn(move || {
486                        let runtime = tokio::runtime::Builder::new_current_thread()
487                            .enable_io()
488                            .enable_time()
489                            .on_thread_park(kumo_server_memory::purge_thread_cache)
490                            .build()
491                            .unwrap();
492                        let event_name = params.event_name.clone();
493
494                        let result = runtime.block_on(async move { params.run().await });
495                        if let Err(err) = result {
496                            tracing::error!("Error while dispatching {event_name}: {err:#}");
497                        }
498                    })?;
499            }
500
501            Ok(())
502        })?,
503    )?;
504
505    kumo_mod.set(
506        "spawn_thread_pool",
507        lua.create_function(|lua, params: Value| {
508            #[derive(Deserialize, Debug)]
509            struct ThreadPoolParams {
510                name: String,
511                num_threads: usize,
512            }
513
514            let params: ThreadPoolParams = lua.from_value(params)?;
515            let num_threads = AtomicUsize::new(params.num_threads);
516
517            if !config::is_validating() {
518                // Create the runtime. We don't need to hold on
519                // to it here, as it will be kept alive in the
520                // runtimes map in that crate
521                let _runtime = kumo_server_runtime::Runtime::new(
522                    &params.name,
523                    |_| params.num_threads,
524                    &num_threads,
525                )
526                .map_err(any_err)?;
527            }
528
529            Ok(())
530        })?,
531    )?;
532
533    kumo_mod.set(
534        "validation_failed",
535        lua.create_function(|_, ()| {
536            config::set_validation_failed();
537            Ok(())
538        })?,
539    )?;
540
541    kumo_mod.set(
542        "enable_memory_callstack_tracking",
543        lua.create_function(|_, enable: bool| {
544            kumo_server_memory::set_tracking_callstacks(enable);
545            Ok(())
546        })?,
547    )?;
548
549    // This function is intended for debugging and testing purposes only.
550    // It is potentially very expensive on a production system with many
551    // thousands of queues.
552    kumo_mod.set(
553        "prometheus_metrics",
554        lua.create_async_function(|lua, ()| async move {
555            use tokio_stream::StreamExt;
556            let mut json_text = String::new();
557            let mut stream = kumo_prometheus::registry::Registry::stream_json();
558            while let Some(text) = stream.next().await {
559                json_text.push_str(&text);
560            }
561            let value: serde_json::Value = serde_json::from_str(&json_text).map_err(any_err)?;
562            lua.to_value_with(&value, serialize_options())
563        })?,
564    )?;
565
566    Ok(())
567}