0f08565d35fc11e8428e1df49a097e78b1588592
[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     Resolver->cancel_resolve();
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 << "not pinging (not requested to)";
174         return;
175     }
176     else if ( Resolver && Resolver->is_resolving() )
177     {
178         GlobalLogger.info() << LogPrefix << "not pinging (DNS not finished)";
179         return;
180     }
181     else if ( !Resolver )
182         // should not happen, but check anyway
183         GlobalLogger.warning() << LogPrefix << "Have no resolver!";
184
185     GlobalLogger.info() << LogPrefix << "start ping";
186     WantToPing = false;
187
188     // try to get an up-to-date IP
189     HostAddress ip = Resolver->get_next_ip();
190
191     if ( !ip.is_valid() && ContinueOnOutdatedIps)
192     {   // we failed to resolve --> try to use outdated IP
193         GlobalLogger.info() << LogPrefix << "Checking for outdated IPs";
194         bool check_up_to_date = false;
195         ip = Resolver->get_next_ip(check_up_to_date);
196     }
197     if ( ip.is_valid() )
198         Ping->ping(
199                 Resolver->get_next_ip().get_ip(),
200                 DestinationPort,
201                 boost::bind(&PingScheduler::ping_done_handler, this, _1)
202         );
203     else
204     {   // should not happen
205         GlobalLogger.error() << LogPrefix << "No IP to ping "
206                              << "-- this should not have happened!!";
207         WantToPing = true;
208         if ( !Resolver->is_resolving() )
209             start_resolving_ping_address();
210     }
211
212     // next time try with up-to-date IP
213     ContinueOnOutdatedIps = false;
214 }
215
216
217 //------------------------------------------------------------------------------
218 // Post Processing of Ping result
219 //------------------------------------------------------------------------------
220
221 /**
222  * @brief called when Ping::ping is done; calls functions to update
223  *   statistics, ping interval and elapsed time;
224  *   schedules a call to ping, thereby closing the loop
225  */
226 void PingScheduler::ping_done_handler( const bool ping_success )
227 {
228     // post-processing
229     // You must call these 3 methods exactly in this order
230     // TODO Fix this method, once it has a semantic dependency with the
231     // update_ping_statistics method, because it depends on the PingAnalyzer
232     // statistics to update the exceeded_ping_failed_limit
233     HostAnalyzer.update_ping_statistics( ping_success );
234     update_ping_interval();
235     update_ping_elapsed_time();
236
237     // get next protocol, possibly start resolving IPs
238     update_ping_protocol();
239
240     // schedule next ping
241     (void) NextPingTimer.expires_from_now( seconds( PingIntervalInSec ) );
242     NextPingTimer.async_wait( bind( &PingScheduler::ping, this,
243                                          boost::asio::placeholders::error ) );
244 }
245
246 void PingScheduler::update_ping_interval()
247 {
248     // have to ping more often?
249     if ( HostAnalyzer.exceeded_ping_failed_limit() )
250     {
251         PingIntervalInSec.speed_up();
252
253         GlobalLogger.debug() << LogPrefix << "- Speeding up ping interval to: "
254                              << PingIntervalInSec << "s";
255     }
256     else
257     {
258         PingIntervalInSec.back_to_original();
259
260         GlobalLogger.debug() << LogPrefix << "- Stick to the original ping "
261                              << "interval: " << PingIntervalInSec << "s";
262     }
263 }
264
265 void PingScheduler::update_ping_elapsed_time()
266 {
267     ptime now = microsec_clock::universal_time();
268     time_resolution_traits_adapted64_impl::int_type elapsed_time_in_sec =
269             (now - TimeSentLastPing).total_seconds();
270     GlobalLogger.debug() << LogPrefix << "- Time elapsed since last ping: "
271                                       << elapsed_time_in_sec << "s";
272
273     TimeSentLastPing = microsec_clock::universal_time();
274 }
275
276
277 //------------------------------------------------------------------------------
278 // Ping Protocol Rotation
279 //------------------------------------------------------------------------------
280
281 void PingScheduler::init_ping_protocol()
282 {
283     ProtocolIter = Protocols.end();
284     get_next_ping_protocol();
285 }
286
287 void PingScheduler::update_ping_protocol()
288 {
289     if ( can_change_ping_protocol() )
290     {
291         get_next_ping_protocol();
292     }
293 }
294
295 void PingScheduler::get_next_ping_protocol()
296 {
297     ++ProtocolIter;
298     if (ProtocolIter == Protocols.end())
299         ProtocolIter = Protocols.begin();
300     PingProtocol ping_protocol = *ProtocolIter;
301     // --> ProtocolIter still points to currently used protocol which is 
302     //     required in dns_resolve_callback
303
304     if (Ping)
305         Ping->stop_pinging();
306
307     Ping = PingerFactory::createPinger(ping_protocol, IoService,
308                                        NetworkInterfaceName, PingReplyTimeout);
309
310     update_dns_resolver( ping_protocol );
311 }
312
313 bool PingScheduler::can_change_ping_protocol() const
314 {
315     // TODO can_change_ping_protocol() and get_next_ping_protocol() may be
316     // implemented in a Algorithm class that can be exchanged in this class to
317     // provide an algorithm neutral class
318     return true;
319 }
320
321 //------------------------------------------------------------------------------
322 // DNS host name resolution
323 //------------------------------------------------------------------------------
324
325 // show "!" after host name if running on outdated IPs
326 void PingScheduler::update_log_prefix()
327 {
328     std::stringstream temp;
329     temp << "PS(" << DestinationAddress;
330     if (ContinueOnOutdatedIps)
331         temp << "!";
332     temp << "): ";
333     LogPrefix = temp.str();
334 }
335
336 void PingScheduler::update_dns_resolver( PingProtocol current_protocol )
337 {
338     if (Resolver && Resolver->is_resolving())
339     {
340         GlobalLogger.warning() << LogPrefix
341                           << "Resolver still seems to be resolving --> cancel!";
342         Resolver->cancel_resolve();
343     }
344
345     // DNS master caches created resolvers and resolved IPs, so this will
346     // probably just return an existing resolver with already resolved IPs for
347     // requested protocol ( ICMP/TCP is ignored, only IPv4/v6 is important)
348     Resolver = DnsMaster::get_instance()->get_resolver_for(DestinationAddress,
349                                                            current_protocol);
350     // start resolving if no ips available
351     if ( Resolver->have_up_to_date_ip() )
352     {
353         if (!Resolver->is_resolving())
354             GlobalLogger.warning() << LogPrefix << "have up to date IPs but "
355                 << "resolver seems to be resolving all the same... "
356                 << "Start pinging anyway!";
357         ping_when_ready();
358     }
359     else
360         start_resolving_ping_address();
361 }
362
363 void PingScheduler::start_resolving_ping_address()
364 {
365     Resolver->async_resolve( boost::bind(&PingScheduler::dns_resolve_callback,
366                                           this, _1, _2) );
367 }
368
369 void PingScheduler::dns_resolve_callback(const bool was_success,
370                                          const int cname_count)
371 {
372     GlobalLogger.info() << LogPrefix << "dns resolution finished "
373                         << "with success = " << was_success << " "
374                         << "and cname_count = " << cname_count;
375
376     // TODO this is too simple, but need to think more about how to update here!
377     // (may have to switch back some time to resolver for original host or so
378     ContinueOnOutdatedIps = !was_success;
379     update_log_prefix();
380
381     if ( was_success )
382     {
383         HostAnalyzer.set_resolved_ip_count( Resolver->get_resolved_ip_count());
384         ping_when_ready();
385     }
386     else
387     {   // host name resolution failed; try again bypassing first outdated CNAME
388         // or using cached IP
389
390         std::string skip_host = Resolver->get_skip_cname();
391
392         if (skip_host.empty())
393         {   // continue with IP
394             GlobalLogger.notice() << LogPrefix << "DNS failed, "
395                 << "try anyway with cached data";
396             HostAnalyzer.set_resolved_ip_count(0);
397             ping_when_ready();
398         }
399         else
400         {   // have CNAME to continue
401             GlobalLogger.notice() << LogPrefix << "DNS failed, "
402                 << "try again skipping a CNAME and resolving "
403                 << skip_host << " directly";
404             Resolver = DnsMaster::get_instance()
405                                    ->get_resolver_for(skip_host, *ProtocolIter);
406             start_resolving_ping_address();
407
408             // (the original resolver is still alive and cached by DnsMaster and
409             //  counting down time to re-try on its own until cancel_resolve)
410         }
411     }
412 }