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