1=head1 NAME
2
3AnyEvent::SNMP - adaptor to integrate Net::SNMP into AnyEvent.
4
5=head1 SYNOPSIS
6
7 use AnyEvent::SNMP;
8 use Net::SNMP;
9
10 # just use Net::SNMP and AnyEvent as you like:
11
12 # use a condvar to transfer results, this is
13 # just an example, you can use a naked callback as well.
14 my $cv = AnyEvent->condvar;
15
16 # ... start non-blocking snmp request(s)...
17 Net::SNMP->session (-hostname => "127.0.0.1",
18                     -community => "public",
19                     -nonblocking => 1)
20          ->get_request (-callback => sub { $cv->send (@_) });
21
22 # ... do something else until the result is required
23 my @result = $cv->wait;
24
25=head1 DESCRIPTION
26
27This module implements an alternative "event dispatcher" for Net::SNMP,
28using AnyEvent as a backend. This integrates Net::SNMP into AnyEvent. That
29means you can make non-blocking Net::SNMP calls and as long as other
30parts of your program also use AnyEvent (or some event loop supported by
31AnyEvent), they will run in parallel.
32
33Also, the Net::SNMP scheduler is very inefficient with respect to both CPU
34and memory usage. Most AnyEvent backends (including the pure-perl backend)
35fare much better than the Net::SNMP dispatcher.
36
37Another major added fetaure of this module over Net::SNMP is automatic
38rate-adjustments:  Net::SNMP is so slow that firing a few thousand
39requests can cause many timeouts simply because Net::SNMP cannot process
40the replies in time. This module automatically adapts the send rate to
41avoid false timeouts caused by slow reply processing.
42
43A potential disadvantage of this module is that replacing the dispatcher
44is not at all a documented thing to do, so future changes in Net::SNP
45might break this module (or the many similar ones).
46
47This module does not export anything and does not require you to do
48anything special apart from loading it I<before doing any non-blocking
49requests with Net::SNMP>. It is recommended but not required to load this
50module before C<Net::SNMP>.
51
52=head1 GLOBAL VARIABLES
53
54=over 4
55
56=item $AnyEvent::SNMP::MAX_OUTSTANDING (default: C<50>, dynamic)
57
58=item AnyEvent::SNMP::set_max_outstanding $new_value
59
60Use this package variable to restrict the number of outstanding SNMP
61requests at any point in time.
62
63Net::SNMP is very fast at creating and sending SNMP requests, but much
64slower at parsing (big, bulk) responses. This makes it easy to request a
65lot of data that can take many seconds to parse.
66
67In the best case, this can lead to unnecessary delays (and even time-outs,
68as the data has been received but not yet processed) and in the worst
69case, this can lead to packet loss, when the receive queue overflows and
70the kernel can no longer accept new packets.
71
72To avoid this, you can (and should) limit the number of outstanding
73requests to a number low enough so that parsing time doesn't introduce
74noticable delays.
75
76Unfortunately, this number depends not only on processing speed and load
77of the machine running Net::SNMP, but also on the network latency and the
78speed of your SNMP agents.
79
80AnyEvent::SNMP tries to dynamically adjust this number upwards and
81downwards.
82
83Increasing C<$MAX_OUTSTANDING> will not automatically use the
84extra request slots. To increase C<$MAX_OUTSTANDING> and make
85C<AnyEvent::SNMP> make use of the extra paralellity, call
86C<AnyEvent::SNMP::set_max_outstanding> with the new value, e.g.:
87
88   AnyEvent::SNMP::set_max_outstanding 500;
89
90Although due to the dynamic adjustment, this might have little lasting
91effect.
92
93Note that you can use L<Net::SNMP::XS> to speed up parsing of responses
94considerably.
95
96=item $AnyEvent::SNMP::MIN_RECVQUEUE (default: C<8>)
97
98=item $AnyEvent::SNMP::MAX_RECVQUEUE (default: C<64>)
99
100These values specify the minimum and maximum receive queue length (in
101units of one response packet).
102
103When AnyEvent::SNMP handles $MAX_RECVQUEUE or more packets per iteration
104it will reduce $MAX_OUTSTANDING. If it handles less than $MIN_RECVQUEUE,
105it increases $MAX_OUTSTANDING.
106
107This has the result of adjusting the number of outstanding requests so that
108the recv queue is between the minimum and maximu, usually.
109
110This algorithm works reasonably well as long as the responses, response
111latencies and processing times are the same size per packet on average.
112
113=back
114
115=head1 COMPATIBILITY
116
117This module may be used as a drop in replacement for the
118Net::SNMP::Dispatcher in existing programs. You can still call
119C<snmp_dispatcher> to start the event-loop, but then you loose the benefit
120of mixing Net::SNMP events with other events.
121
122   use AnyEvent::SNMP;
123   use Net::SNMP;
124
125   # just use Net::SNMP as before
126
127   # ... start non-blocking snmp request(s)...
128   Net::SNMP->session (
129         -hostname    => "127.0.0.1",
130         -community   => "public",
131         -nonblocking => 1,
132      )->get_request (-callback => sub { ... });
133
134   snmp_dispatcher;
135
136=cut
137
138package AnyEvent::SNMP;
139
140use common::sense;
141
142# it is possible to do this without loading
143# Net::SNMP::Dispatcher, but much more awkward.
144use Net::SNMP::Dispatcher;
145
146# we could inherit fro Net:SNMP::Dispatcher, but since this is undocumented,
147# I'd rather see it die (and reported) than silenty and subtly fail.
148*msg_handle_alloc = \&Net::SNMP::Dispatcher::msg_handle_alloc;
149
150sub Net::SNMP::Dispatcher::instance {
151   AnyEvent::SNMP::
152}
153
154use Net::SNMP ();
155use AnyEvent ();
156
157our $VERSION = '6.0';
158
159$Net::SNMP::DISPATCHER = instance Net::SNMP::Dispatcher;
160
161our $MESSAGE_PROCESSING = $Net::SNMP::Dispatcher::MESSAGE_PROCESSING;
162
163our $BUSY;
164our $DONE; # finished all jobs
165our @TRANSPORT; # fileno => [count, watcher]
166our @QUEUE;
167our $MAX_OUTSTANDING = 50;
168our $MIN_RECVQUEUE   =  8;
169our $MAX_RECVQUEUE   = 64;
170
171sub kick_job;
172
173sub _send_pdu {
174   my ($pdu, $retries) = @_;
175
176   # mostly copied from Net::SNMP::Dispatch
177
178   # Pass the PDU to Message Processing so that it can
179   # create the new outgoing message.
180   my $msg = $MESSAGE_PROCESSING->prepare_outgoing_msg ($pdu);
181
182   if (!defined $msg) {
183      --$BUSY;
184      kick_job;
185      # Inform the command generator about the Message Processing error.
186      $pdu->status_information ($MESSAGE_PROCESSING->error);
187      return;
188   }
189
190   # Actually send the message.
191   if (!defined $msg->send) {
192      $MESSAGE_PROCESSING->msg_handle_delete ($pdu->msg_id)
193         if $pdu->expect_response;
194
195      # A crude attempt to recover from temporary failures.
196      if ($retries-- > 0 && ($!{EAGAIN} || $!{EWOULDBLOCK} || $!{ENOSPC})) {
197         my $retry_w; $retry_w = AE::timer $pdu->timeout, 0, sub {
198            undef $retry_w;
199            _send_pdu ($pdu, $retries);
200         };
201      } else {
202         --$BUSY;
203         kick_job;
204      }
205
206      # Inform the command generator about the send() error.
207      $pdu->status_information ($msg->error);
208      return;
209   }
210
211   # Schedule the timeout handler if the message expects a response.
212   if ($pdu->expect_response) {
213      my $transport = $msg->transport;
214      my $fileno    = $transport->fileno;
215
216      # register the transport
217      unless ($TRANSPORT[$fileno][0]++) {
218         $TRANSPORT[$fileno][1] = AE::io $transport->socket, 0, sub {
219            for my $count (1..$MAX_RECVQUEUE) { # handle up to this many requests in one go
220               # Create a new Message object to receive the response
221               my ($msg, $error) = Net::SNMP::Message->new (-transport => $transport);
222
223               if (!defined $msg) {
224                  die sprintf 'Failed to create Message object [%s]', $error;
225               }
226
227               # Read the message from the Transport Layer
228               if (!defined $msg->recv) {
229                  if ($transport->connectionless) {
230                     # if we handled very few replies and we have queued work, try
231                     # to increase the parallelity as we probably can handle more.
232                     if ($count < $MIN_RECVQUEUE && @QUEUE) {
233                        ++$MAX_OUTSTANDING;
234                        kick_job;
235                     }
236                  } else {
237                     # for some reason, connected-oriented transports seem to need this
238                     delete $TRANSPORT[$fileno]
239                        unless --$TRANSPORT[$fileno][0];
240                  }
241
242                  $msg->error;
243                  return;
244               }
245
246               # For connection-oriented Transport Domains, it is possible to
247               # "recv" an empty buffer if reassembly is required.
248               if (!$msg->length) {
249                  return;
250               }
251
252               # Hand the message over to Message Processing.
253               if (!defined $MESSAGE_PROCESSING->prepare_data_elements ($msg)) {
254                  $MESSAGE_PROCESSING->error;
255                  return;
256               }
257
258               # Set the error if applicable.
259               $msg->error ($MESSAGE_PROCESSING->error) if $MESSAGE_PROCESSING->error;
260
261               # Notify the command generator to process the response.
262               $msg->process_response_pdu;
263
264               # Cancel the timeout.
265               my $rtimeout_w = $msg->timeout_id;
266               if ($$rtimeout_w) {
267                  undef $$rtimeout_w;
268
269                  --$BUSY;
270                  kick_job;
271
272                  unless (--$TRANSPORT[$fileno][0]) {
273                     delete $TRANSPORT[$fileno];
274                     return;
275                  }
276               }
277            }
278
279            # when we end up here, we successfully handled $MAX_RECVQUEUE
280            # replies in one iteration, so assume we are overloaded
281            # and reduce the amount of parallelity.
282            $MAX_OUTSTANDING = (int $MAX_OUTSTANDING * 0.95) || 1;
283         };
284      }
285
286      $msg->timeout_id (\(my $rtimeout_w =
287         AE::timer $pdu->timeout, 0, sub {
288            my $rtimeout_w = $msg->timeout_id;
289            if ($$rtimeout_w) {
290               undef $$rtimeout_w;
291               delete $TRANSPORT[$fileno]
292                  unless --$TRANSPORT[$fileno][0];
293            }
294
295            if ($retries--) {
296               _send_pdu ($pdu, $retries);
297            } else {
298               $MESSAGE_PROCESSING->msg_handle_delete ($pdu->msg_id);
299               $pdu->status_information ("No response from remote host '%s'", $pdu->hostname);
300
301               --$BUSY;
302               kick_job;
303            }
304         })
305      );
306   } else {
307     --$BUSY;
308     kick_job;
309   }
310}
311
312sub kick_job {
313   while ($BUSY < $MAX_OUTSTANDING) {
314      my $pdu = shift @QUEUE
315         or last;
316
317      ++$BUSY;
318      _send_pdu $pdu, $pdu->retries;
319   }
320
321   $DONE and $DONE->() unless $BUSY;
322}
323
324sub send_pdu($$$) {
325   my (undef, $pdu, $delay) = @_;
326
327   # $delay is not very sensibly implemented by AnyEvent::SNMP,
328   # but apparently it is not a very sensible feature.
329   if ($delay > 0) {
330      ++$BUSY;
331      my $delay_w; $delay_w = AE::timer $delay, 0, sub {
332         undef $delay_w;
333         push @QUEUE, $pdu;
334         --$BUSY;
335         kick_job;
336      };
337      return 1;
338   }
339
340   push @QUEUE, $pdu;
341   kick_job;
342
343   1
344}
345
346sub loop($) {
347   while ($BUSY) {
348      $DONE = AE::cv;
349      $DONE->recv;
350      undef $DONE;
351   }
352}
353
354*activate = \&loop; # 5.x compatibility?
355*listen   = \&loop; # 5.x compatibility?
356
357sub one_event($) {
358   # should not ever be used
359   AnyEvent->one_event; #d# todo
360}
361
362sub set_max_outstanding($) {
363   $MAX_OUTSTANDING = $_[0];
364   kick_job;
365}
366
367# not provided yet:
368# schedule            # apparently only used by Net::SNMP::Dispatcher itself
369# register            # apparently only used by Net::SNMP::Dispatcher itself
370# deregister          # apparently only used by Net::SNMP::Dispatcher itself
371# cancel              # apparently only used by Net::SNMP::Dispatcher itself
372# return_response_pdu # apparently not used at all?
373# error               # only used by Net::SNMP::Dispatcher itself?
374# debug               # only used by Net::SNMP::Dispatcher itself?
375
376=head1 SEE ALSO
377
378L<AnyEvent>, L<Net::SNMP>, L<Net::SNMP::XS>, L<Net::SNMP::EV>.
379
380=head1 AUTHOR
381
382 Marc Lehmann <schmorp@schmorp.de>
383 http://home.schmorp.de/
384
385=cut
386
3871
388
389