412 lines
14 KiB
Zig
412 lines
14 KiB
Zig
|
const std = @import("std");
|
||
|
const builtin = @import("builtin");
|
||
|
const assert = std.debug.assert;
|
||
|
const os = std.os;
|
||
|
|
||
|
/// A timer fires a callback after a specified amount of time. A timer can
|
||
|
/// repeat by returning "rearm" in the callback or by rescheduling the
|
||
|
/// start within the callback.
|
||
|
pub fn Timer(comptime xev: type) type {
|
||
|
return struct {
|
||
|
const Self = @This();
|
||
|
|
||
|
/// Create a new timer.
|
||
|
pub fn init() !Self {
|
||
|
return .{};
|
||
|
}
|
||
|
|
||
|
pub fn deinit(self: *const Self) void {
|
||
|
// Nothing for now.
|
||
|
_ = self;
|
||
|
}
|
||
|
|
||
|
/// Start the timer. The timer will execute in next_ms milliseconds from
|
||
|
/// now.
|
||
|
///
|
||
|
/// This will use the monotonic clock on your system if available so
|
||
|
/// this is immune to system clock changes or drift. The callback is
|
||
|
/// guaranteed to fire NO EARLIER THAN "next_ms" milliseconds. We can't
|
||
|
/// make any guarantees about exactness or time bounds because its possible
|
||
|
/// for your OS to just... pause.. the process for an indefinite period of
|
||
|
/// time.
|
||
|
///
|
||
|
/// Like everything else in libxev, if you want something to repeat, you
|
||
|
/// must then requeue the completion manually. This punts off one of the
|
||
|
/// "hard" aspects of timers: it is up to you to determine what the semantic
|
||
|
/// meaning of intervals are. For example, if you want a timer to repeat every
|
||
|
/// 10 seconds, is it every 10th second of a wall clock? every 10th second
|
||
|
/// after an invocation? every 10th second after the work time from the
|
||
|
/// invocation? You have the power to answer these questions, manually.
|
||
|
pub fn run(
|
||
|
self: Self,
|
||
|
loop: *xev.Loop,
|
||
|
c: *xev.Completion,
|
||
|
next_ms: u64,
|
||
|
comptime Userdata: type,
|
||
|
userdata: ?*Userdata,
|
||
|
comptime cb: *const fn (
|
||
|
ud: ?*Userdata,
|
||
|
l: *xev.Loop,
|
||
|
c: *xev.Completion,
|
||
|
r: RunError!void,
|
||
|
) xev.CallbackAction,
|
||
|
) void {
|
||
|
_ = self;
|
||
|
|
||
|
loop.timer(c, next_ms, userdata, (struct {
|
||
|
fn callback(
|
||
|
ud: ?*anyopaque,
|
||
|
l_inner: *xev.Loop,
|
||
|
c_inner: *xev.Completion,
|
||
|
r: xev.Result,
|
||
|
) xev.CallbackAction {
|
||
|
return @call(.always_inline, cb, .{
|
||
|
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
|
||
|
l_inner,
|
||
|
c_inner,
|
||
|
if (r.timer) |trigger| @as(RunError!void, switch (trigger) {
|
||
|
.request, .expiration => {},
|
||
|
.cancel => error.Canceled,
|
||
|
}) else |err| err,
|
||
|
});
|
||
|
}
|
||
|
}).callback);
|
||
|
}
|
||
|
|
||
|
/// Reset a timer to execute in next_ms milliseconds. If the timer
|
||
|
/// is already started, this will stop it and restart it. If the
|
||
|
/// timer has never been started, this is equivalent to running "run".
|
||
|
/// In every case, the timer callback is updated to the given userdata
|
||
|
/// and callback.
|
||
|
///
|
||
|
/// This requires an additional completion c_cancel to represent
|
||
|
/// the need to possibly cancel the previous timer. You can check
|
||
|
/// if c_cancel was used by checking the state() after the call.
|
||
|
///
|
||
|
/// VERY IMPORTANT: both c and c_cancel MUST NOT be undefined. They
|
||
|
/// must be initialized to ".{}" if being used for the first time.
|
||
|
pub fn reset(
|
||
|
self: Self,
|
||
|
loop: *xev.Loop,
|
||
|
c: *xev.Completion,
|
||
|
c_cancel: *xev.Completion,
|
||
|
next_ms: u64,
|
||
|
comptime Userdata: type,
|
||
|
userdata: ?*Userdata,
|
||
|
comptime cb: *const fn (
|
||
|
ud: ?*Userdata,
|
||
|
l: *xev.Loop,
|
||
|
c: *xev.Completion,
|
||
|
r: RunError!void,
|
||
|
) xev.CallbackAction,
|
||
|
) void {
|
||
|
_ = self;
|
||
|
|
||
|
loop.timer_reset(c, c_cancel, next_ms, userdata, (struct {
|
||
|
fn callback(
|
||
|
ud: ?*anyopaque,
|
||
|
l_inner: *xev.Loop,
|
||
|
c_inner: *xev.Completion,
|
||
|
r: xev.Result,
|
||
|
) xev.CallbackAction {
|
||
|
return @call(.always_inline, cb, .{
|
||
|
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
|
||
|
l_inner,
|
||
|
c_inner,
|
||
|
if (r.timer) |trigger| @as(RunError!void, switch (trigger) {
|
||
|
.request, .expiration => {},
|
||
|
.cancel => error.Canceled,
|
||
|
}) else |err| err,
|
||
|
});
|
||
|
}
|
||
|
}).callback);
|
||
|
}
|
||
|
|
||
|
/// Cancel a previously started timer. The timer to cancel used the completion
|
||
|
/// "c_cancel". A new completion "c" must be specified which will be called
|
||
|
/// with the callback once cancellation is complete.
|
||
|
///
|
||
|
/// The original timer will still have its callback fired but with the
|
||
|
/// error "error.Canceled".
|
||
|
pub fn cancel(
|
||
|
self: Self,
|
||
|
loop: *xev.Loop,
|
||
|
c_timer: *xev.Completion,
|
||
|
c_cancel: *xev.Completion,
|
||
|
comptime Userdata: type,
|
||
|
userdata: ?*Userdata,
|
||
|
comptime cb: *const fn (
|
||
|
ud: ?*Userdata,
|
||
|
l: *xev.Loop,
|
||
|
c: *xev.Completion,
|
||
|
r: CancelError!void,
|
||
|
) xev.CallbackAction,
|
||
|
) void {
|
||
|
_ = self;
|
||
|
|
||
|
c_cancel.* = switch (xev.backend) {
|
||
|
.io_uring => .{
|
||
|
.op = .{
|
||
|
.timer_remove = .{
|
||
|
.timer = c_timer,
|
||
|
},
|
||
|
},
|
||
|
|
||
|
.userdata = userdata,
|
||
|
.callback = (struct {
|
||
|
fn callback(
|
||
|
ud: ?*anyopaque,
|
||
|
l_inner: *xev.Loop,
|
||
|
c_inner: *xev.Completion,
|
||
|
r: xev.Result,
|
||
|
) xev.CallbackAction {
|
||
|
return @call(.always_inline, cb, .{
|
||
|
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
|
||
|
l_inner,
|
||
|
c_inner,
|
||
|
if (r.timer_remove) |_| {} else |err| err,
|
||
|
});
|
||
|
}
|
||
|
}).callback,
|
||
|
},
|
||
|
|
||
|
.epoll,
|
||
|
.kqueue,
|
||
|
.wasi_poll,
|
||
|
.iocp,
|
||
|
=> .{
|
||
|
.op = .{
|
||
|
.cancel = .{
|
||
|
.c = c_timer,
|
||
|
},
|
||
|
},
|
||
|
|
||
|
.userdata = userdata,
|
||
|
.callback = (struct {
|
||
|
fn callback(
|
||
|
ud: ?*anyopaque,
|
||
|
l_inner: *xev.Loop,
|
||
|
c_inner: *xev.Completion,
|
||
|
r: xev.Result,
|
||
|
) xev.CallbackAction {
|
||
|
return @call(.always_inline, cb, .{
|
||
|
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
|
||
|
l_inner,
|
||
|
c_inner,
|
||
|
if (r.cancel) |_| {} else |err| err,
|
||
|
});
|
||
|
}
|
||
|
}).callback,
|
||
|
},
|
||
|
};
|
||
|
|
||
|
loop.add(c_cancel);
|
||
|
}
|
||
|
|
||
|
/// Error that could happen while running a timer.
|
||
|
pub const RunError = error{
|
||
|
/// The timer was canceled before it could expire
|
||
|
Canceled,
|
||
|
|
||
|
/// Some unexpected error.
|
||
|
Unexpected,
|
||
|
};
|
||
|
|
||
|
pub const CancelError = xev.CancelError;
|
||
|
|
||
|
test "timer" {
|
||
|
const testing = std.testing;
|
||
|
|
||
|
var loop = try xev.Loop.init(.{});
|
||
|
defer loop.deinit();
|
||
|
|
||
|
var timer = try init();
|
||
|
defer timer.deinit();
|
||
|
|
||
|
// Add the timer
|
||
|
var called = false;
|
||
|
var c1: xev.Completion = undefined;
|
||
|
timer.run(&loop, &c1, 1, bool, &called, (struct {
|
||
|
fn callback(
|
||
|
ud: ?*bool,
|
||
|
_: *xev.Loop,
|
||
|
_: *xev.Completion,
|
||
|
r: RunError!void,
|
||
|
) xev.CallbackAction {
|
||
|
_ = r catch unreachable;
|
||
|
ud.?.* = true;
|
||
|
return .disarm;
|
||
|
}
|
||
|
}).callback);
|
||
|
|
||
|
// Wait
|
||
|
try loop.run(.until_done);
|
||
|
try testing.expect(called);
|
||
|
}
|
||
|
|
||
|
test "timer reset" {
|
||
|
const testing = std.testing;
|
||
|
|
||
|
var loop = try xev.Loop.init(.{});
|
||
|
defer loop.deinit();
|
||
|
|
||
|
var timer = try init();
|
||
|
defer timer.deinit();
|
||
|
|
||
|
var c_timer: xev.Completion = .{};
|
||
|
var c_cancel: xev.Completion = .{};
|
||
|
const cb = (struct {
|
||
|
fn callback(
|
||
|
ud: ?*bool,
|
||
|
_: *xev.Loop,
|
||
|
_: *xev.Completion,
|
||
|
r: RunError!void,
|
||
|
) xev.CallbackAction {
|
||
|
_ = r catch unreachable;
|
||
|
ud.?.* = true;
|
||
|
return .disarm;
|
||
|
}
|
||
|
}).callback;
|
||
|
|
||
|
// Add the timer
|
||
|
var canceled = false;
|
||
|
timer.run(&loop, &c_timer, 100_000, bool, &canceled, cb);
|
||
|
|
||
|
// Wait
|
||
|
try loop.run(.no_wait);
|
||
|
try testing.expect(!canceled);
|
||
|
|
||
|
// Reset it
|
||
|
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
|
||
|
|
||
|
try loop.run(.until_done);
|
||
|
try testing.expect(canceled);
|
||
|
try testing.expect(c_timer.state() == .dead);
|
||
|
try testing.expect(c_cancel.state() == .dead);
|
||
|
}
|
||
|
|
||
|
test "timer reset before tick" {
|
||
|
const testing = std.testing;
|
||
|
|
||
|
var loop = try xev.Loop.init(.{});
|
||
|
defer loop.deinit();
|
||
|
|
||
|
var timer = try init();
|
||
|
defer timer.deinit();
|
||
|
|
||
|
var c_timer: xev.Completion = .{};
|
||
|
var c_cancel: xev.Completion = .{};
|
||
|
const cb = (struct {
|
||
|
fn callback(
|
||
|
ud: ?*bool,
|
||
|
_: *xev.Loop,
|
||
|
_: *xev.Completion,
|
||
|
r: RunError!void,
|
||
|
) xev.CallbackAction {
|
||
|
_ = r catch unreachable;
|
||
|
ud.?.* = true;
|
||
|
return .disarm;
|
||
|
}
|
||
|
}).callback;
|
||
|
|
||
|
// Add the timer
|
||
|
var canceled = false;
|
||
|
timer.run(&loop, &c_timer, 100_000, bool, &canceled, cb);
|
||
|
|
||
|
// Reset it
|
||
|
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
|
||
|
|
||
|
try loop.run(.until_done);
|
||
|
try testing.expect(canceled);
|
||
|
try testing.expect(c_timer.state() == .dead);
|
||
|
try testing.expect(c_cancel.state() == .dead);
|
||
|
}
|
||
|
|
||
|
test "timer reset after trigger" {
|
||
|
const testing = std.testing;
|
||
|
|
||
|
var loop = try xev.Loop.init(.{});
|
||
|
defer loop.deinit();
|
||
|
|
||
|
var timer = try init();
|
||
|
defer timer.deinit();
|
||
|
|
||
|
var c_timer: xev.Completion = .{};
|
||
|
var c_cancel: xev.Completion = .{};
|
||
|
const cb = (struct {
|
||
|
fn callback(
|
||
|
ud: ?*bool,
|
||
|
_: *xev.Loop,
|
||
|
_: *xev.Completion,
|
||
|
r: RunError!void,
|
||
|
) xev.CallbackAction {
|
||
|
_ = r catch unreachable;
|
||
|
ud.?.* = true;
|
||
|
return .disarm;
|
||
|
}
|
||
|
}).callback;
|
||
|
|
||
|
// Add the timer
|
||
|
var canceled = false;
|
||
|
timer.run(&loop, &c_timer, 1, bool, &canceled, cb);
|
||
|
try loop.run(.until_done);
|
||
|
try testing.expect(canceled);
|
||
|
canceled = false;
|
||
|
|
||
|
// Reset it
|
||
|
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
|
||
|
|
||
|
try loop.run(.until_done);
|
||
|
try testing.expect(canceled);
|
||
|
try testing.expect(c_timer.state() == .dead);
|
||
|
try testing.expect(c_cancel.state() == .dead);
|
||
|
}
|
||
|
|
||
|
test "timer cancel" {
|
||
|
const testing = std.testing;
|
||
|
|
||
|
var loop = try xev.Loop.init(.{});
|
||
|
defer loop.deinit();
|
||
|
|
||
|
var timer = try init();
|
||
|
defer timer.deinit();
|
||
|
|
||
|
// Add the timer
|
||
|
var canceled = false;
|
||
|
var c1: xev.Completion = undefined;
|
||
|
timer.run(&loop, &c1, 100_000, bool, &canceled, (struct {
|
||
|
fn callback(
|
||
|
ud: ?*bool,
|
||
|
_: *xev.Loop,
|
||
|
_: *xev.Completion,
|
||
|
r: RunError!void,
|
||
|
) xev.CallbackAction {
|
||
|
ud.?.* = if (r) false else |err| err == error.Canceled;
|
||
|
return .disarm;
|
||
|
}
|
||
|
}).callback);
|
||
|
|
||
|
// Cancel
|
||
|
var cancel_confirm = false;
|
||
|
var c2: xev.Completion = undefined;
|
||
|
timer.cancel(&loop, &c1, &c2, bool, &cancel_confirm, (struct {
|
||
|
fn callback(
|
||
|
ud: ?*bool,
|
||
|
_: *xev.Loop,
|
||
|
_: *xev.Completion,
|
||
|
r: CancelError!void,
|
||
|
) xev.CallbackAction {
|
||
|
_ = r catch unreachable;
|
||
|
ud.?.* = true;
|
||
|
return .disarm;
|
||
|
}
|
||
|
}).callback);
|
||
|
|
||
|
// Wait
|
||
|
try loop.run(.until_done);
|
||
|
try testing.expect(canceled);
|
||
|
try testing.expect(cancel_confirm);
|
||
|
}
|
||
|
};
|
||
|
}
|