NAME

Acme::Parataxis::Ticker - Drift-Free Interval Timers for Scheduled Fibers

SYNOPSIS

use v5.40;
use Acme::Parataxis qw[async await_sleep];
use Acme::Parataxis::Ticker;

async {
    my $ticker = Acme::Parataxis::Ticker->new( interval => 1000 );    # 1 second ticks

    for ( 1 .. 5 ) {
        last unless my $tick_at = $ticker->wait_next;
        say "Tick at $tick_at; processing work...";
        await_sleep(250);    # work takes time, but ticker intervals do not drift!
    }

    $ticker->stop;           # stop the background ticker fiber
};

DESCRIPTION

A standard loop of the form while (1) { do_work(); await_sleep($ms) } suffers from cumulative timing drift: each iteration takes $ms plus however long do_work() took to execute. Over time, the schedule slides later and later.

An Acme::Parataxis::Ticker calculates intervals against absolute scheduled tick boundaries, sleeping only the remainder of each period. Ticks land on the boundary regardless of how long the consumer took to process the preceding tick.

Under the hood, a private background fiber measures boundaries, sleeps the remainder of the interval, and pushes the timestamp into a capacity-1 Acme::Parataxis::Channel. Before publishing a new tick, any unread tick in the channel is dropped. Exactly one tick is ever outstanding: a slow consumer silently drops missed intervals rather than accumulating a stale backlog.

Calling stop() terminates the ticker: any fiber parked in wait_next() is unblocked and receives undef, and the background ticker fiber's sleep is interrupted so its background job is recalled immediately.

METHODS

new( interval => $ms )

my $ticker = Acme::Parataxis::Ticker->new( interval => 500 );

Constructs a new ticker firing every $ms milliseconds. The first tick lands one full interval after construction. $ms must be positive and defined.

Starts the internal background fiber immediately. Call stop() when the ticker is no longer needed.

wait_next( )

while ( my $tick = $ticker->wait_next ) {
    execute_task();
}

Parks the calling fiber until the next tick boundary arrives and returns that tick's scheduled timestamp. Returns undef once the ticker has been stopped. Must be called from within a scheduled fiber (croaks otherwise).

stop( )

$ticker->stop;

Stops the ticker. Halts publishing, releases any fiber currently waiting in wait_next() with undef, and interrupts the background timer so no orphaned tasks remain in the thread pool. Idempotent. Returns true on the first call.

running( )

Returns true if the ticker is active; false if stop() has been called.

interval( )

Returns the configured interval in milliseconds.

pending( )

Returns the number of unread ticks waiting in the channel (always 0 or 1).

fired( )

Returns the total count of ticks produced since construction.

dropped( )

Returns the count of ticks superseded and dropped because a consumer did not collect them before the next interval arrived.

skipped( )

Returns the count of tick boundaries leapfrogged when the runtime woke significantly behind schedule. Skipped boundaries are not counted as fired or dropped.

Recipes

Periodic maintenance loops

async {
    my $ticker = Acme::Parataxis::Ticker->new( interval => 5000 );

    # Terminating loop: ends naturally when stop() is called:
    while ( my $t = $ticker->wait_next ) {
        prune_cache();
    }
};

Virtual clock testing

Tickers integrate with Acme::Parataxis virtual time under run( virtual => 1 ):

Acme::Parataxis->run(
    virtual => 1,
    code    => sub {
        my $ticker = Acme::Parataxis::Ticker->new( interval => 1000 );
        my $ticks  = 0;

        fiber {
            while ( $ticker->wait_next ) { $ticks++ }
        };
        Acme::Parataxis->yield;

        # Fast-forward 3000ms virtually:
        Acme::Parataxis->advance(3000);
        Acme::Parataxis->yield;

        say "Ticks collected: $ticks";    # 3
        $ticker->stop;
    }
);

Behavior Notes

  • Always call stop()

    An unstopped ticker maintains a background fiber waiting on future tick boundaries. Forgetting to call stop() will cause run() to hang waiting for all fibers to drain.

  • Exact boundary spacing

    The timestamp returned by wait_next() is the scheduled boundary time, not the delivery time, keeping consecutive timestamps evenly spaced even if a tick woke slightly late.

SEE ALSO

Acme::Parataxis, Acme::Parataxis::RateLimiter, Acme::Parataxis::Channel

AUTHOR

Sanko Robinson https://github.com/sanko

LICENSE

Copyright (C) Sanko Robinson.

This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.