Data-HierTimingWheel-Shared
view release on metacpan or search on metacpan
hiertimingwheel.h view on Meta::CPAN
/*
* hiertimingwheel.h -- Shared-memory hierarchical timing wheel for Linux
*
* O(1) timer scheduling at any delay: a hierarchical (Varghese-Lauck) timing
* wheel of num_levels cascading wheels, each with num_slots (=S) buckets. A
* level-k slot spans S^k ticks, so the structure schedules any delay in
* [1, S^num_levels). Scheduling and cancelling a timer are O(1); a far-future
* timer waits in a coarse level and cascades down to finer levels as its time
* approaches, so it is touched only once per coarse tick (versus a single-level
* wheel that revisits it every rotation). The wheels live in a shared mapping so
* several processes schedule into and advance one clock; a write-preferring futex
* rwlock with reader-slot dead-process recovery guards mutation. Each timer
* carries an arbitrary 64-bit payload returned when it fires.
*
* Layout: Header -> reader_slots[1024] -> occ bitmap -> buckets[num_levels*num_slots] -> timers[capacity]
*/
#ifndef HW_H
#define HW_H
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <math.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <sys/syscall.h>
#include <linux/futex.h>
#include <pthread.h>
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
#error "hiertimingwheel.h: requires little-endian architecture"
#endif
/* ================================================================
* Constants
* ================================================================ */
#define HW_MAGIC 0x4C575448 /* HierTimingWheel */
#define HW_VERSION 2 /* 2: added the occupancy bitmap region (layout change) */
#define HW_ERR_BUFLEN 256
#ifndef HW_READER_SLOTS
#define HW_READER_SLOTS 1024 /* max concurrent reader processes for dead-process recovery */
#endif
/* Occupancy bitmap: one bit per reader slot, set when a process claims a slot and
* cleared on clean release. A writer scans these HW_OCC_WORDS words to visit
* only OCCUPIED slots (O(words + live readers)) instead of all HW_READER_SLOTS. */
#define HW_OCC_WORDS (((HW_READER_SLOTS) + 63) / 64) /* 16 for 1024 slots */
#define HW_OCC_BYTES ((uint64_t)HW_OCC_WORDS * 8) /* 128 bytes */
#define HW_MIN_SLOTS 2
#define HW_MAX_SLOTS 0x10000U /* 2^16 slots per level */
#define HW_MIN_LEVELS 1
#define HW_MAX_LEVELS 16 /* number of cascading wheels */
#define HW_MIN_CAP 1
#define HW_MAX_CAP 0x1000000U /* 2^24 concurrent timers (index fits uint32, < HW_NIL) */
#define HW_NIL 0xFFFFFFFFU /* empty list link / free-list terminator */
#define HW_ERR(fmt, ...) do { if (errbuf) snprintf(errbuf, HW_ERR_BUFLEN, fmt, ##__VA_ARGS__); } while (0)
hiertimingwheel.h view on Meta::CPAN
HW_ERR("ftruncate: %s", strerror(errno)); close(fd); return NULL;
}
(void)fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW);
void *base = mmap(NULL, (size_t)total, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (base == MAP_FAILED) { HW_ERR("mmap: %s", strerror(errno)); close(fd); return NULL; }
hw_init_header(base, (uint32_t)num_slots, (uint32_t)num_levels, (uint32_t)capacity, total);
return hw_setup(base, (size_t)total, NULL, fd);
}
static HwHandle *hw_open_fd(int fd, char *errbuf) {
if (errbuf) errbuf[0] = '\0';
struct stat st;
if (fstat(fd, &st) < 0) { HW_ERR("fstat: %s", strerror(errno)); return NULL; }
if ((uint64_t)st.st_size < sizeof(HwHeader)) { HW_ERR("too small"); return NULL; }
size_t ms = (size_t)st.st_size;
void *base = mmap(NULL, ms, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (base == MAP_FAILED) { HW_ERR("mmap: %s", strerror(errno)); return NULL; }
if (!hw_validate_header((HwHeader *)base, (uint64_t)st.st_size)) {
HW_ERR("invalid hierarchical timing-wheel table"); munmap(base, ms); return NULL;
}
int myfd = fcntl(fd, F_DUPFD_CLOEXEC, 0);
if (myfd < 0) { HW_ERR("fcntl: %s", strerror(errno)); munmap(base, ms); return NULL; }
return hw_setup(base, ms, NULL, myfd);
}
static void hw_destroy(HwHandle *h) {
if (!h) return;
/* Release our reader slot on clean teardown (else short-lived-reader churn
* exhausts the slot table); skip if a read lock is still held (rdepth>0). */
if (h->reader_slots && h->my_slot_idx != UINT32_MAX && h->cached_pid &&
h->cached_fork_gen == __atomic_load_n(&hw_fork_gen, __ATOMIC_RELAXED) &&
__atomic_load_n(&h->reader_slots[h->my_slot_idx].rdepth, __ATOMIC_ACQUIRE) == 0) {
/* Clear our occ bit BEFORE freeing the slot: we still own the pid so no
* claimant can take the slot mid-clear, and rdepth==0 so no writer needs
* to see us. (A crash skips this -> the bit is reclaimed lazily by a
* writer scan / re-claim, same as the pid.) */
hw_occ_clear(h, h->my_slot_idx);
uint32_t expected = h->cached_pid;
__atomic_compare_exchange_n(&h->reader_slots[h->my_slot_idx].pid,
&expected, 0, 0, __ATOMIC_RELEASE, __ATOMIC_RELAXED);
}
if (h->backing_fd >= 0) close(h->backing_fd);
if (h->base) munmap(h->base, h->mmap_size);
free(h->path);
free(h);
}
static inline int hw_msync(HwHandle *h) {
if (!h || !h->base) return 0;
return msync(h->base, h->mmap_size, MS_SYNC);
}
/* ================================================================
* Hierarchical timing-wheel operations (callers hold the lock)
*
* num_levels cascading wheels of num_slots (=S) buckets each; a level-k slot
* spans tick[k]=S^k ticks, so the whole structure schedules any delay in
* [1, S^num_levels). A timer stores its absolute expiry; it is placed in the
* lowest level whose range covers its remaining delay, at slot (expiry/tick[k])%S.
* On each tick level 0 fires its current slot; when level 0 wraps, the next
* level's now-current slot cascades down -- its timers are re-binned into finer
* levels by remaining delay -- recursively up the levels. Timers live in a fixed
* pool with a free list; each bucket is a doubly-linked list (prev/next) so
* cancellation is O(1). Every timer index and bucket index read from shared
* memory is bounds checked, so a corrupt link can never drive an OOB access.
* ================================================================ */
/* unlink timer `t` from its current bucket's doubly-linked list */
static void hw_unlink(HwHandle *h, uint32_t t) {
HwTimer *tm = hw_timer(h, t);
uint32_t pv = tm->prev, nx = tm->next;
if (HW_TIMER_OK(h, pv)) hw_timer(h, pv)->next = nx;
else if (HW_BUCKET_OK(h, tm->bucket)) hw_slots(h)[tm->bucket] = nx; /* t was the head */
if (HW_TIMER_OK(h, nx)) hw_timer(h, nx)->prev = pv;
}
/* return timer `t` to the free list (caller has already unlinked it) */
static void hw_free(HwHandle *h, uint32_t t) {
HwTimer *tm = hw_timer(h, t);
tm->state &= ~1u; /* clear active bit; keep generation so the next reuse of this slot gets a fresh one */
tm->prev = HW_NIL;
tm->bucket = HW_NIL;
tm->next = h->hdr->free_head;
h->hdr->free_head = t;
}
/* link an already-populated timer `t` into flat bucket `b` (prepend) */
static void hw_link(HwHandle *h, uint32_t t, uint64_t b) {
HwTimer *tm = hw_timer(h, t);
tm->bucket = (uint32_t)b;
tm->prev = HW_NIL;
uint32_t head = hw_slots(h)[b];
tm->next = head;
if (HW_TIMER_OK(h, head)) hw_timer(h, head)->prev = t;
hw_slots(h)[b] = t;
}
/* choose the flat bucket for a timer whose absolute expiry is E, given the
* current time now: the lowest level k with (E-now) < tick[k+1], slot
* (E/tick[k])%S. A remaining delay of 0 lands in level 0's current slot. */
static uint64_t hw_bucket_for(HwHandle *h, uint64_t E, uint64_t now) {
uint64_t delay = (E > now) ? E - now : 0;
uint32_t S = h->num_slots, L = h->num_levels, k = 0;
while (k + 1 < L && delay >= h->tick[k + 1]) k++; /* lowest level covering `delay` */
uint64_t slot = (E / h->tick[k]) % S;
return (uint64_t)k * S + slot; /* flat bucket index */
}
/* schedule a timer to fire in `delay` ticks (>= 1); returns its id, or -1 if the
* pool is full or the delay exceeds the wheel's range. (caller holds wrlock) */
static int64_t hw_add_locked(HwHandle *h, uint64_t delay, uint64_t payload) {
if (h->num_slots == 0 || h->num_levels == 0 || h->capacity == 0) return -1;
if (delay < 1) delay = 1; /* minimum effective delay is one tick */
if (delay >= h->tick[h->num_levels]) return -2; /* beyond S^num_levels: not schedulable */
uint32_t t = h->hdr->free_head;
if (!HW_TIMER_OK(h, t)) return -1; /* full (or corrupt free head) */
if (hw_timer(h, t)->state & 1u) return -1; /* Layer B: free head points at an active timer */
h->hdr->free_head = hw_timer(h, t)->next; /* pop the free list */
uint64_t now = h->hdr->now, E = now + delay;
HwTimer *tm = hw_timer(h, t);
tm->payload = payload;
tm->expiry = E;
uint32_t gen = (tm->state >> 1) + 1; /* bump generation for this reuse */
tm->state = (gen << 1) | 1u; /* low bit = active, upper bits = generation */
hw_link(h, t, hw_bucket_for(h, E, now));
h->hdr->count++;
return ((int64_t)gen << 32) | (int64_t)t; /* id encodes the generation so cancel() can reject a reused slot */
}
/* cancel timer `t`; returns 1 if it was active and is now removed, else 0.
* (caller holds the write lock) */
static int hw_cancel_locked(HwHandle *h, uint64_t id) {
uint32_t idx = (uint32_t)(id & 0xFFFFFFFFu);
uint32_t gen = (uint32_t)((id >> 32) & 0x7FFFFFFFu);
if (!HW_TIMER_OK(h, idx)) return 0;
HwTimer *tm = hw_timer(h, idx);
if (!(tm->state & 1u)) return 0; /* free / already fired */
if ((tm->state >> 1) != gen) return 0; /* stale id: this slot was reused by a later timer */
hw_unlink(h, idx);
hw_free(h, idx);
if (h->hdr->count) h->hdr->count--;
return 1;
}
/* re-bin every timer in level-k's now-current bucket into a finer level (called
* when `now` has just reached a multiple of tick[k]). If level k also wrapped,
* cascade level k+1 first (top-down) so higher levels feed into this one. */
static void hw_cascade(HwHandle *h, uint32_t k) {
uint32_t S = h->num_slots;
uint64_t slot = (h->hdr->now / h->tick[k]) % S;
if (slot == 0 && k + 1 < h->num_levels) hw_cascade(h, k + 1); /* level k wrapped too */
uint64_t b = (uint64_t)k * S + slot;
uint32_t t = hw_slots(h)[b];
hw_slots(h)[b] = HW_NIL; /* detach the whole list */
uint64_t guard = 0;
while (HW_TIMER_OK(h, t) && guard++ <= (uint64_t)h->capacity) {
HwTimer *tm = hw_timer(h, t);
uint32_t nx = tm->next;
if (tm->state & 1u) /* re-insert into a lower level (or level-0 current slot) */
hw_link(h, t, hw_bucket_for(h, tm->expiry, h->hdr->now));
t = nx;
}
}
/* advance the wheel by `ticks`, collecting fired payloads into out[] (capped at
* out_cap); returns the number fired. (caller holds the write lock) */
static uint64_t hw_advance_locked(HwHandle *h, uint64_t ticks, uint64_t *out, uint64_t out_cap) {
uint64_t fired = 0;
uint32_t S = h->num_slots;
if (S == 0 || h->num_levels == 0) return 0;
for (uint64_t j = 0; j < ticks; j++) {
h->hdr->now++;
uint64_t now = h->hdr->now;
if (now % S == 0 && h->num_levels > 1) /* level 0 wrapped -> cascade higher levels down */
hw_cascade(h, 1);
uint64_t b = now % S; /* level-0 current slot (tick[0]==1) */
uint32_t t = hw_slots(h)[b];
uint64_t guard = 0;
while (HW_TIMER_OK(h, t) && guard++ <= (uint64_t)h->capacity) {
HwTimer *tm = hw_timer(h, t);
if (!(tm->state & 1u)) break; /* Layer B: corrupt link into a freed node */
uint32_t nx = tm->next;
hw_unlink(h, t); /* every timer in level-0's current slot is due now */
hw_free(h, t);
if (h->hdr->count) h->hdr->count--;
if (fired < out_cap) out[fired++] = tm->payload; /* cap BOTH the write and the count */
t = nx;
}
}
return fired;
}
/* reset to an empty wheel: rethread the free list, clear the buckets, reset time.
* (caller holds the write lock) */
static inline void hw_clear_locked(HwHandle *h) {
uint64_t nb = hw_num_bucket(h), cap = h->capacity;
uint64_t smax = (h->slots_off < h->mmap_size) ? (h->mmap_size - h->slots_off) / sizeof(uint32_t) : 0;
if (nb > smax) nb = smax; /* Layer B */
uint64_t tmax = hw_timers_max(h);
if (cap > tmax) cap = tmax;
uint32_t *slots = hw_slots(h);
for (uint64_t s = 0; s < nb; s++) slots[s] = HW_NIL;
for (uint64_t i = 0; i < cap; i++) {
HwTimer *tm = hw_timer(h, i);
tm->next = (i + 1 < cap) ? (uint32_t)(i + 1) : HW_NIL;
tm->prev = HW_NIL;
tm->bucket = HW_NIL;
tm->state &= ~1u; /* clear active bit; keep generation so ids from before clear() stay distinguishable (no clear-then-reuse ABA) */
}
h->hdr->now = 0;
h->hdr->count = 0;
h->hdr->free_head = cap ? 0 : HW_NIL;
}
#endif /* HW_H */
( run in 1.009 second using v1.01-cache-2.11-cpan-e7c6538aa59 )