b72dcbac58b59f4fa743c222c9708f9f0f5d5099
[pingcheck] / src / host / pingscheduler.cpp
1 /*
2 The software in this package is distributed under the GNU General
3 Public License version 2 (with a special exception described below).
4
5 A copy of GNU General Public License (GPL) is included in this distribution,
6 in the file COPYING.GPL.
7
8 As a special exception, if other files instantiate templates or use macros
9 or inline functions from this file, or you compile this file and link it
10 with other works to produce a work based on this file, this file
11 does not by itself cause the resulting work to be covered
12 by the GNU General Public License.
13
14 However the source code for this file must still be made available
15 in accordance with section (3) of the GNU General Public License.
16
17 This exception does not invalidate any other reasons why a work based
18 on this file might be covered by the GNU General Public License.
19 */
20 #include "host/pingscheduler.h"
21
22 #include <iostream>
23 #include <limits>
24
25 #include <boost/bind.hpp>
26 #include <boost/foreach.hpp>
27
28 #include <logfunc.hpp>
29
30 #include "boost_assert_handler.h"
31 #include "host/pingerfactory.h"
32 #include "dns/dnsmaster.h"
33 #include "icmp/icmppinger.h"
34 #include "link/linkstatus.h"
35
36 using namespace std;
37 using boost::asio::io_service;
38 using boost::bind;
39 using boost::date_time::time_resolution_traits_adapted64_impl;
40 using boost::posix_time::microsec_clock;
41 using boost::posix_time::ptime;
42 using boost::posix_time::seconds;
43 using boost::shared_ptr;
44 using I2n::Logger::GlobalLogger;
45
46 //-----------------------------------------------------------------------------
47 // PingScheduler
48 //-----------------------------------------------------------------------------
49
50 /**
51  * @brief Parameterized constructor.
52  *
53  * @param io_serv The one @c io_serv object that controls async processing
54  * @param network_interface The name of the network interface sending the pings.
55  * @param destination_address The remote address to ping.
56  * @param destination_port The remote port to ping.
57  * @param ping_protocol_list A list of protocols to use.
58  * @param ping_interval_in_sec Amount of time between each ping.
59  * @param ping_fail_percentage_limit Maximum amount of pings that can fail.
60  * @param link_analyzer The object to monitor the link status.
61  * @param first_delay Delay in seconds from start_pinging to first ping attempt
62  */
63 PingScheduler::PingScheduler(
64         const IoServiceItem io_serv,
65         const string &network_interface,
66         const string &destination_address,
67         const uint16_t destination_port,
68         const PingProtocolList &ping_protocol_list,
69         const long ping_interval_in_sec,
70         const int ping_fail_percentage_limit,
71         const int ping_reply_timeout,
72         LinkStatusItem link_analyzer,
73         const int first_delay
74
75 ) :
76     IoService( io_serv ),
77     NetworkInterfaceName( network_interface ),
78     DestinationAddress( destination_address ),
79     DestinationPort( destination_port ),
80     Protocols( ping_protocol_list ),
81     ProtocolIter(),
82     PingIntervalInSec( ping_interval_in_sec ),
83     FirstDelay( first_delay ),
84     NextPingTimer( *io_serv ),
85     TimeSentLastPing( microsec_clock::universal_time() ),
86     PingReplyTimeout( ping_reply_timeout ),
87     HostAnalyzer( destination_address, ping_fail_percentage_limit,
88                   link_analyzer ),
89     Resolver(),
90     Ping(),
91     WantToPing( false ),
92     LogPrefix(),
93     ContinueOnOutdatedIps( false )
94 {
95     BOOST_ASSERT( !network_interface.empty() );
96     BOOST_ASSERT( !destination_address.empty() );
97     BOOST_ASSERT( ( 0 < destination_port ) &&
98                   ( destination_port < numeric_limits<uint16_t>::max() ) );
99     BOOST_ASSERT( 0 < ping_interval_in_sec );
100     BOOST_ASSERT( (0 <= ping_fail_percentage_limit) &&
101                   ( ping_fail_percentage_limit <= 100) );
102
103     update_log_prefix();
104
105     init_ping_protocol();
106 }
107
108 /**
109  * @brief Destructor.
110  */
111 PingScheduler::~PingScheduler()
112 {
113 }
114
115 void PingScheduler::stop_pinging()
116 {
117     // stop pinger and resolver
118     GlobalLogger.debug() << LogPrefix << "scheduler: stop pinging";
119     Ping->stop_pinging();
120     cancel_resolve(true);
121
122     // now cancel the own timer in case that pinger cancelation called callback
123     GlobalLogger.debug() << LogPrefix << "scheduler: cancel timer";
124     NextPingTimer.cancel();
125 }
126
127 /**
128  * @brief Start into infinite loop of calls to ping
129  *
130  * Does not start yet but set NextPingTimer (possibly to 0), so action starts
131  *   when io_service is started
132  */
133 void PingScheduler::start_pinging()
134 {
135     if ( FirstDelay > 0 )
136         GlobalLogger.info() << LogPrefix << "Delaying first ping by "
137                                          << FirstDelay << "s";
138     else
139         GlobalLogger.info() << LogPrefix << "Schedule ping as soon as possible";
140
141     (void) NextPingTimer.expires_from_now( seconds( FirstDelay ) );
142     NextPingTimer.async_wait( bind( &PingScheduler::ping, this,
143                                           boost::asio::placeholders::error ) );
144 }
145
146
147 /**
148  * @brief call Ping::ping and schedule a call to ping_done_handler when finished
149  */
150 void PingScheduler::ping(const boost::system::error_code &error)
151 {
152     if ( error )
153     {   // get here, e.g. by NextPingTimer.cancel in stop_pinging
154         if ( error ==  boost::asio::error::operation_aborted )
155             GlobalLogger.error() << LogPrefix << "Timer for ping was cancelled!"
156                                  << " --> Stopping";
157         else
158             GlobalLogger.error() << LogPrefix << "Received error " << error
159                                  << " waiting for ping! Stopping";
160         return;
161     }
162
163     // ping as soon as dns is ready
164     WantToPing = true;
165     ping_when_ready();
166 }
167
168
169 void PingScheduler::ping_when_ready()
170 {
171     if ( !WantToPing )
172     {
173         GlobalLogger.info() << LogPrefix << "waiting for ping request "
174             << "(should take less than " << PingIntervalInSec << "s)";
175         return;
176     }
177     else if ( Resolver && Resolver->is_resolving() )
178     {
179         GlobalLogger.info() << LogPrefix << "waiting for DNS to finish";
180         return;
181     }
182     else if ( !Resolver )
183         // should not happen, but check anyway
184         GlobalLogger.warning() << LogPrefix << "Have no resolver!";
185
186     GlobalLogger.info() << LogPrefix << "start ping";
187     WantToPing = false;
188
189     // try to get an up-to-date IP (ContinueOnOutdatedIps may only be set
190     //   because a CNAME was out of date -- IPs may still be current)
191     HostAddress ip = Resolver->get_next_ip();
192
193     if ( !ip.is_valid() )
194     {   // this can happen in 2 cases: if ContinueOnOutdatedIps==true
195         // or when ip went out of date between resolve and now
196         // --> try to use outdated IP
197         GlobalLogger.info() << LogPrefix << "Checking for outdated IPs";
198         bool check_up_to_date = false;
199         ip = Resolver->get_next_ip(check_up_to_date);
200     }
201     if ( !ip.is_valid() )
202     {   // still no valid IP --> should not happen
203         GlobalLogger.error() << LogPrefix << "Not even outdated IP to ping "
204              << "-- this should not have happened!! Treat like a failed ping.";
205         if (DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() > 0)
206             GlobalLogger.warning() << LogPrefix << "This probably happened "
207                 << "because you specified a TTL threshold > 0 but resolving"
208                 << " had no effect on TTLs since external cache is only "
209                 << "updated when TTL=0 is reached.";
210
211         // skip the ping and directly call ping_done_handler
212         ping_done_handler(false);
213     }
214     else
215     {
216         uint32_t ttl = ip.get_ttl().get_updated_value();
217         std::string expiry;
218         if (ttl == 0)
219             expiry = "out of date!";
220         else
221         {
222             boost::posix_time::ptime now =
223                                   boost::posix_time::second_clock::local_time();
224             expiry = boost::posix_time::to_simple_string(now + seconds(ttl));
225         }
226
227         GlobalLogger.info() << LogPrefix << "pinging IP " << ip.get_ip()
228             << " with TTL " << ttl << "s (" << expiry << ")";
229         Ping->ping( ip.get_ip(),
230                     DestinationPort,
231                     boost::bind(&PingScheduler::ping_done_handler, this, _1) );
232     }
233 }
234
235
236 //------------------------------------------------------------------------------
237 // Post Processing of Ping result
238 //------------------------------------------------------------------------------
239
240 /**
241  * @brief called when Ping::ping is done; calls functions to update
242  *   statistics, ping interval and elapsed time;
243  *   schedules a call to ping, thereby closing the loop
244  */
245 void PingScheduler::ping_done_handler( const bool ping_success )
246 {
247     // post-processing
248     // You must call these 3 methods exactly in this order
249     // TODO Fix this method, once it has a semantic dependency with the
250     // update_ping_statistics method, because it depends on the PingAnalyzer
251     // statistics to update the exceeded_ping_failed_limit
252     HostAnalyzer.update_ping_statistics( ping_success );
253     update_ping_interval();
254     update_ping_elapsed_time();
255
256     if (ping_success)
257     {   // reset ContinueOnOutdatedIps
258         ContinueOnOutdatedIps = false;
259         update_log_prefix();
260     }
261
262     // get next protocol, possibly start resolving IPs
263     update_ping_protocol();
264
265     // schedule next ping
266     (void) NextPingTimer.expires_from_now( seconds( PingIntervalInSec ) );
267     NextPingTimer.async_wait( bind( &PingScheduler::ping, this,
268                                          boost::asio::placeholders::error ) );
269 }
270
271 void PingScheduler::update_ping_interval()
272 {
273     // have to ping more often?
274     if ( HostAnalyzer.exceeded_ping_failed_limit() )
275     {
276         PingIntervalInSec.speed_up();
277
278         GlobalLogger.debug() << LogPrefix << "- Speeding up ping interval to: "
279                              << PingIntervalInSec << "s";
280     }
281     else
282     {
283         PingIntervalInSec.back_to_original();
284
285         GlobalLogger.debug() << LogPrefix << "- Stick to the original ping "
286                              << "interval: " << PingIntervalInSec << "s";
287     }
288 }
289
290 void PingScheduler::update_ping_elapsed_time()
291 {
292     ptime now = microsec_clock::universal_time();
293     time_resolution_traits_adapted64_impl::int_type elapsed_time_in_sec =
294             (now - TimeSentLastPing).total_seconds();
295     GlobalLogger.debug() << LogPrefix << "- Time elapsed since last ping: "
296                                       << elapsed_time_in_sec << "s";
297
298     TimeSentLastPing = microsec_clock::universal_time();
299 }
300
301
302 //------------------------------------------------------------------------------
303 // Ping Protocol Rotation
304 //------------------------------------------------------------------------------
305
306 void PingScheduler::init_ping_protocol()
307 {
308     ProtocolIter = Protocols.end();
309     get_next_ping_protocol();
310 }
311
312 void PingScheduler::update_ping_protocol()
313 {
314     if ( can_change_ping_protocol() )
315     {
316         get_next_ping_protocol();
317     }
318 }
319
320 void PingScheduler::get_next_ping_protocol()
321 {
322     GlobalLogger.debug() << LogPrefix
323         << "------------------------------------------------------------------";
324     ++ProtocolIter;
325     if (ProtocolIter == Protocols.end())
326         ProtocolIter = Protocols.begin();
327     PingProtocol ping_protocol = *ProtocolIter;
328     // --> ProtocolIter still points to currently used protocol which is
329     //     required in dns_resolve_callback
330
331     if (Ping)
332         Ping->stop_pinging();
333
334     Ping = PingerFactory::createPinger(ping_protocol, IoService,
335                                        NetworkInterfaceName, PingReplyTimeout);
336
337     update_dns_resolver( ping_protocol );
338
339 }
340
341 bool PingScheduler::can_change_ping_protocol() const
342 {
343     // TODO can_change_ping_protocol() and get_next_ping_protocol() may be
344     // implemented in a Algorithm class that can be exchanged in this class to
345     // provide an algorithm neutral class
346     return true;
347 }
348
349 //------------------------------------------------------------------------------
350 // DNS host name resolution
351 //------------------------------------------------------------------------------
352
353 // show "!" after host name if running on outdated IPs
354 void PingScheduler::update_log_prefix()
355 {
356     std::stringstream temp;
357     temp << "Sched(" << DestinationAddress;
358     if (ContinueOnOutdatedIps)
359         temp << "!";
360     temp << "): ";
361     LogPrefix = temp.str();
362 }
363
364 void PingScheduler::update_dns_resolver( PingProtocol current_protocol )
365 {
366     if (Resolver && Resolver->is_resolving())
367         cancel_resolve(false);
368
369     if (ContinueOnOutdatedIps)
370     {
371         ContinueOnOutdatedIps = false;
372         update_log_prefix();
373     }
374
375     // DNS master caches created resolvers and resolved IPs, so this will
376     // probably just return an existing resolver with already resolved IPs for
377     // requested protocol ( ICMP/TCP is ignored, only IPv4/v6 is important)
378     Resolver = DnsMaster::get_instance()->get_resolver_for(DestinationAddress,
379                                                            current_protocol);
380
381     // get number of up-to-date IPs
382     // TODO should check here, if they will be up to date in PingIntervalInSec
383     bool check_up_to_date = true;
384     int ip_count = Resolver->get_resolved_ip_count(check_up_to_date);
385     if (ip_count > 0)
386     {
387         GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
388             << ip_count << " where IPs may be outdated: " << !check_up_to_date;
389         HostAnalyzer.set_resolved_ip_count( ip_count );
390
391         if (Resolver->is_resolving())
392             GlobalLogger.warning() << LogPrefix << "have up to date IPs but "
393                 << "resolver seems to be resolving all the same... "
394                 << "Start pinging anyway!";
395         ping_when_ready();
396     }
397     else
398     {
399         GlobalLogger.info() << LogPrefix
400                             << "No up-to-date IPs --> start resolve";
401         start_resolving_ping_address();
402         // set resolved_ip_count will be called in resolve callback
403     }
404 }
405
406 void PingScheduler::start_resolving_ping_address()
407 {
408     Resolver->async_resolve( boost::bind(&PingScheduler::dns_resolve_callback,
409                                           this, _1, _2) );
410 }
411
412 void PingScheduler::dns_resolve_callback(const bool was_success,
413                                          const int recursion_count)
414 {
415     GlobalLogger.info() << LogPrefix << "dns resolution finished "
416                         << "with success = " << was_success << " "
417                         << "after " << recursion_count << " recursions";
418
419     if ( was_success )
420     {
421         // trust that a successfull DNS resolve means we have an IP with TTL>0
422         int ip_count = Resolver->get_resolved_ip_count(!ContinueOnOutdatedIps);
423         if (ip_count == 0)
424         {   // this will create trouble in HostAnalyzer
425             GlobalLogger.warning() << LogPrefix
426                 << "Should not have reached this case: resolve was "
427                 << "successfull but still have no IPs (up-to-date="
428                 << !ContinueOnOutdatedIps << ")!";
429             if (DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() > 0)
430                 GlobalLogger.warning() << LogPrefix << "This probably happened "
431                     << "because you specified a TTL threshold > 0 but resolving"
432                     << " had no effect on TTLs since external cache is only "
433                     << "updated when TTL=0 is reached.";
434         }
435         else
436         {
437             GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
438                 << ip_count << " where IPs may be outdated: "
439                 << ContinueOnOutdatedIps << " --> could ping now";
440             HostAnalyzer.set_resolved_ip_count( ip_count );
441         }
442         ping_when_ready();
443     }
444     else
445     {   // host name resolution failed; try again bypassing first outdated CNAME
446         // or using cached IP
447         ContinueOnOutdatedIps = true;
448         update_log_prefix();
449
450         std::string skip_host = Resolver->get_skip_cname();
451
452         if (skip_host.empty())
453         {   // continue with IP
454             GlobalLogger.notice() << LogPrefix << "DNS failed, "
455                 << "try anyway with cached data";
456
457             int ip_count = Resolver->get_resolved_ip_count(false);
458             GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
459                 << ip_count << " where IPs may be outdated: " << true;
460             HostAnalyzer.set_resolved_ip_count( ip_count );
461
462             ping_when_ready();
463         }
464         else
465         {   // have CNAME to continue
466             GlobalLogger.notice() << LogPrefix << "DNS failed, "
467                 << "try again skipping a CNAME and resolving "
468                 << skip_host << " directly";
469
470             cancel_resolve(false);
471
472             // now create new resolver
473             Resolver = DnsMaster::get_instance()
474                                    ->get_resolver_for(skip_host, *ProtocolIter);
475             start_resolving_ping_address();
476         }
477     }
478 }
479
480 /**
481  * cancel resolver if force_cancel or if it is not resolving DestinationAddress
482  *
483  * Resolvers have a life on their own: they are cached by DnsMaster so never go
484  *   out of scope and even after calling callbacks, there might still be a
485  *   longterm timer active to re-try resolving.
486  * We want to cancel that long-term timer only if the Resolver is not for our
487  *   real, original DestinationAddress but a CNAME, which can happen when trying
488  *   to skip cnames and working on out-dated IPs
489  */
490 void PingScheduler::cancel_resolve(const bool force_cancel)
491 {
492     if (force_cancel)
493     {
494         GlobalLogger.info() << "Cancelling resolver (forced)";
495         Resolver->cancel_resolve();
496     }
497     else if ( Resolver->get_hostname() == DestinationAddress )
498         GlobalLogger.info() << LogPrefix
499                             << "Leave original resolver active in background";
500     else
501     {
502         GlobalLogger.info() << LogPrefix << "Cancel resolver for "
503             << Resolver->get_hostname() << " since is not the original "
504             << DestinationAddress;
505         Resolver->cancel_resolve();
506     }
507 }
508