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