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! {
13static 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
52struct Timer {
56 timer: Option<HistogramTimer>,
57}
58
59impl Drop for Timer {
60 fn drop(&mut self) {
61 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 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}