Async-Redis
view release on metacpan or search on metacpan
examples/pagi-chat/lib/ChatApp/State.pm view on Meta::CPAN
our @EXPORT_OK = qw(
init_redis get_redis get_pubsub
get_session create_session update_session remove_session
get_session_by_name set_session_connected set_session_disconnected
is_session_connected
get_room add_room remove_room get_all_rooms
add_user_to_room remove_user_from_room get_room_users
add_message get_room_messages
get_stats generate_id sanitize_username sanitize_room_name
subscribe_broadcasts register_local_session unregister_local_session
broadcast_to_room broadcast_global add_local_room
add_background_task
);
my $JSON = JSON::MaybeXS->new->utf8->canonical;
# Redis connections (per-worker)
my $redis;
my $pubsub;
my $pubsub_subscription;
# Background task selector (per-worker)
my $background_selector;
# Local session callbacks (for this worker only)
# Redis PubSub delivers to all workers, but we only call callbacks for OUR clients
my %local_sessions;
use constant {
MAX_MESSAGES_PER_ROOM => 100,
SESSION_TTL => 86400, # 24 hours
BROADCAST_CHANNEL => 'chat:broadcast',
};
# Track server start time for uptime
my $server_start_time = time();
# Initialize Redis connections for this worker
async sub init_redis {
my (%args) = @_;
require Async::Redis;
my $host = $args{host} // $ENV{REDIS_HOST} // 'localhost';
my $port = $args{port} // $ENV{REDIS_PORT} // 6379;
# Main connection for commands
$redis = Async::Redis->new(host => $host, port => $port);
await $redis->connect;
# Separate connection for PubSub
my $pubsub_redis = Async::Redis->new(host => $host, port => $port);
await $pubsub_redis->connect;
$pubsub = await $pubsub_redis->subscribe(BROADCAST_CHANNEL);
# Initialize background task selector and start the runner
$background_selector = Future::Selector->new;
_start_selector_runner();
# Start periodic stats timer (every 10 seconds)
_start_stats_timer();
# Initialize default rooms
await add_room('general', 'system');
await add_room('random', 'system');
await add_room('help', 'system');
print STDERR "Worker $$: Redis state initialized\n";
return $redis;
}
sub get_redis { $redis }
sub get_pubsub { $pubsub }
# Background selector runner
my $selector_runner_future;
# Periodic stats timer
my $stats_timer;
# Start the selector with the PubSub listener as the main long-running task
sub _start_selector_runner {
# Add the broadcast listener as a long-running task
# Use gen => to provide a generator that creates the future
# (f => expects a completed future, gen => expects a coderef)
$background_selector->add(
data => 'pubsub-listener',
gen => sub { _broadcast_listener() },
);
# Run the selector in the background
$selector_runner_future = $background_selector->run->on_fail(sub {
my ($err) = @_;
warn "[selector] Runner failed: $err";
})->retain;
}
# Start periodic stats timer using Future::IO (event-loop agnostic)
sub _start_stats_timer {
$stats_timer = (async sub {
while (1) {
await Future::IO->sleep(10);
next unless %local_sessions; # Skip if no clients
eval {
my $stats = await get_stats();
my $msg = $JSON->encode({
global => 1,
payload => {
type => 'stats',
users_online => $stats->{users_online},
rooms_count => $stats->{rooms_count},
uptime => $stats->{uptime},
},
});
await $redis->publish(BROADCAST_CHANNEL, $msg);
};
warn "[stats] Timer error: $@" if $@;
}
})->()->retain;
}
# Add a fire-and-forget background task to the selector
# Pass a coderef that returns a future, not the future itself
sub add_background_task {
my ($gen, $description) = @_;
$description //= 'background task';
return unless $background_selector;
# gen => expects a coderef that generates futures
# It will be called once, and again if the future completes (until it returns undef)
my $called = 0;
$background_selector->add(
data => $description,
gen => sub {
return undef if $called++; # One-shot: only generate once
return ref($gen) eq 'CODE' ? $gen->() : $gen;
},
);
}
# Long-running broadcast listener (async sub with while loop)
async sub _broadcast_listener {
print STDERR "[pubsub] Worker $$: Broadcast listener started\n";
while (my $msg = await $pubsub->next_message) {
next unless $msg->{type} eq 'message';
my $data = eval { $JSON->decode($msg->{message}) };
next unless $data;
my $payload = $data->{payload};
# Global broadcast - deliver to ALL local sessions
if ($data->{global}) {
for my $session_id (keys %local_sessions) {
my $local = $local_sessions{$session_id};
next unless $local && $local->{send_cb};
eval { $local->{send_cb}->($payload) };
( run in 0.529 second using v1.01-cache-2.11-cpan-bbcb1afb8fc )