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
46struct Timer {
50 timer: Option<HistogramTimer>,
51}
52
53impl Drop for Timer {
54 fn drop(&mut self) {
55 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 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}