Game loops, polling mechanics ("send a heartbeat ping every 5 seconds"), and API integrations ("only make 10 API requests per second to Stripe") require precise timing. await_sleep is good for one-offs, but will drift over time in a loop. For very high accuracy situations, we could use a ::Ticker that yields from a channel on a strict interval, automatically dropping ticks if the fiber is too slow (preventing queue buildup), or a token bucket RateLimiter that fibers can await.

The Ticker

You might think you can just do this to run a task every 1 second:

while (1) {
    do_work();
    Acme::Parataxis::await_sleep(1000);
}

As I mentioned, this would be susceptible to clock drift. If do_work() takes 200ms, your loop actually runs every 1,200ms. Over a minute, you miss 10 executions unless you design your system to account for drift.

A ::Ticker would be a dedicated time object that compensates for execution time to automatically keep a strict cadence. It will push a "tick" onto a channel at exact intervals.

# Fires exactly every 1000ms, regardless of how long the loop takes
my $ticker = Acme::Parataxis::Ticker->new( interval => 1000 );

while (my $tick_time = $ticker->wait_next) {
    do_work(); 
}

A background fiber will record time(), sleep for the remaining time needed to hit the next interval, and use $channel->try_put($time). If the user's loop is so slow that the channel fills up, try_put silently drops the tick so you don't build up a massive backlog of stale ticks.

The Rate Limiter (Token Bucket)

In the age of LLM scrapers gulping down content, you'll be banned if you fire 1,000 concurrent fibers at it at once to a webservice even if you have no ill-intent.

A ::RateLimiter would enforce a strict speed limit across your entire application.

# Allow 5 requests per second, with a maximum burst of 10
my $limiter = Acme::Parataxis::RateLimiter->new(
    rate  => 5,
    burst => 10
);

# Spawn 1,000 concurrent workers
for (1..1000) {
    fiber {
        # This will park the fiber if we are going too fast!
        $limiter->acquire(1);

        my $res = fetch_url("https://api.example.com/data/$_");
    };
}

I could actually build this using the existing Semaphore and Ticker:

  1. The Semaphore represents the bucket of API tokens. It is initialized with count => $burst.
  2. To make an API call, a fiber calls $semaphore->down (parking if the bucket is empty).
  3. A background Ticker fiber fires X times per second, and calls $semaphore->try_up to add tokens back to the bucket (up to the burst limit).

Alternatively, a math based approach calculates the exact microsecond the next request is allowed and simply calls await_sleep() for the difference. I'm not sure which way I'll go. Way back in 2010, I wrote AnyEvent::Handle::Throttle and early this year I wrote Algorithm::RateLimiter::TokenBucket to do this same task in synchronous systems like Net::BitTorrent.

Gevent-style Transparent Unblocking

This is more of a dark magic feature but Python's gevent and Ruby's Fiber::Scheduler intercept standard blocking system calls. Because they all return a negative number from keyword.c's keywords() function in the perl sourcecode, sleep, read, sysread, etc. can all be overridden.

use v5.40;
$|++;
BEGIN {
    *CORE::GLOBAL::sleep = sub : prototype(;$) ( $seconds //= 0 ) {
        say "Custom global sleep for $seconds seconds";
        CORE::sleep($seconds)    # Call the original built-in if needed
    };
}
warn time;
sleep 5;
warn time;

If I implemented this, when a user calls sleep(5) anywhere in the codebase, instead of freezing the OS thread for a number of seconds, the override intercepts it and calls Acme::Parataxis::await_sleep(5000) instead. Existing, synchronous CPAN modules (like LWP::UserAgent or DBI) suddenly become fully asynchronous and yield to other fibers without rewriting a single line of their code!