c23bfa185f7f141785da8cc966114d89b8391e7c
[scpubgit/Object-Remote.git] / lib / Object / Remote / MiniLoop.pm
1 package Object::Remote::MiniLoop;
2
3 use IO::Select;
4 use Time::HiRes qw(time);
5 use Object::Remote::Logging qw( :log :dlog );
6 use Moo;
7
8 # this is ro because we only actually set it using local in sub run
9
10 has is_running => (is => 'ro', clearer => 'stop');
11
12 has _read_watches => (is => 'ro', default => sub { {} });
13 has _read_select => (is => 'ro', default => sub { IO::Select->new });
14
15 has _write_watches => (is => 'ro', default => sub { {} });
16 has _write_select => (is => 'ro', default => sub { IO::Select->new });
17
18 has _timers => (is => 'ro', default => sub { [] });
19
20 sub pass_watches_to {
21   my ($self, $new_loop) = @_;
22   log_debug { "passing watches to new run loop" };
23   foreach my $fh ($self->_read_select->handles) {
24     $new_loop->watch_io(
25       handle => $fh,
26       on_read_ready => $self->_read_watches->{$fh}
27     );
28   }
29   foreach my $fh ($self->_write_select->handles) {
30     $new_loop->watch_io(
31       handle => $fh,
32       on_write_ready => $self->_write_watches->{$fh}
33     );
34   }
35 }
36
37 sub watch_io {
38   my ($self, %watch) = @_;
39   my $fh = $watch{handle};
40   Dlog_debug { my $type = ref($fh); "Adding IO watch for $_" } $fh;
41   if (my $cb = $watch{on_read_ready}) {
42     log_trace { "IO watcher is registering with select() for reading" };
43     $self->_read_select->add($fh);
44     $self->_read_watches->{$fh} = $cb;
45   }
46   if (my $cb = $watch{on_write_ready}) {
47     log_trace { "IO watcher is registering with select() for writing" };
48     $self->_write_select->add($fh);
49     $self->_write_watches->{$fh} = $cb;
50   }
51   return;
52 }
53
54 sub unwatch_io {
55   my ($self, %watch) = @_;
56   my $fh = $watch{handle};
57   Dlog_debug { "Removing IO watch for $_" } $fh;
58   if ($watch{on_read_ready}) {
59     log_trace { "IO watcher is removing read from select()" };
60     $self->_read_select->remove($fh);
61     delete $self->_read_watches->{$fh};
62   }
63   if ($watch{on_write_ready}) {
64     log_trace { "IO watcher is removing write from select()" };
65     $self->_write_select->remove($fh);
66     delete $self->_write_watches->{$fh};
67   }
68   return;
69 }
70
71 sub watch_time {
72   my ($self, %watch) = @_;
73   my $at = $watch{at} || do {
74     die "watch_time requires at or after" unless my $after = $watch{after};
75     time() + $after;
76   };
77   die "watch_time requires code" unless my $code = $watch{code};
78   my $timers = $self->_timers;
79   my $new = [ $at => $code ];
80   @{$timers} = sort { $a->[0] <=> $b->[0] } @{$timers}, $new;
81   log_debug { "Created new timer that expires at '$at'" };
82   return "$new";
83 }
84
85 sub unwatch_time {
86   my ($self, $id) = @_;
87   log_debug { "Removing timer with id of '$id'" };
88   @$_ = grep !($_ eq $id), @$_ for $self->_timers;
89   return;
90 }
91
92 sub _next_timer_expires_delay {
93   my ($self) = @_;
94   my $timers = $self->_timers;
95   #undef means no timeout, select only returns
96   #when data is ready - when the system
97   #deadlocks the chatter from the timeout in
98   #select clogs up the logs
99   #TODO should make this an attribute
100   my $delay_max = undef;
101     
102   return $delay_max unless @$timers;
103   my $duration = $timers->[0]->[0] - time;
104
105   log_trace { "next timer fires in '$duration' seconds " };
106   
107   if ($duration < 0) {
108     $duration = 0; 
109   } elsif (defined $delay_max && $duration > $delay_max) {
110     $duration = $delay_max;
111   }
112   
113   #uncomment for original behavior
114   #return .5;    
115   return $duration; 
116 }
117
118 sub loop_once {
119   my ($self) = @_;
120   my $read = $self->_read_watches;
121   my $write = $self->_write_watches;
122   my $read_count = 0;
123   my $write_count = 0; 
124   my @c = caller;
125   my $wait_time = $self->_next_timer_expires_delay;
126   log_debug {  sprintf("Run loop: loop_once() has been invoked by $c[1]:$c[2] with read:%i write:%i select timeout:%s",
127       scalar(keys(%$read)), scalar(keys(%$write)), defined $wait_time ? $wait_time : 'indefinite' ) };
128   #TODO The docs state that select() in some instances can return a socket as ready to
129   #read data even if reading from it would block and the recomendation is to set
130   #handles used with select() as non-blocking but Perl on Windows can not set a 
131   #handle to use non-blocking IO - If Windows is not one of the operating
132   #systems where select() returns a handle that could block it would work to
133   #enable non-blocking mode only under Posix - the non-blocking sysread()
134   #logic would work unmodified for both blocking and non-blocking handles
135   #under Posix and Windows.
136   my ($readable, $writeable) = IO::Select->select(
137     #TODO how come select() isn't used to identify handles with errors on them?
138     #TODO is there a specific reason for a half second maximum wait duration?
139     #The two places I've found for the runloop to be invoked don't return control
140     #to the caller until a controlling variable interrupts the loop that invokes
141     #loop_once() - is this to allow that variable to be polled and exit the
142     #run loop? If so why isn't that behavior event driven and causes select() to
143     #return? 
144     $self->_read_select, $self->_write_select, undef, $wait_time
145   ); 
146   log_debug { 
147     my $readable_count = defined $readable ? scalar(@$readable) : 0;
148     my $writable_count = defined $writeable ? scalar(@$writeable) : 0;
149     "Run loop: select returned readable:$readable_count writeable:$writable_count";
150   };
151   # I would love to trap errors in the select call but IO::Select doesn't
152   # differentiate between an error and a timeout.
153   #   -- no, love, mst.
154   log_trace { "Reading from all ready filehandles" };
155   foreach my $fh (@$readable) {
156     next unless $read->{$fh};
157     $read_count++;
158     $read->{$fh}();
159 #    $read->{$fh}() if $read->{$fh};
160   }
161   log_trace { "Writing to all ready filehandles" };
162   foreach my $fh (@$writeable) {
163     next unless $write->{$fh};
164     $write_count++;
165     $write->{$fh}();
166 #    $write->{$fh}() if $write->{$fh};
167   }
168   log_trace { "Read from $read_count filehandles; wrote to $write_count filehandles" };
169   my $timers = $self->_timers;
170   my $now = time();
171   log_trace { "Checking timers" };
172   while (@$timers and $timers->[0][0] <= $now) {
173     Dlog_debug { "Found timer that needs to be executed: $_" } $timers->[0];
174     (shift @$timers)->[1]->();
175   }
176   log_debug { "Run loop: single loop is completed" };
177   return;
178 }
179
180 #::Node and ::ConnectionServer use the want_run() / want_stop()
181 #counter to cause a run-loop to execute while something is active;
182 #the futures do this via a different mechanism
183 sub want_run {
184   my ($self) = @_;
185   Dlog_debug { "Run loop: Incrimenting want_running, is now $_" }
186     ++$self->{want_running};
187 }
188
189 sub run_while_wanted {
190   my ($self) = @_;
191   log_debug { my $wr = $self->{want_running}; "Run loop: run_while_wanted() invoked; want_running: $wr" };
192   $self->loop_once while $self->{want_running};
193   log_debug { "Run loop: run_while_wanted() completed" };
194   return;
195 }
196
197 sub want_stop {
198   my ($self) = @_;
199   if (! $self->{want_running}) {
200     log_debug { "Run loop: want_stop() was called but want_running was not true" };
201     return; 
202   }
203   Dlog_debug { "Run loop: decrimenting want_running, is now $_" }
204     --$self->{want_running};
205 }
206
207 #TODO Hypothesis: Futures invoke run() which gives that future
208 #it's own localized is_running attribute - any adjustment to the
209 #is_running attribute outside of that future will not effect that
210 #future so each future winds up able to call run() and stop() at 
211 #will with out interfering with each other 
212 sub run {
213   my ($self) = @_;
214   log_info { "Run loop: run() invoked" };
215   local $self->{is_running} = 1;
216   while ($self->is_running) {
217     $self->loop_once;
218   }
219   log_info { "Run loop: run() completed" };
220   return;
221 }
222
223 1;