You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 takesmy$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 10my$limiter = Acme::Parataxis::RateLimiter->new(
rate=> 5,
burst=> 10
);
# Spawn 1,000 concurrent workersfor (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:
The Semaphore represents the bucket of API tokens. It is initialized with count => $burst.
To make an API call, a fiber calls $semaphore->down (parking if the bucket is empty).
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
};
}
warntime;
sleep 5;
warntime;
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!
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
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_sleepis good for one-offs, but will drift over time in a loop. For very high accuracy situations, we could use a::Tickerthat yields from a channel on a strict interval, automatically dropping ticks if the fiber is too slow (preventing queue buildup), or a token bucketRateLimiterthat 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
::Tickerwould 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.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_putsilently 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
::RateLimiterwould enforce a strict speed limit across your entire application.I could actually build this using the existing
SemaphoreandTicker:Semaphorerepresents the bucket of API tokens. It is initialized withcount => $burst.$semaphore->down(parking if the bucket is empty).Tickerfiber fires X times per second, and calls$semaphore->try_upto 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
geventand Ruby'sFiber::Schedulerintercept standard blocking system calls. Because they all return a negative number from keyword.c'skeywords()function in the perl sourcecode,sleep,read,sysread, etc. can all be overridden.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 callsAcme::Parataxis::await_sleep(5000)instead. Existing, synchronous CPAN modules (likeLWP::UserAgentorDBI) suddenly become fully asynchronous and yield to other fibers without rewriting a single line of their code!All reactions