How to implement doorbell x86 without race conditions

I’m trying to make a mini scheduler where processes can send a message over a ringbuffer, then call ring() to wake up the receiving core if it was asleep, then that core services whatever it needs in it’s state.update() function. I’m using x86 umonitor and umwait as light sleep is what I’m going for. And I also added the if load on the ring, cause if I go with like fetchAdd generation u32, then it’d be a bunch of cacheline bouncing because ring() can be called a few million times/s from other cores as they submit messages to the main core. I’m pretty much completely lost in terms of properly implementing this with atomics. I think this is on the right track because I check update one more time before truly sleeping which is kinda Dekker’s algorithm, but I think its still wrong. Any help would be super appreciated, thanks.

const DoorbellState = enum(u8) {
    awake,
    asleep,
};

pub const Doorbell = struct {
    const Self = @This();

    state: mem.CachePadded(atomic.Value(DoorbellState)),

    pub fn init() Self {
        return .{ .state = .init(.awake) };
    }

    pub fn ring(self: *Self) void {
        if (self.state.load(.seq_cst) == .awake) {
            @branchHint(.likely);
            return;
        }

        self.state.store(.awake, .release);
    }

    pub fn prepare(self: *Self) void {
        self.state.store(.asleep, .seq_cst);
        hw.time.monitor(&self.state); // calls umonitor
    }

    pub fn cancel(self: *Self) void {
        self.state.store(.awake, .release);
    }

    pub fn commit(self: *Self, deadline: unit.Tick(u64, .n4)) void {
        if (self.state.load(.acquire) == .awake) return;
        hw.time.wait(deadline); // calls umwait
        self.state.store(.awake, .release);
    }
};
var last_time = hw.time.now().to(Tick(u32, .n4));
    while (true) {
        const now = hw.time.now(); // Tick(u64, .n4)
        const now_short = now.to(Tick(u32, .n4));
        last_time = now_short;

        const delay = state.update();

        if (delay.raw > min_sleep) {
            sys.doorbells[cpu].prepare();

            const delay2 = state.update();
            if (delay2.raw <= min_sleep) {
                sys.doorbells[cpu].cancel();
                continue;
            }

            const sleep_now = hw.time.now();
            sys.doorbells[cpu].commit(sleep_now.add(delay2.to(Tick(u64, .n4))));
        } else {
            core.atomic.spinLoop();
        }
    }

I don’t think there is enough context here to write a suitable answer. What are you using this for? What does state.update() do exactly?

From this, I am able to say however that the orderings (notably seq_cst) could probably be relaxed a bit here, and it should not be optimal to sleep immediately when there is no work. The CPU should only go to sleep if it truly has been idle for a while and can’t steal work.