mod_time/
lib.rs

1use chrono::format::StrftimeItems;
2use chrono::{DateTime, Datelike, LocalResult, TimeZone, Timelike, Utc};
3use config::{any_err, get_or_create_module, get_or_create_sub_module};
4use humantime::format_duration;
5use kumo_prometheus::declare_metric;
6use kumo_prometheus::prometheus::HistogramTimer;
7use mlua::{
8    FromLua, IntoLua, Lua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef,
9};
10use tokio::time::Duration;
11
12declare_metric! {
13/// How many seconds something user-defined took to run in your lua policy.
14///
15/// This histogram is updated by policy scripts that employ the
16/// [kumo.time.start_timer](../../kumo.time/start_timer.md) function
17/// to record a duration in the policy.
18///
19/// The `label` is whatever you specified as the label(s) to the various
20/// `kumo.time.start_timer` calls in the policy.
21static LATENCY_HIST: HistogramVec(
22    "user_lua_latency",
23    &["label"]
24);
25}
26
27pub fn register(lua: &Lua) -> anyhow::Result<()> {
28    let kumo_mod = get_or_create_module(lua, "kumo")?;
29    let time_mod = get_or_create_sub_module(lua, "time")?;
30
31    let sleep_fn = lua.create_async_function(sleep)?;
32    kumo_mod.set("sleep", sleep_fn.clone())?;
33    time_mod.set("sleep", sleep_fn)?;
34
35    time_mod.set("start_timer", lua.create_function(Timer::start)?)?;
36    time_mod.set("now", lua.create_function(Time::now)?)?;
37    time_mod.set(
38        "from_unix_timestamp",
39        lua.create_function(Time::from_unix_timestamp)?,
40    )?;
41    time_mod.set("with_ymd_hms", lua.create_function(Time::with_ymd_hms)?)?;
42    time_mod.set("parse_rfc3339", lua.create_function(Time::parse_rfc3339)?)?;
43    time_mod.set("parse_rfc2822", lua.create_function(Time::parse_rfc2822)?)?;
44    time_mod.set(
45        "parse_duration",
46        lua.create_function(TimeDelta::parse_duration)?,
47    )?;
48
49    Ok(())
50}
51
52/// A Timer keeps track of the time since it was started,
53/// and will record the duration until its done method is
54/// called, or the __close metamethod is invoked.
55struct Timer {
56    timer: Option<HistogramTimer>,
57}
58
59impl Drop for Timer {
60    fn drop(&mut self) {
61        // We might be called some time after the code is done due
62        // to gc delays and pooling. We don't want the default
63        // Drop impl for HistogramTimer to record in that case:
64        // we will only report when our done method is explicitly
65        // called in lua
66        if let Some(timer) = self.timer.take() {
67            timer.stop_and_discard();
68        }
69    }
70}
71
72impl Timer {
73    fn start(_lua: &Lua, name: String) -> mlua::Result<Self> {
74        let timer = LATENCY_HIST
75            .get_metric_with_label_values(&[&name])
76            .expect("to get histo")
77            .start_timer();
78        Ok(Self { timer: Some(timer) })
79    }
80
81    fn done(_lua: &Lua, this: &mut Self, _: ()) -> mlua::Result<Option<f64>> {
82        Ok(this.timer.take().map(|timer| timer.stop_and_record()))
83    }
84}
85
86impl UserData for Timer {
87    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
88        methods.add_method_mut("done", Self::done);
89        methods.add_meta_method_mut(MetaMethod::Close, Self::done);
90    }
91}
92
93async fn sleep(_lua: Lua, seconds: f64) -> mlua::Result<()> {
94    tokio::time::sleep(Duration::from_secs_f64(seconds)).await;
95    Ok(())
96}
97
98pub struct Time {
99    t: DateTime<Utc>,
100}
101
102impl From<DateTime<Utc>> for Time {
103    fn from(t: DateTime<Utc>) -> Self {
104        Self { t }
105    }
106}
107
108#[derive(Clone, Copy)]
109pub struct TimeDelta(chrono::TimeDelta);
110
111impl From<chrono::TimeDelta> for TimeDelta {
112    fn from(delta: chrono::TimeDelta) -> Self {
113        Self(delta)
114    }
115}
116
117impl From<TimeDelta> for chrono::TimeDelta {
118    fn from(delta: TimeDelta) -> Self {
119        delta.0
120    }
121}
122
123impl Time {
124    fn now(_lua: &Lua, _: ()) -> mlua::Result<Self> {
125        Ok(Self { t: Utc::now() })
126    }
127
128    fn from_unix_timestamp(_lua: &Lua, seconds: mlua::Value) -> mlua::Result<Self> {
129        let dt = match seconds {
130            mlua::Value::Integer(i) => DateTime::from_timestamp_secs(i),
131            mlua::Value::Number(n) => {
132                let seconds = n.trunc() as i64;
133                let nanos = (n.fract() * 1e9) as u32;
134                DateTime::from_timestamp(seconds, nanos)
135            }
136            _ => {
137                return Err(mlua::Error::external(
138                    "timestamp must be either an integer or floating point number of seconds",
139                ));
140            }
141        };
142        Ok(Self {
143            t: dt
144                .ok_or_else(|| mlua::Error::external("invalid timestamp"))?
145                .to_utc(),
146        })
147    }
148
149    fn parse_rfc3339(_lua: &Lua, spec: String) -> mlua::Result<Self> {
150        Ok(Self {
151            t: DateTime::parse_from_rfc3339(&spec)
152                .map_err(any_err)?
153                .to_utc(),
154        })
155    }
156
157    fn parse_rfc2822(_lua: &Lua, spec: String) -> mlua::Result<Self> {
158        Ok(Self {
159            t: DateTime::parse_from_rfc2822(&spec)
160                .map_err(any_err)?
161                .to_utc(),
162        })
163    }
164
165    fn with_ymd_hms(
166        _lua: &Lua,
167        (year, month, day, h, m, s): (i32, u32, u32, u32, u32, u32),
168    ) -> mlua::Result<Self> {
169        match Utc.with_ymd_and_hms(year, month, day, h, m, s) {
170            LocalResult::Single(t) => Ok(Self { t }),
171            // I'm not sure that these LocalResult variants are possible
172            // with UTC, but I'm filling them in with a reasonable error case.
173            LocalResult::Ambiguous(_, _) => Err(mlua::Error::external(
174                "time cannot be represented unambiguously due to a fold in local time",
175            )),
176            LocalResult::None => Err(mlua::Error::external(
177                "time cannot be represented due to a gap in local time",
178            )),
179        }
180    }
181}
182
183impl UserData for Time {
184    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
185        methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
186            Ok(this.t.to_string())
187        });
188        methods.add_meta_method(MetaMethod::Eq, |_, this, other: UserDataRef<Time>| {
189            Ok(this.t.eq(&other.t))
190        });
191        methods.add_method("format", |_, this, fmt: String| {
192            let items = StrftimeItems::new_lenient(&fmt).parse().map_err(any_err)?;
193            Ok(this
194                .t
195                .format_with_items(items.as_slice().iter())
196                .to_string())
197        });
198        methods.add_meta_method(
199            MetaMethod::Sub,
200            |lua, this, value: mlua::Value| match UserDataRef::<Time>::from_lua(value.clone(), lua)
201            {
202                Ok(time) => TimeDelta(this.t.signed_duration_since(time.t)).into_lua(lua),
203                Err(err1) => match UserDataRef::<TimeDelta>::from_lua(value, lua) {
204                    Ok(delta) => Time {
205                        t: this
206                            .t
207                            .checked_sub_signed(delta.0)
208                            .ok_or_else(|| mlua::Error::external("time would overflow"))?,
209                    }
210                    .into_lua(lua),
211                    Err(err2) => Err(mlua::Error::external(format!(
212                        "could not represent argument as \
213                         either Time ({err1:#}) or TimeDelta ({err2:#}"
214                    ))),
215                },
216            },
217        );
218        methods.add_meta_method(MetaMethod::Add, |_, this, delta: UserDataRef<TimeDelta>| {
219            Ok(Time {
220                t: this
221                    .t
222                    .checked_add_signed(delta.0)
223                    .ok_or_else(|| mlua::Error::external("time would overflow"))?,
224            })
225        });
226    }
227
228    fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
229        fields.add_field_method_get("year", |_, this| Ok(this.t.year()));
230        fields.add_field_method_get("month", |_, this| Ok(this.t.month()));
231        fields.add_field_method_get("day", |_, this| Ok(this.t.day()));
232        fields.add_field_method_get("hour", |_, this| Ok(this.t.hour()));
233        fields.add_field_method_get("minute", |_, this| Ok(this.t.minute()));
234        fields.add_field_method_get("second", |_, this| Ok(this.t.second()));
235        fields.add_field_method_get("unix_timestamp", |_, this| Ok(this.t.timestamp()));
236        fields.add_field_method_get("unix_timestamp_millis", |_, this| {
237            Ok(this.t.timestamp_millis())
238        });
239        fields.add_field_method_get("rfc2822", |_, this| Ok(this.t.to_rfc2822()));
240        fields.add_field_method_get("rfc3339", |_, this| Ok(this.t.to_rfc3339()));
241        fields.add_field_method_get("elapsed", |_, this| {
242            let now = Utc::now();
243            Ok(TimeDelta(now.signed_duration_since(this.t)))
244        });
245    }
246}
247
248impl TimeDelta {
249    fn parse_duration(_lua: &Lua, value: mlua::Value) -> mlua::Result<Self> {
250        let delta = match value {
251            mlua::Value::Integer(seconds) => chrono::TimeDelta::try_seconds(seconds)
252                .ok_or_else(|| mlua::Error::external("seconds out of range"))?,
253            mlua::Value::Number(n) => {
254                let d = Duration::from_secs_f64(n);
255                chrono::TimeDelta::from_std(d).map_err(any_err)?
256            }
257            mlua::Value::String(s) => {
258                let s = s.to_str()?;
259                let d = humantime::parse_duration(&s).map_err(any_err)?;
260                chrono::TimeDelta::from_std(d).map_err(any_err)?
261            }
262            _ => return Err(mlua::Error::external("invalid duration value")),
263        };
264
265        Ok(TimeDelta(delta))
266    }
267}
268
269impl UserData for TimeDelta {
270    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
271        methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| {
272            Ok(format_duration(this.0.to_std().map_err(any_err)?).to_string())
273        });
274        methods.add_meta_method(MetaMethod::Eq, |_, this, other: UserDataRef<TimeDelta>| {
275            Ok(this.0.eq(&other.0))
276        });
277        methods.add_meta_method(MetaMethod::Sub, |_, this, other: UserDataRef<TimeDelta>| {
278            Ok(TimeDelta(this.0.checked_sub(&other.0).ok_or_else(
279                || mlua::Error::external("time would overflow"),
280            )?))
281        });
282        methods.add_meta_method(MetaMethod::Add, |_, this, other: UserDataRef<TimeDelta>| {
283            Ok(TimeDelta(this.0.checked_add(&other.0).ok_or_else(
284                || mlua::Error::external("time would overflow"),
285            )?))
286        });
287    }
288
289    fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
290        fields.add_field_method_get("seconds", |_, this| Ok(this.0.as_seconds_f64()));
291        fields.add_field_method_get("nanoseconds", |_, this| Ok(this.0.num_nanoseconds()));
292        fields.add_field_method_get("milliseconds", |_, this| Ok(this.0.num_milliseconds()));
293        fields.add_field_method_get("microseconds", |_, this| Ok(this.0.num_microseconds()));
294        fields.add_field_method_get("human", |_, this| {
295            Ok(format_duration(this.0.to_std().map_err(any_err)?).to_string())
296        });
297    }
298}