From 1a41f9aebb2770cf422a1ad13868b32a05d1f076 Mon Sep 17 00:00:00 2001 From: Pragmatic Software Date: Mon, 21 Jun 2021 17:26:24 -0700 Subject: [PATCH] Refactor PBot::Timer into PBot::EventQueue Make better use of PBot::IRC's select loop Remove use of SIGALRM --- PBot/AntiFlood.pm | 2 +- PBot/BanList.pm | 22 +-- PBot/ChanOps.pm | 3 +- PBot/Channels.pm | 4 +- PBot/DualIndexHashObject.pm | 2 +- PBot/DualIndexSQLiteObject.pm | 4 +- PBot/EventDispatcher.pm | 2 + PBot/{Timer.pm => EventQueue.pm} | 256 ++++++++++++++++--------------- PBot/IRCHandlers.pm | 3 +- PBot/IgnoreList.pm | 8 +- PBot/Interpreter.pm | 4 +- PBot/LagChecker.pm | 6 +- PBot/MessageHistory_SQLite.pm | 4 +- PBot/MiscCommands.pm | 4 +- PBot/PBot.pm | 23 +-- PBot/ProcessManager.pm | 6 +- PBot/WebPaste.pm | 2 - Plugins/AntiRepeat.pm | 4 +- Plugins/AntiTwitter.pm | 4 +- Plugins/Battleship.pm | 8 +- Plugins/Connect4.pm | 8 +- Plugins/RelayUnreg.pm | 4 +- Plugins/RemindMe.pm | 8 +- Plugins/Spinach.pm | 14 +- 24 files changed, 206 insertions(+), 199 deletions(-) rename PBot/{Timer.pm => EventQueue.pm} (50%) diff --git a/PBot/AntiFlood.pm b/PBot/AntiFlood.pm index e3e6b2c1..13878f81 100644 --- a/PBot/AntiFlood.pm +++ b/PBot/AntiFlood.pm @@ -36,7 +36,7 @@ sub initialize { $self->{'ban-exemptions'} = PBot::DualIndexHashObject->new(name => 'Ban exemptions', filename => $filename, pbot => $self->{pbot}); $self->{'ban-exemptions'}->load; - $self->{pbot}->{timer}->register(sub { $self->adjust_offenses }, 60 * 60 * 1, 'AntiFlood Adjust Offenses'); + $self->{pbot}->{event_queue}->enqueue(sub { $self->adjust_offenses }, 60 * 60 * 1, 'Adjust anti-flood offenses'); $self->{pbot}->{registry}->add_default('text', 'antiflood', 'enforce', $conf{enforce_antiflood} // 1); diff --git a/PBot/BanList.pm b/PBot/BanList.pm index b0325ab2..d17d0246 100644 --- a/PBot/BanList.pm +++ b/PBot/BanList.pm @@ -59,7 +59,7 @@ sub initialize { $self->{ban_queue} = {}; $self->{unban_queue} = {}; - $self->{pbot}->{timer}->register(sub { $self->flush_unban_queue }, 30, 'Unban Queue'); + $self->{pbot}->{event_queue}->enqueue(sub { $self->flush_unban_queue }, 30, 'Flush unban queue'); } sub cmd_banlist { @@ -203,7 +203,7 @@ sub compare_banlist { $self->{pbot}->{logger}->log("BanList: Saved ban +b $mask no longer exists in $channel.\n"); # TODO option to restore ban $self->{banlist}->remove($channel, $mask, undef, 1); - $self->{pbot}->{timer}->dequeue_event("unban $channel $mask"); + $self->{pbot}->{event_queue}->dequeue_event("unban $channel $mask"); } } @@ -248,7 +248,7 @@ sub compare_quietlist { $self->{pbot}->{logger}->log("BanList: Saved quiet +q $mask no longer exists in $channel.\n"); # TODO option to restore quiet $self->{quietlist}->remove($channel, $mask, undef, 1); - $self->{pbot}->{timer}->dequeue_event("unmute $channel $mask"); + $self->{pbot}->{event_queue}->dequeue_event("unmute $channel $mask"); } } @@ -294,15 +294,15 @@ sub track_mode { if ($mode eq "-b") { $self->{banlist}->remove($channel, $mask); - $self->{pbot}->{timer}->dequeue_event("unban $channel $mask"); + $self->{pbot}->{event_queue}->dequeue_event("unban $channel $mask"); # freenode strips channel forwards from unban result if no ban exists with a channel forward my $join_flood_channel = $self->{pbot}->{registry}->get_value('antiflood', 'join_flood_channel') // '#stop-join-flood'; $self->{banlist}->remove($channel, "$mask\$$join_flood_channel"); - $self->{pbot}->{timer}->dequeue_event(lc "unban $channel $mask\$$join_flood_channel"); + $self->{pbot}->{event_queue}->dequeue_event(lc "unban $channel $mask\$$join_flood_channel"); } elsif ($mode eq "-$mute_char") { $self->{quietlist}->remove($channel, $mask); - $self->{pbot}->{timer}->dequeue_event("unmute $channel $mask"); + $self->{pbot}->{event_queue}->dequeue_event("unmute $channel $mask"); } } @@ -312,7 +312,7 @@ sub track_mode { if ($nick eq "ChanServ" or $mask =~ m/##fix_your_connection$/i) { if ($self->{banlist}->exists($channel, $mask)) { $self->{banlist}->set($channel, $mask, 'timeout', gettimeofday + $self->{pbot}->{registry}->get_value('banlist', 'chanserv_ban_timeout')); - $self->{pbot}->{timer}->update_interval("unban $channel $mask", $self->{pbot}->{registry}->get_value('banlist', 'chanserv_ban_timeout')); + $self->{pbot}->{event_queue}->update_interval("unban $channel $mask", $self->{pbot}->{registry}->get_value('banlist', 'chanserv_ban_timeout')); } else { my $data = { reason => 'Temp ban for banned-by-ChanServ or mask is *!*@*##fix_your_connection', @@ -349,7 +349,7 @@ sub track_mode { $self->{pbot}->{logger}->log("WEIRD MUTE THING $nick...\n"); if ($self->{quietlist}->exists($channel, $mask)) { $self->{quietlist}->set($channel, $mask, 'timeout', gettimeofday + $self->{pbot}->{registry}->get_value('banlist', 'chanserv_ban_timeout')); - $self->{pbot}->{timer}->update_interval("unmute $channel $mask", $self->{pbot}->{registry}->get_value('banlist', 'chanserv_ban_timeout')); + $self->{pbot}->{event_queue}->update_interval("unmute $channel $mask", $self->{pbot}->{registry}->get_value('banlist', 'chanserv_ban_timeout')); } else { my $data = { reason => 'Temp mute', @@ -557,7 +557,7 @@ sub ban_user_timed { } my $method = $mode eq 'b' ? 'unban' : 'unmute'; - $self->{pbot}->{timer}->dequeue_event("$method $channel $mask"); + $self->{pbot}->{event_queue}->dequeue_event("$method $channel $mask"); if ($length > 0) { $self->enqueue_unban($channel, $mode, $mask, $length); @@ -704,9 +704,9 @@ sub enqueue_unban { my $method = $mode eq 'b' ? 'unban' : 'unmute'; - $self->{pbot}->{timer}->enqueue_event( + $self->{pbot}->{event_queue}->enqueue_event( sub { - $self->{pbot}->{timer}->update_interval("$method $channel $hostmask", 60 * 15, 1); # try again in 15 minutes + $self->{pbot}->{event_queue}->update_interval("$method $channel $hostmask", 60 * 15, 1); # try again in 15 minutes return if not $self->{pbot}->{joined_channels}; $self->unban_user($channel, $mode, $hostmask); }, $interval, "$method $channel $hostmask", 1 diff --git a/PBot/ChanOps.pm b/PBot/ChanOps.pm index 3d94fb3f..2ed923b6 100644 --- a/PBot/ChanOps.pm +++ b/PBot/ChanOps.pm @@ -27,7 +27,8 @@ sub initialize { $self->{pbot}->{registry}->add_default('text', 'general', 'deop_timeout', 300); - $self->{pbot}->{timer}->register(sub { $self->check_opped_timeouts }, 10, 'Check Opped Timeouts'); + # TODO: enqueue OP events as needed instead of naively checking every 10 seconds + $self->{pbot}->{event_queue}->enqueue(sub { $self->check_opped_timeouts }, 10, 'Check opped timeouts'); } sub track_mode { diff --git a/PBot/Channels.pm b/PBot/Channels.pm index 71fd1d05..870a7e64 100644 --- a/PBot/Channels.pm +++ b/PBot/Channels.pm @@ -82,8 +82,8 @@ sub cmd_remove { # clear banlists $self->{pbot}->{banlist}->{banlist}->remove($context->{arguments}); $self->{pbot}->{banlist}->{quietlist}->remove($context->{arguments}); - $self->{pbot}->{timer}->dequeue_event("unban $context->{arguments} .*"); - $self->{pbot}->{timer}->dequeue_event("unmute $context->{arguments} .*"); + $self->{pbot}->{event_queue}->dequeue_event("unban $context->{arguments} .*"); + $self->{pbot}->{event_queue}->dequeue_event("unmute $context->{arguments} .*"); # TODO: ignores, etc? return $self->{channels}->remove($context->{arguments}); diff --git a/PBot/DualIndexHashObject.pm b/PBot/DualIndexHashObject.pm index 29d381dc..97bbdc53 100644 --- a/PBot/DualIndexHashObject.pm +++ b/PBot/DualIndexHashObject.pm @@ -125,7 +125,7 @@ sub save { if ($self->{save_queue_timeout}) { # enqueue the save to prevent save-thrashing - $self->{pbot}->{timer}->replace_subref_or_enqueue_event( + $self->{pbot}->{enqueue_event}->replace_subref_or_enqueue_event( $subref, $self->{save_queue_timeout}, "save $self->{name}", diff --git a/PBot/DualIndexSQLiteObject.pm b/PBot/DualIndexSQLiteObject.pm index 0118dde2..4c7a7dae 100644 --- a/PBot/DualIndexSQLiteObject.pm +++ b/PBot/DualIndexSQLiteObject.pm @@ -39,7 +39,7 @@ sub initialize { $self->{pbot}->{registry}->add_trigger('dualindexsqliteobject', "debug_$self->{name}", sub { $self->sqlite_debug_trigger(@_) }); $self->{pbot}->{atexit}->register(sub { $self->end; return; }); - $self->{pbot}->{timer}->register(sub {$self->trim_cache }, 60, "DualIndexSQLiteObject $self->{name} Timer"); + $self->{pbot}->{event_queue}->enqueue(sub {$self->trim_cache }, 60, "Trim $self->{name} cache"); $self->begin; } @@ -80,7 +80,7 @@ sub end { $self->{dbh} = undef; } - $self->{pbot}->{timer}->unregister("DualIndexSQLiteObject $self->{name} Timer"); + $self->{pbot}->{event_queue}->dequeue("Trim $self->{name} cache"); } sub load { diff --git a/PBot/EventDispatcher.pm b/PBot/EventDispatcher.pm index ae908f73..e498cb32 100644 --- a/PBot/EventDispatcher.pm +++ b/PBot/EventDispatcher.pm @@ -1,6 +1,8 @@ # File: EventDispatcher.pm # # Purpose: Registers event handlers and dispatches events to them. +# +# Note: PBot::EventDispatcher has no relation to PBot::EventQueue. # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this diff --git a/PBot/Timer.pm b/PBot/EventQueue.pm similarity index 50% rename from PBot/Timer.pm rename to PBot/EventQueue.pm index 0f746487..32a60ac6 100644 --- a/PBot/Timer.pm +++ b/PBot/EventQueue.pm @@ -1,50 +1,33 @@ -# File: Timer.pm +# File: EventQueue.pm # -# Purpose: Provides functionality to register subroutines/events to be invoked +# Purpose: Provides functionality to manage event subroutines which are invoked # at a future time, optionally recurring. # -# If no subroutines/events are registered/enqueued, the default on_tick() -# method, which can be overridden, is invoked. -# -# Uses own internal seconds counter and relative-intervals to avoid -# timeout desyncs due to system clock changes. -# -# Note: Uses ALARM signal. +# Note: PBot::EventQueue has no relation to PBot::EventDispatcher. # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -package PBot::Timer; +package PBot::EventQueue; use parent 'PBot::Class'; use PBot::Imports; -use Time::Duration qw/concise duration/; - -our $seconds //= 0; -our $waitfor //= 1; -our @timer_funcs; - -# alarm signal handler (poor-man's timer) -$SIG{ALRM} = sub { - $seconds += $waitfor; - foreach my $func (@timer_funcs) { &$func; } -}; +use Time::HiRes qw/time/; +use Time::Duration; sub initialize { my ($self, %conf) = @_; - my $timeout = $conf{timeout} // 10; - $self->{name} = $conf{name} // "Unnamed ${timeout}s Timer"; - $self->{enabled} = 0; + + # array of pending events $self->{event_queue} = []; - $self->{last} = $seconds; - $self->{timeout} = $timeout; - $self->{pbot}->{commands}->register(sub { $self->cmd_eventqueue(@_) }, 'eventqueue', 1); + # register `eventqueue` bot command + $self->{pbot}->{commands}->register(sub { $self->cmd_eventqueue(@_) }, 'eventqueue', 1); + + # add `can-eventqueue` capability to admin group $self->{pbot}->{capabilities}->add('admin', 'can-eventqueue', 1); - - $self->{timer_func} = sub { $self->on_tick_handler(@_) }; } sub cmd_eventqueue { @@ -63,6 +46,7 @@ sub cmd_eventqueue { my $result = eval { my $text = "Queued events:\n"; + my ($regex) = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); my $i = 0; @@ -76,19 +60,29 @@ sub cmd_eventqueue { $events++; - my $duration = concise duration $event->{timeout} - $seconds; - $text .= " $i) in $duration: $event->{id}"; + my $duration = $event->{timeout} - time; + + if ($duration < 0) { + # current time has passed an event's time but the + # event hasn't left the queue yet. we'll show these + # as, e.g., "pending 5s ago" + $duration = 'pending ' . concise ago -$duration; + } else { + $duration = 'in ' . concise duration $duration; + } + + $text .= " $i) $duration: $event->{id}"; $text .= ' [R]' if $event->{repeating}; $text .= ";\n"; } return "No events found." if $events == 0; - return $text; + return $text . "$events events.\n"; }; - if ($@) { - my $error = $@; + if (my $error = $@) { + # strip source information to prettify error for non-developer consumption $error =~ s/ at PBot.*//; return "Bad regex: $error"; } @@ -98,22 +92,28 @@ sub cmd_eventqueue { if ($command eq 'add') { my ($duration, $command) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); - return "Usage: eventqueue add [-repeat]" if not defined $duration or not defined $command; - my ($delay, $error) = $self->{pbot}->{parsedate}->parsedate($duration); + if (not defined $duration or not defined $command) { + return "Usage: eventqueue add [-repeat]"; + } + + # convert text like "5 minutes" or "1 week" or "next tuesday" to seconds + my ($seconds, $error) = $self->{pbot}->{parsedate}->parsedate($duration); return $error if defined $error; - my $repeating = 0; - $repeating = 1 if $command =~ s/^-repeat\s+|\s+-repeat$//g; + # check for `-repeating` at front or end of command + my $repeating = $command =~ s/^-repeat\s+|\s+-repeat$//g; my $cmd = { - nick => $context->{nick}, - user => $context->{user}, - host => $context->{host}, - command => $command, + nick => $context->{nick}, + user => $context->{user}, + host => $context->{host}, + hostmask => $context->{hostmask}, + command => $command, }; - $self->{pbot}->{interpreter}->add_to_command_queue($context->{from}, $cmd, $delay, $repeating); + $self->{pbot}->{interpreter}->add_to_command_queue($context->{from}, $cmd, $seconds, $repeating); + return "Command added to event queue."; } @@ -127,44 +127,39 @@ sub cmd_eventqueue { return "Unknown command '$command'. $usage"; } -sub start { - my $self = shift; - $self->{enabled} = 1; - push @timer_funcs, $self->{timer_func}; - alarm 1; -} - -sub stop { - my $self = shift; - $self->{enabled} = 0; - @timer_funcs = grep { $_ != $self->{timer_func} } @timer_funcs; -} - sub find_enqueue_position { - my ($self, $value) = @_; + my ($self, $time) = @_; + # no events in queue yet, early-return first position return 0 if not @{$self->{event_queue}}; - if ($value < $self->{event_queue}->[0]->{timeout}) { + # early-return first position if event's time is less + # than first position's + if ($time < $self->{event_queue}->[0]->{timeout}) { return 0; } - if ($value > $self->{event_queue}->[@{$self->{event_queue}} - 1]->{timeout}) { + # early-return last position if event's time is greater + if ($time > $self->{event_queue}->[@{$self->{event_queue}} - 1]->{timeout}) { return scalar @{$self->{event_queue}}; } + # binary search to find enqueue position + my $lo = 0; my $hi = scalar @{$self->{event_queue}} - 1; while ($lo <= $hi) { my $mid = int (($hi + $lo) / 2); - if ($value < $self->{event_queue}->[$mid]->{timeout}) { + if ($time < $self->{event_queue}->[$mid]->{timeout}) { $hi = $mid - 1; - } elsif ($value > $self->{event_queue}->[$mid]->{timeout}) { + } elsif ($time > $self->{event_queue}->[$mid]->{timeout}) { $lo = $mid + 1; } else { - while ($mid < @{$self->{event_queue}} and $self->{event_queue}->[$mid]->{timeout} == $value) { + while ($mid < @{$self->{event_queue}} and $self->{event_queue}->[$mid]->{timeout} == $time) { + # found a slot with the same time. we "slide" down the array + # to append this event to the end of this region of same-times. $mid++; } return $mid; @@ -175,57 +170,64 @@ sub find_enqueue_position { } sub replace_subref_or_enqueue_event { - my ($self, $ref, $interval, $id, $repeating) = @_; + my ($self, $subref, $interval, $id, $repeating) = @_; + # find events matching id my @events = grep { $_->{id} eq $id } @{$self->{event_queue}}; + # no events found, enqueue new event if (not @events) { - $self->enqueue_event($ref, $interval, $id, $repeating); + $self->enqueue_event($subref, $interval, $id, $repeating); return; } + # otherwise update existing events foreach my $event (@events) { - $event->{subref} = $ref; + $event->{subref} = $subref; } } sub replace_or_enqueue_event { - my ($self, $ref, $interval, $id, $repeating) = @_; + my ($self, $subref, $interval, $id, $repeating) = @_; + + # remove event if it exists $self->dequeue_event($id) if $self->exists($id); - $self->enqueue_event($ref, $interval, $id, $repeating); + + # enqueue new event + $self->enqueue_event($subref, $interval, $id, $repeating); } sub enqueue_event_unless_exists { - my ($self, $ref, $interval, $id, $repeating) = @_; + my ($self, $subref, $interval, $id, $repeating) = @_; + + # event already exists, bail out return if $self->exists($id); - $self->enqueue_event($ref, $interval, $id, $repeating); + + # enqueue new event + $self->enqueue_event($subref, $interval, $id, $repeating); } sub enqueue_event { - my ($self, $ref, $interval, $id, $repeating) = @_; + my ($self, $subref, $interval, $id, $repeating) = @_; - $id //= 'anonymous event'; + $id //= 'an event'; $repeating //= 0; $interval //= 0; my $event = { id => $id, - subref => $ref, + subref => $subref, interval => $interval, - timeout => $seconds + $interval, + timeout => time + $interval, repeating => $repeating, }; my $i = $self->find_enqueue_position($event->{timeout}); splice @{$self->{event_queue}}, $i, 0, $event; - if ($interval < $waitfor) { - $self->waitfor($interval); - } - - my $debug = $self->{pbot}->{registry}->get_value('timer', 'debug') // 0; + my $debug = $self->{pbot}->{registry}->get_value('eventqueue', 'debug') // 0; if ($debug > 1) { - $self->{pbot}->{logger}->log("Enqueued new timer event $id at position $i: timeout=$event->{timeout} interval=$interval repeating=$repeating\n"); + $self->{pbot}->{logger}->log("Enqueued new event $id at position $i: timeout=$event->{timeout} interval=$interval repeating=$repeating\n"); } } @@ -267,12 +269,14 @@ sub execute_and_dequeue_event { return $self->dequeue_event($id, 1); } -sub register { - my ($self, $ref, $interval, $id) = @_; - $self->enqueue_event($ref, $interval, $id, 1); +# adds event with repeating defaulting to enabled +sub enqueue { + my ($self, $subref, $interval, $id, $repeating) = @_; + $repeating //= 1; + $self->enqueue_event($subref, $interval, $id, $repeating); } -sub unregister { +sub dequeue { my ($self, $id) = @_; $self->dequeue_event($id); } @@ -299,8 +303,11 @@ sub update_interval { for (my $i = 0; $i < @{$self->{event_queue}}; $i++) { if ($self->{event_queue}->[$i]->{id} eq $id) { if ($dont_enqueue) { + # update interval in-place without moving event to new place in queue + # (allows event to fire at expected time, then updates to new timeout afterwards) $self->{event_queue}->[$i]->{interval} = $interval; } else { + # remove and add event in new position in queue my $event = splice(@{$self->{event_queue}}, $i, 1); $self->enqueue_event($event->{subref}, $interval, $id, $event->{repeating}); } @@ -309,60 +316,57 @@ sub update_interval { } } -sub waitfor { - my ($self, $duration) = @_; - $duration = 1 if $duration < 1; - alarm $duration; - $waitfor = $duration; +sub duration_until_next_event { + my ($self) = @_; + return 0 if not @{$self->{event_queue}}; + return $self->{event_queue}->[0]->{timeout} - time; } -sub on_tick_handler { +# invokes the next event, if ready, and then returns seconds until next event + +sub do_next_event { my ($self) = @_; - return if not $self->{enabled}; - my $debug = $self->{pbot}->{registry}->get_value('timer', 'debug') // 0; - $self->{pbot}->{logger}->log("$self->{name} tick $seconds\n") if $debug; + # early-return if no events available + return 0 if not @{$self->{event_queue}}; - if (@{$self->{event_queue}}) { - my $next_tick = 1; - my @enqueue = (); - for (my $i = 0; $i < @{$self->{event_queue}}; $i++) { - if ($seconds >= $self->{event_queue}->[$i]->{timeout}) { - my $event = $self->{event_queue}->[$i]; - $self->{pbot}->{logger}->log("Processing timer event $i: $event->{id}\n") if $debug > 1; - $event->{subref}->($event); - splice @{$self->{event_queue}}, $i--, 1; - push @enqueue, $event if $event->{repeating}; - } else { - if ($debug > 2) { - $self->{pbot}->{logger}->log("Event not ready yet: $self->{event_queue}->[$i]->{id} (timeout=$self->{event_queue}->[$i]->{timeout})\n"); - } + my $debug = $self->{pbot}->{registry}->get_value('eventqueue', 'debug') // 0; - $next_tick = $self->{event_queue}->[$i]->{timeout} - $seconds; - last; + # repeating events to re-enqueue + my @enqueue; + + for (my $i = 0; $i < @{$self->{event_queue}}; $i++) { + # we call time for a fresh time, instead of using a stale $now that + # could be well in the past depending on a previous event's duration + if (time >= $self->{event_queue}->[$i]->{timeout}) { + my $event = $self->{event_queue}->[$i]; + + $self->{pbot}->{logger}->log("Processing event $i: $event->{id}\n") if $debug > 1; + + # call event's subref, passing event as argument + $event->{subref}->($event); + + # remove event from queue + splice @{$self->{event_queue}}, $i--, 1; + + # add event to re-enqueue queue if repeating + push @enqueue, $event if $event->{repeating}; + } else { + # no more events ready at this time + if ($debug > 2) { + $self->{pbot}->{logger}->log("Event not ready yet: $self->{event_queue}->[$i]->{id} (timeout=$self->{event_queue}->[$i]->{timeout})\n"); } - } - $self->waitfor($next_tick); - - foreach my $event (@enqueue) { - $self->enqueue_event($event->{subref}, $event->{interval}, $event->{id}, 1); + last; } - } else { - # no queued events, call default overridable on_tick() method if timeout has elapsed - if ($seconds - $self->{last} >= $self->{timeout}) { - $self->{last} = $seconds; - $self->on_tick; - } - - $self->waitfor($self->{timeout} - $seconds - $self->{last}); } -} -# default overridable handler, executed whenever timeout is triggered -sub on_tick { - my ($self) = @_; - $self->{pbot}->{logger}->log("Tick! $self->{name} $self->{timeout} $self->{last} $seconds\n"); + # re-enqueue repeating events + foreach my $event (@enqueue) { + $self->enqueue_event($event->{subref}, $event->{interval}, $event->{id}, 1); + } + + return $self->duration_until_next_event; } 1; diff --git a/PBot/IRCHandlers.pm b/PBot/IRCHandlers.pm index a82c7c91..eeae8fad 100644 --- a/PBot/IRCHandlers.pm +++ b/PBot/IRCHandlers.pm @@ -64,7 +64,8 @@ sub initialize { $self->{pbot}->{event_dispatcher}->register_handler('pbot.join', sub { $self->on_self_join(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('pbot.part', sub { $self->on_self_part(@_) }); - $self->{pbot}->{timer}->register(sub { $self->check_pending_whos }, 10, 'Check Pending Whos'); + # TODO: enqueue these events as needed instead of naively checking every 10 seconds + $self->{pbot}->{event_queue}->enqueue(sub { $self->check_pending_whos }, 10, 'Check pending WHOs'); } sub default_handler { diff --git a/PBot/IgnoreList.pm b/PBot/IgnoreList.pm index 4e0befe7..ec09178b 100644 --- a/PBot/IgnoreList.pm +++ b/PBot/IgnoreList.pm @@ -98,7 +98,7 @@ sub enqueue_ignores { my $interval = $timeout - $now; $interval = 0 if $interval < 0; - $self->{pbot}->{timer}->enqueue_event(sub { + $self->{pbot}->{event_queue}->enqueue_event(sub { $self->remove($channel, $hostmask); }, $interval, "ignore_timeout $channel $hostmask" ); @@ -136,9 +136,9 @@ sub add { $self->{ignorelist}->add($channel, $hostmask, $data); if ($length > 0) { - $self->{pbot}->{timer}->dequeue_event("ignore_timeout $channel $hostmask"); + $self->{pbot}->{event_queue}->dequeue_event("ignore_timeout $channel $hostmask"); - $self->{pbot}->{timer}->enqueue_event(sub { + $self->{pbot}->{event_queue}->enqueue_event(sub { $self->remove($channel, $hostmask); }, $length, "ignore_timeout $channel $hostmask" ); @@ -159,7 +159,7 @@ sub remove { $channel = '.*' if $channel !~ /^#/; - $self->{pbot}->{timer}->dequeue_event("ignore_timeout $channel $hostmask"); + $self->{pbot}->{event_queue}->dequeue_event("ignore_timeout $channel $hostmask"); return $self->{ignorelist}->remove($channel, $hostmask); } diff --git a/PBot/Interpreter.pm b/PBot/Interpreter.pm index b2bbeafa..143b36b5 100644 --- a/PBot/Interpreter.pm +++ b/PBot/Interpreter.pm @@ -887,7 +887,7 @@ sub output_result { sub add_message_to_output_queue { my ($self, $channel, $message, $delay) = @_; - $self->{pbot}->{timer}->enqueue_event( + $self->{pbot}->{event_queue}->enqueue_event( sub { my $context = { from => $channel, @@ -909,7 +909,7 @@ sub add_message_to_output_queue { sub add_to_command_queue { my ($self, $channel, $command, $delay, $repeating) = @_; - $self->{pbot}->{timer}->enqueue_event( + $self->{pbot}->{event_queue}->enqueue_event( sub { my $context = { from => $channel, diff --git a/PBot/LagChecker.pm b/PBot/LagChecker.pm index 3ec4a992..046767ca 100644 --- a/PBot/LagChecker.pm +++ b/PBot/LagChecker.pm @@ -45,8 +45,8 @@ sub initialize { # registry trigger for lag_history_interval changes $self->{pbot}->{registry}->add_trigger('lagchecker', 'lag_history_interval', sub { $self->trigger_lag_history_interval(@_) }); - # timer to send PINGs - $self->{pbot}->{timer}->register( + # enqueue repeating event to send PINGs + $self->{pbot}->{event_queue}->enqueue( sub { $self->send_ping }, $self->{pbot}->{registry}->get_value('lagchecker', 'lag_history_interval'), 'lag check' @@ -62,7 +62,7 @@ sub initialize { # registry trigger fires when value changes sub trigger_lag_history_interval { my ($self, $section, $item, $newvalue) = @_; - $self->{pbot}->{timer}->update_interval('lag check', $newvalue); + $self->{pbot}->{event_queue}->update_interval('lag check', $newvalue); } # lagcheck bot command diff --git a/PBot/MessageHistory_SQLite.pm b/PBot/MessageHistory_SQLite.pm index 6b919a5a..bb415c20 100644 --- a/PBot/MessageHistory_SQLite.pm +++ b/PBot/MessageHistory_SQLite.pm @@ -35,7 +35,7 @@ sub initialize { $self->{pbot}->{registry}->add_trigger('messagehistory', 'sqlite_commit_interval', sub { $self->sqlite_commit_interval_trigger(@_) }); $self->{pbot}->{registry}->add_trigger('messagehistory', 'sqlite_debug', sub { $self->sqlite_debug_trigger(@_) }); - $self->{pbot}->{timer}->register( + $self->{pbot}->{event_queue}->enqueue( sub { $self->commit_message_history }, $self->{pbot}->{registry}->get_value('messagehistory', 'sqlite_commit_interval'), 'messagehistory commit' @@ -47,7 +47,7 @@ sub initialize { sub sqlite_commit_interval_trigger { my ($self, $section, $item, $newvalue) = @_; - $self->{pbot}->{timer}->update_interval('messagehistory commit', $newvalue); + $self->{pbot}->{event_queue}->update_interval('messagehistory commit', $newvalue); } sub sqlite_debug_trigger { diff --git a/PBot/MiscCommands.pm b/PBot/MiscCommands.pm index 0e376792..26cf6406 100644 --- a/PBot/MiscCommands.pm +++ b/PBot/MiscCommands.pm @@ -191,8 +191,8 @@ sub cmd_reload { }, 'banlist' => sub { - $self->{pbot}->{timer}->dequeue_event('unban #.*'); - $self->{pbot}->{timer}->dequeue_event('unmute #.*'); + $self->{pbot}->{event_queue}->dequeue_event('unban #.*'); + $self->{pbot}->{event_queue}->dequeue_event('unmute #.*'); $self->{pbot}->{banlist}->{banlist}->load; $self->{pbot}->{banlist}->{quietlist}->load; $self->{pbot}->{banlist}->enqueue_timeouts($self->{pbot}->{banlist}->{banlist}, 'b'); diff --git a/PBot/PBot.pm b/PBot/PBot.pm index 8c50d25c..39687864 100644 --- a/PBot/PBot.pm +++ b/PBot/PBot.pm @@ -37,6 +37,7 @@ use PBot::ChanOps; use PBot::DualIndexHashObject; use PBot::DualIndexSQLiteObject; use PBot::EventDispatcher; +use PBot::EventQueue; use PBot::Factoids; use PBot::Functions; use PBot::HashObject; @@ -55,7 +56,6 @@ use PBot::Registry; use PBot::Refresher; use PBot::SelectHandler; use PBot::StdinReader; -use PBot::Timer; use PBot::Updater; use PBot::Users; use PBot::Utils::ParseDate; @@ -178,7 +178,7 @@ sub initialize { $self->{irc} = PBot::IRC->new; # prepare remaining core PBot modules -- do not change this order - $self->{timer} = PBot::Timer->new(pbot => $self, timeout => 10, name => 'PBot Timer', %conf); + $self->{event_queue} = PBot::EventQueue->new(pbot => $self, name => 'PBot event queue', %conf); $self->{event_dispatcher} = PBot::EventDispatcher->new(pbot => $self, %conf); $self->{users} = PBot::Users->new(pbot => $self, filename => "$conf{data_dir}/users", %conf); $self->{antiflood} = PBot::AntiFlood->new(pbot => $self, %conf); @@ -213,9 +213,9 @@ sub initialize { # -- this must happen last after all modules have registered their capabilities -- $self->{capabilities}->rebuild_botowner_capabilities; - # flush all pending save events to disk at exit + # fire all pending save events at exit $self->{atexit}->register(sub { - $self->{timer}->execute_and_dequeue_event('save *'); + $self->{event_queue}->execute_and_dequeue_event('save .*'); return; } ); @@ -267,9 +267,6 @@ sub connect { $self->{connected} = 1; - # start timer once connected - $self->{timer}->start; - # set up handlers for the IRC engine $self->{conn}->add_default_handler(sub { $self->{irchandlers}->default_handler(@_) }, 1); $self->{conn}->add_handler([251, 252, 253, 254, 255, 302], sub { $self->{irchandlers}->on_init(@_) }); @@ -308,7 +305,6 @@ sub register_signal_handlers { sub atexit { my $self = shift; $self->{atexit}->execute_all; - alarm 0; if (exists $self->{logger}) { $self->{logger}->log("Good-bye.\n"); } else { @@ -335,7 +331,16 @@ sub exit { # main loop sub do_one_loop { my ($self) = @_; - $self->{irc}->do_one_loop(); + + # do an irc engine loop (select, eventqueues, etc) + $self->{irc}->do_one_loop; + + # check for an available PBot event (returns seconds until next event) + my $waitfor = $self->{event_queue}->do_next_event; + + # tell irc select loop to sleep for this many seconds + # (or until its own internal eventqueue has an event) + $self->{irc}->timeout($waitfor); } # main entry point diff --git a/PBot/ProcessManager.pm b/PBot/ProcessManager.pm index 8d2acc62..c3c1259a 100644 --- a/PBot/ProcessManager.pm +++ b/PBot/ProcessManager.pm @@ -243,13 +243,12 @@ sub execute_process { # remove atexit handlers $self->{pbot}->{atexit}->unregister_all; - # FIXME: close databases and files too? Or just set everything to check for $self->{pbot}->{child} == 1 or $context->{pid} == 0? - # execute the provided subroutine, results are stored in $context eval { local $SIG{ALRM} = sub { die "Process `$context->{commands}->[0]` timed-out" }; alarm $timeout; $subref->($context); + alarm 0; }; # check for errors @@ -265,9 +264,6 @@ sub execute_process { $context->{result} =~ s/\s+...propagated at .*$//ms; } - # turn alarm back on for PBot::Timer - alarm 1; - # print $context to pipe my $json = encode_json $context; print $writer "$json\n"; diff --git a/PBot/WebPaste.pm b/PBot/WebPaste.pm index d41470fb..0078c8e3 100644 --- a/PBot/WebPaste.pm +++ b/PBot/WebPaste.pm @@ -82,8 +82,6 @@ sub paste { $result =~ s/^\s+|\s+$//g; - alarm 1; # LWP::UserAgent::Paranoid kills alarm - return $result; } diff --git a/Plugins/AntiRepeat.pm b/Plugins/AntiRepeat.pm index 6d07dbf4..5dc223fe 100644 --- a/Plugins/AntiRepeat.pm +++ b/Plugins/AntiRepeat.pm @@ -26,7 +26,7 @@ sub initialize { sub unload { my $self = shift; - $self->{pbot}->{timer}->dequeue_event('antirepeat .*'); + $self->{pbot}->{event_queue}->dequeue_event('antirepeat .*'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.public'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.caction'); } @@ -112,7 +112,7 @@ sub on_public { $self->{offenses}->{$account}->{$channel}->{last_offense} = gettimeofday; $self->{offenses}->{$account}->{$channel}->{offenses}++; - $self->{pbot}->{timer}->enqueue_event(sub { + $self->{pbot}->{event_queue}->enqueue_event(sub { my ($event) = @_; $self->{offenses}->{$account}->{$channel}->{offenses}--; diff --git a/Plugins/AntiTwitter.pm b/Plugins/AntiTwitter.pm index bb68d488..cb429c35 100644 --- a/Plugins/AntiTwitter.pm +++ b/Plugins/AntiTwitter.pm @@ -24,7 +24,7 @@ sub initialize { sub unload { my ($self) = @_; $self->{pbot}->{event_dispatcher}->remove_handler('irc.public'); - $self->{pbot}->{timer}->dequeue_event('antitwitter .*'); + $self->{pbot}->{event_queue}->dequeue_event('antitwitter .*'); } sub on_public { @@ -68,7 +68,7 @@ sub on_public { ); $self->{pbot}->{chanops}->gain_ops($channel); - $self->{pbot}->{timer}->enqueue_event(sub { + $self->{pbot}->{event_queue}->enqueue_event(sub { my ($event) = @_; if (--$self->{offenses}->{$channel}->{$nick}->{offenses} <= 0) { delete $self->{offenses}->{$channel}->{$nick}; diff --git a/Plugins/Battleship.pm b/Plugins/Battleship.pm index b44f1b66..f5452555 100644 --- a/Plugins/Battleship.pm +++ b/Plugins/Battleship.pm @@ -34,7 +34,7 @@ sub initialize { sub unload { my $self = shift; $self->{pbot}->{commands}->unregister('battleship'); - $self->{pbot}->{timer}->dequeue_event('battleship loop'); + $self->{pbot}->{event_queue}->dequeue_event('battleship loop'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.part'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.quit'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.kick'); @@ -124,7 +124,7 @@ sub cmd_battleship { $player = {id => -1, name => undef, missedinputs => 0}; push @{$self->{state_data}->{players}}, $player; - $self->{pbot}->{timer}->enqueue_event(sub { + $self->{pbot}->{event_queue}->enqueue_event(sub { $self->run_one_state; }, 1, 'battleship loop', 1 ); @@ -147,7 +147,7 @@ sub cmd_battleship { $player = {id => $id, name => $challengee, missedinputs => 0}; push @{$self->{state_data}->{players}}, $player; - $self->{pbot}->{timer}->enqueue_event(sub { + $self->{pbot}->{event_queue}->enqueue_event(sub { $self->battleship_loop; }, 1, 'battleship loop', 1 ); @@ -965,7 +965,7 @@ sub show_battlefield { sub nogame { my ($self, $state) = @_; $state->{result} = 'nogame'; - $self->{pbot}->{timer}->update_repeating('battleship loop', 0); + $self->{pbot}->{event_queue}->update_repeating('battleship loop', 0); return $state; } diff --git a/Plugins/Connect4.pm b/Plugins/Connect4.pm index 42be925e..838816d1 100644 --- a/Plugins/Connect4.pm +++ b/Plugins/Connect4.pm @@ -32,7 +32,7 @@ sub unload { $self->{pbot}->{event_dispatcher}->remove_handler('irc.part'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.quit'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.kick'); - $self->{pbot}->{timer}->dequeue_event('connect4 loop'); + $self->{pbot}->{event_queue}->dequeue_event('connect4 loop'); } sub on_kick { @@ -156,7 +156,7 @@ sub cmd_connect4 { $self->{current_state} = 'accept'; $self->{state_data} = {players => [], counter => 0}; - $self->{pbot}->{timer}->enqueue_event( + $self->{pbot}->{event_queue}->enqueue_event( sub { $self->run_one_state; }, 1, 'connect4 loop', 1 @@ -185,7 +185,7 @@ sub cmd_connect4 { $self->{current_state} = 'accept'; $self->{state_data} = {players => [], counter => 0}; - $self->{pbot}->{timer}->enqueue_event( + $self->{pbot}->{event_queue}->enqueue_event( sub { $self->run_one_state; }, 1, 'connect4 loop', 1 @@ -693,7 +693,7 @@ sub show_board { sub nogame { my ($self, $state) = @_; $state->{result} = 'nogame'; - $self->{pbot}->{timer}->update_repeating('connect4 loop', 0); + $self->{pbot}->{event_queue}->update_repeating('connect4 loop', 0); return $state; } diff --git a/Plugins/RelayUnreg.pm b/Plugins/RelayUnreg.pm index 05715b15..3ed0dca4 100644 --- a/Plugins/RelayUnreg.pm +++ b/Plugins/RelayUnreg.pm @@ -14,12 +14,12 @@ sub initialize { $self->{pbot}->{event_dispatcher}->register_handler('irc.public', sub { $self->on_public(@_) }); $self->{queue} = []; $self->{notified} = {}; - $self->{pbot}->{timer}->register(sub { $self->check_queue }, 1, 'RelayUnreg'); + $self->{pbot}->{event_queue}->enqueue(sub { $self->check_queue }, 1, 'RelayUnreg'); } sub unload { my $self = shift; - $self->{pbot}->{timer}->unregister('RelayUnreg'); + $self->{pbot}->{event_queue}->dequeue('RelayUnreg'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.public'); } diff --git a/Plugins/RemindMe.pm b/Plugins/RemindMe.pm index 57a27a7f..51a560b4 100644 --- a/Plugins/RemindMe.pm +++ b/Plugins/RemindMe.pm @@ -25,7 +25,7 @@ sub unload { my $self = shift; $self->dbi_end; $self->{pbot}->{commands}->unregister('remindme'); - $self->{pbot}->{timer}->dequeue_event('reminder .*'); + $self->{pbot}->{event_queue}->dequeue_event('reminder .*'); } sub enqueue_reminders { @@ -56,7 +56,7 @@ sub enqueue_reminders { $timeout = 10 if $timeout < 10; my $repeating = $reminder->{repeat}; - $self->{pbot}->{timer}->enqueue_event( + $self->{pbot}->{event_queue}->enqueue_event( sub { my ($event) = @_; $self->do_reminder($reminder->{id}, $event); @@ -126,7 +126,7 @@ sub add_reminder { return 0; } - $self->{pbot}->{timer}->enqueue_event( + $self->{pbot}->{event_queue}->enqueue_event( sub { my ($event) = @_; $self->do_reminder($id, $event); @@ -151,7 +151,7 @@ sub delete_reminder { return 0; } - $self->{pbot}->{timer}->dequeue_event("reminder $id"); + $self->{pbot}->{event_queue}->dequeue_event("reminder $id"); return 1; } diff --git a/Plugins/Spinach.pm b/Plugins/Spinach.pm index c641a2e9..071dce3f 100644 --- a/Plugins/Spinach.pm +++ b/Plugins/Spinach.pm @@ -67,7 +67,7 @@ sub initialize { sub unload { my $self = shift; $self->{pbot}->{commands}->unregister('spinach'); - $self->{pbot}->{timer}->dequeue_event('spinach loop'); + $self->{pbot}->{event_queue}->dequeue_event('spinach loop'); $self->{stats}->end if $self->{stats_running}; $self->{pbot}->{event_dispatcher}->remove_handler('irc.part'); $self->{pbot}->{event_dispatcher}->remove_handler('irc.quit'); @@ -336,7 +336,7 @@ sub cmd_spinach { $self->{state_data} = { players => [], counter => 0 }; $self->{current_state} = 'getplayers'; - $self->{pbot}->{timer}->enqueue_event(sub { + $self->{pbot}->{event_queue}->enqueue_event(sub { $self->run_one_state; }, 1, 'spinach loop', 1 ); @@ -409,7 +409,7 @@ sub cmd_spinach { if (not @{$self->{state_data}->{players}}) { $self->{current_state} = 'nogame'; - $self->{pbot}->{timer}->update_repeating('spinach loop', 0); + $self->{pbot}->{event_queue}->update_repeating('spinach loop', 0); return "/msg $self->{channel} $context->{nick} has left the game! All players have left. The game has been stopped."; } else { return "/msg $self->{channel} $context->{nick} has left the game!"; @@ -935,7 +935,7 @@ sub run_one_state { if (not @{$self->{state_data}->{players}}) { $self->send_message($self->{channel}, "All players have left the game!"); $self->{current_state} = 'nogame'; - $self->{pbot}->{timer}->update_repeating('spinach loop', 0); + $self->{pbot}->{event_queue}->update_repeating('spinach loop', 0); } } @@ -945,7 +945,7 @@ sub run_one_state { if (not defined $self->{current_state}) { $self->{pbot}->{logger}->log("Spinach state broke.\n"); $self->{current_state} = 'nogame'; - $self->{pbot}->{timer}->update_repeating('spinach loop', 0); + $self->{pbot}->{event_queue}->update_repeating('spinach loop', 0); return; } @@ -2152,7 +2152,7 @@ sub nogame { delete $self->{stats_running}; } $state->{result} = 'nogame'; - $self->{pbot}->{timer}->update_repeating('spinach loop', 0); + $self->{pbot}->{event_queue}->update_repeating('spinach loop', 0); return $state; } @@ -2205,7 +2205,7 @@ sub getplayers { $self->send_message($self->{channel}, "All players have left the queue. The game has been stopped."); $self->{current_state} = 'nogame'; $self->{result} = 'nogame'; - $self->{pbot}->{timer}->update_repeating('spinach loop', 0); + $self->{pbot}->{event_queue}->update_repeating('spinach loop', 0); return $state; } }