1 /*******************************************************************************
2 
3     Manages ITimeoutClient instances where each one has an individual timeout
4     value.
5 
6     To use the timeout manager, create a TimeoutManager subclass capable of
7     these two things:
8         1. It implements setTimeout() to set a timer that expires at the wall
9            clock time that is passed to setTimeout() as argument.
10         2. When the timer is expired, it calls checkTimeouts().
11 
12     Objects that can time out, the so-called timeout clients, must implement
13     ITimeoutClient. For each client create an ExpiryRegistration instance and
14     pass the object to the ExpiryRegistration constructor.
15     Call ExpiryRegistration.register() to set a timeout for the corresponding
16     client. When checkTimeouts() is called, it calls the timeout() method of
17     each timed out client.
18 
19 
20     Link with:
21         -Llibebtree.a
22 
23     Build flags:
24         -debug=TimeoutManager = verbose output
25 
26     Copyright:
27         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
28         All rights reserved.
29 
30     License:
31         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
32         Alternatively, this file may be distributed under the terms of the Tango
33         3-Clause BSD License (see LICENSE_BSD.txt for details).
34 
35 *******************************************************************************/
36 
37 module ocean.time.timeout.TimeoutManager;
38 
39 
40 import ocean.time.timeout.model.ITimeoutManager,
41        ocean.time.timeout.model.ITimeoutClient,
42        ocean.time.timeout.model.IExpiryRegistration,
43        ocean.time.timeout.model.ExpiryRegistrationBase;                 // ExpiryTree, Expiry, ExpiryRegistrationBase
44 
45 import ocean.time.MicrosecondsClock;
46 
47 import ocean.util.container.AppendBuffer;
48 
49 
50 import ocean.util.container.map.Map,
51        ocean.util.container.map.model.StandardHash,
52        ocean.util.container.map.model.IAllocator;
53 
54 import ocean.meta.types.Qualifiers;
55 
56 debug
57 {
58     import core.stdc.time: time_t, ctime;
59     import core.stdc.string: strlen;
60 }
61 
62 /*******************************************************************************
63 
64     Timeout manager
65 
66 *******************************************************************************/
67 
68 class TimeoutManager : TimeoutManagerBase
69 {
70     /***************************************************************************
71 
72         Default expected number of elements in expiry registration to
73         ISelectClient map.
74 
75     ***************************************************************************/
76 
77     private static immutable default_expected_no_elements = 1024;
78 
79     /***************************************************************************
80 
81         Expiry registration class for an object that can time out.
82 
83     ***************************************************************************/
84 
85     public class ExpiryRegistration : ExpiryRegistrationBase
86     {
87         /***********************************************************************
88 
89             Constructor
90 
91             Params:
92                 client = object that can time out
93 
94         ***********************************************************************/
95 
96         public this ( ITimeoutClient client )
97         {
98             super(this.outer..new TimeoutManagerInternal);
99             super.client = client;
100         }
101 
102         /***********************************************************************
103 
104             Identifier string for debugging.
105 
106         ***********************************************************************/
107 
108         debug public override cstring id ( )
109         {
110             return super.client.id;
111         }
112     }
113 
114 
115     /***************************************************************************
116 
117         Constructor.
118 
119             n = expected number of elements in expiry registration to
120                 ISelectClient map
121             allocator = use this bucket element allocator for the expiry
122                 registration to ISelectClient map. If it is null the default map
123                 allocator (BucketElementGCAllocator) is used.
124 
125     ***************************************************************************/
126 
127     public this ( size_t n = default_expected_no_elements, IAllocator allocator = null )
128     {
129         super(n, allocator);
130     }
131 
132     /***************************************************************************
133 
134         Constructor.
135 
136         Params:
137             allocator = use this bucket element allocator for the expiry
138                 registration to ISelectClient map.
139 
140     ***************************************************************************/
141 
142     public this ( IAllocator allocator )
143     {
144         super(default_expected_no_elements, allocator);
145     }
146 
147     /***************************************************************************
148 
149         Creates a new expiry registration instance, associates client with it
150         and registers client with this timeout manager.
151         The returned object should be reused. The client will remain associated
152         to the expiry registration after it has been unregistered from the
153         timeout manager.
154 
155         Params:
156             client = client to register
157 
158         Returns:
159             new expiry registration object with client associated to.
160 
161     ***************************************************************************/
162 
163     public IExpiryRegistration getRegistration ( ITimeoutClient client )
164     {
165         return this.new ExpiryRegistration(client);
166     }
167 }
168 
169 /*******************************************************************************
170 
171     Timeout manager base class. Required for derivation because inside a
172     TimeoutManager subclass a nested ExpiryRegistration subclass is impossible.
173 
174 *******************************************************************************/
175 
176 abstract class TimeoutManagerBase : ITimeoutManager
177 {
178     /***************************************************************************
179 
180         Enables IExpiryRegistration to access TimeoutManager internals.
181 
182     ***************************************************************************/
183 
184     protected class TimeoutManagerInternal : ExpiryRegistrationBase.ITimeoutManagerInternal
185     {
186         /***********************************************************************
187 
188             Registers registration and sets the timeout for its client.
189 
190             Params:
191                 registration = IExpiryRegistration instance to register
192                 timeout_us   = timeout in microseconds from now
193 
194             Returns:
195                 expiry token: required for unregister(); the "key" member is the
196                 wall clock time of expiration as UNIX time in microseconds.
197 
198         ***********************************************************************/
199 
200         Expiry* register ( IExpiryRegistration registration, ulong timeout_us )
201         {
202             return this.outer.register(registration, timeout_us);
203         }
204 
205         /***********************************************************************
206 
207             Unregisters IExpiryRegistration instance corresponding to expiry.
208 
209             Params:
210                 expiry = expiry token returned by register() when registering
211                          the IExpiryRegistration instance to unregister
212 
213             In:
214                 Must not be called from within timeout(). Doing so would still
215                 leave already fired timer events in the TimeoutManager internal
216                 list and their respective `timeout` will be called despite
217                 unregistration. be called from within timeout().
218 
219         ***********************************************************************/
220 
221         void unregister ( ref Expiry expiry )
222         {
223             this.outer.unregister(expiry);
224         }
225 
226         /***********************************************************************
227 
228             If the expiry is present in the list of expired registrations being
229             currently iterated over by checkTimeouts, then it will be removed
230             (its timeout method will not be called). (This means that drop can
231             be called from timeout callbacks, unlike unregister.)
232 
233             Does NOT unregister expiry, `this.unregister` has to be called
234             for that additionally.
235 
236             Params:
237                 registration = expiry registration reference
238 
239         ***********************************************************************/
240 
241         protected void drop ( IExpiryRegistration registration )
242         {
243             // If this method is called while checkTimeouts is iterating over
244             // the list of expired registrations, then we need to check whether
245             // the expiry to be dropped is present in the list and remove it, if
246             // it is.
247             // This makes it possible to disable timer events from the
248             // timeout callbacks of other timer events.
249 
250             foreach (ref pending; this.outer.expired_registrations[])
251             {
252                 if (pending is registration)
253                     pending = null;
254             }
255         }
256 
257         /***********************************************************************
258 
259             Returns:
260                 the current wall clock time as UNIX time in microseconds.
261 
262         ***********************************************************************/
263 
264         ulong now ( )
265         {
266             return this.outer.now();
267         }
268     }
269 
270     /***************************************************************************
271 
272         EBTree storing expiry time of registred clients in terms of microseconds
273         since the construction of this object (for direct comparison against
274         this.now_).
275 
276     ***************************************************************************/
277 
278     private ExpiryTree expiry_tree;
279 
280 
281     /***************************************************************************
282 
283         Array map mapping from an expiry registration ( a node in the tree of
284         expiry times) to an ISelectClient.
285 
286     ***************************************************************************/
287 
288     static class ExpiryToClient : Map!(IExpiryRegistration, Expiry*)
289     {
290         /***********************************************************************
291 
292             Constructor.
293 
294             Params:
295                 n = expected number of elements in mapping
296                 allocator = use this bucket element allocator for the map. If it
297                     is null the default allocator is used.
298 
299         ***********************************************************************/
300 
301         public this ( size_t n, IAllocator allocator = null )
302         {
303             // create the map with the default allocator
304             // BucketElementGCAllocator
305             if ( allocator is null )
306             {
307                 super(n);
308             }
309             else
310             {
311                 super(allocator, n);
312             }
313         }
314 
315         protected override hash_t toHash ( in Expiry* expiry )
316         {
317             return StandardHash.fnv1aT(expiry);
318         }
319     }
320 
321 
322     private ExpiryToClient expiry_to_client;
323 
324     /***************************************************************************
325 
326         List of expired registrations. Used by the checkTimeouts() method.
327 
328         Elements can be set to `null` by `drop` method, in which case they
329         are ignored.
330 
331     ***************************************************************************/
332 
333     private AppendBuffer!(IExpiryRegistration) expired_registrations;
334 
335     /***************************************************************************
336 
337         Constructor.
338 
339             n = expected number of elements in expiry registration to
340                 ISelectClient map
341             allocator = use this bucket element allocator for the expiry
342                 registration to ISelectClient map. If it is null the default
343                 allocator (BucketElementGCAllocator) is used.
344 
345     ***************************************************************************/
346 
347     protected this ( size_t n = 1024, IAllocator allocator = null )
348     {
349         this.expiry_tree           = new ExpiryTree;
350         this.expiry_to_client      = new ExpiryToClient(n, allocator);
351         this.expired_registrations = new AppendBuffer!(IExpiryRegistration)(n);
352     }
353 
354 
355     /***************************************************************************
356 
357         Tells the wall clock time time when the next client will expire.
358 
359         Returns:
360             the wall clock time when the next client will expire as UNIX time
361             in microseconds or ulong.max if no client is currently registered.
362 
363     ***************************************************************************/
364 
365     public ulong next_expiration_us ( )
366     {
367         Expiry* expiry = this.expiry_tree.first;
368 
369         ulong us = expiry? expiry.key : ulong.max;
370 
371         debug ( TimeoutManager ) if (!this.next_expiration_us_called_from_internal)
372         {
373             this.next_expiration_us_called_from_internal = false;
374 
375             Stderr("next expiration: ");
376 
377             if (us < us.max)
378             {
379                 this.printTime(us);
380             }
381             else
382             {
383                 Stderr("∞\n").flush();
384             }
385         }
386 
387         return us;
388     }
389 
390     /***************************************************************************
391 
392         Tells the time left until the next client will expire.
393 
394         Returns:
395             the time left until next client will expire in microseconds or
396             ulong.max if no client is currently registered. 0 indicates that
397             there are timed out clients that have not yet been notified and
398             unregistered.
399 
400     ***************************************************************************/
401 
402     public ulong us_left ( )
403     {
404         Expiry* expiry = this.expiry_tree.first;
405 
406         if (expiry)
407         {
408             ulong next_expiration_us = expiry.key,
409                   now                = this.now;
410 
411             debug ( TimeoutManager )
412             {
413                 ulong us = next_expiration_us > now? next_expiration_us - now : 0;
414 
415                 this.printTime(now, false);
416                 Stderr(": ")(us)(" µs left\n").flush();
417 
418                 return us;
419             }
420             else
421             {
422                 return next_expiration_us > now? next_expiration_us - now : 0;
423             }
424         }
425         else
426         {
427             return ulong.max;
428         }
429     }
430 
431     /***************************************************************************
432 
433         Returns:
434             the number of registered clients.
435 
436     ***************************************************************************/
437 
438     public size_t pending ( )
439     {
440         return this.expiry_tree.length;
441     }
442 
443     /***************************************************************************
444 
445         Returns the current wall clock time according to gettimeofday().
446 
447         Returns:
448             the current wall clock time as UNIX time value in microseconds.
449 
450     ***************************************************************************/
451 
452     public final ulong now ( )
453     {
454         return MicrosecondsClock.now_us();
455     }
456 
457     /***************************************************************************
458 
459         Checks for timed out clients. For any timed out client its timeout()
460         method is called, then it is unregistered, finally dg() is called with
461         it as argument.
462 
463         This method should be called when the timeout set by setTimeout() has
464         expired.
465 
466         If dg returns false to cancel, the clients iterated over so far are
467         removed. To remove the remaining clients, call this method again.
468 
469         Params:
470             dg = optional callback delegate that will be called with each timed
471                  out client and must return true to continue or false to cancel.
472 
473         Returns:
474             the number of expired clients.
475 
476     ***************************************************************************/
477 
478     public size_t checkTimeouts ( scope bool delegate ( ITimeoutClient client ) dg = null )
479     {
480         return this.checkTimeouts(this.now, dg);
481     }
482 
483     public size_t checkTimeouts ( ulong now, scope bool delegate ( ITimeoutClient client ) dg = null )
484     {
485         debug ( TimeoutManager )
486         {
487             this.printTime(now, false);
488             Stderr(" --------------------- checkTimeouts\n");
489 
490             this.next_expiration_us_called_from_internal = true;
491         }
492 
493         ulong previously_next = this.next_expiration_us;
494 
495         this.expired_registrations.clear();
496 
497         // We first build up a list of all expired registrations, in order to
498         // avoid the situation of the timeout() delegates potentially modifying
499         // the tree while iterating over it.
500 
501         version (all)
502         {
503             scope expiries = this.expiry_tree..new PartIterator(now);
504 
505             foreach_reverse (ref expiry; expiries)
506             {
507                 IExpiryRegistration registration = *this.expiry_to_client.get(&expiry);
508 
509                 debug ( TimeoutManager ) Stderr('\t')(registration.id)(" timed out\n");
510 
511                 this.expired_registrations ~= registration;
512             }
513         }
514         else foreach (expiry, expire_time; this.expiry_tree.lessEqual(now))
515         {
516             IExpiryRegistration registration = this.expiry_to_client[expiry];
517 
518             debug ( TimeoutManager ) Stderr('\t')(registration.id)(" timed out\n");
519 
520             this.expired_registrations ~= registration;
521         }
522 
523         debug ( TimeoutManager ) Stderr.flush();
524 
525         // All expired registrations are removed from the expiry tree. They are
526         // removed before the timeout() delegates are called in order to avoid
527         // the situation of an expiry registration re-registering itself in its
528         // timeout() method, thus being registered in the expiry tree twice.
529         foreach (registration; this.expired_registrations[])
530         {
531             registration.unregister();
532         }
533 
534         // Finally all expired registrations in the list are set to null and
535         // the timeout is updated.
536         scope ( exit )
537         {
538             this.expired_registrations[] = cast(IExpiryRegistration)null;
539 
540             this.setTimeout_(previously_next);
541         }
542 
543         // The timeout() method of all expired registrations is called, until
544         // the optional delegate returns false.
545         foreach (ref registration; this.expired_registrations[])
546         {
547             // registration can be disabled by `drop` method by being set to
548             // `null`
549             if (registration is null)
550                 continue;
551 
552             ITimeoutClient client = registration.timeout();
553 
554             if (dg !is null) if (!dg(client)) break;
555         }
556 
557         return this.expired_registrations.length;
558     }
559 
560     /***************************************************************************
561 
562         Registers registration and sets the timeout for its client.
563 
564         Params:
565             registration = IExpiryRegistration instance to register
566             timeout_us   = timeout in microseconds from now
567 
568         Returns:
569             expiry token: required for unregister(); the "key" member is the
570             wall clock time of expiration as UNIX time in microseconds.
571 
572     ***************************************************************************/
573 
574     protected Expiry* register ( IExpiryRegistration registration, ulong timeout_us )
575     out (expiry)
576     {
577         assert (expiry);
578     }
579     do
580     {
581         ulong now = this.now;
582 
583         ulong t = now + timeout_us;
584 
585         debug ( TimeoutManager ) this.next_expiration_us_called_from_internal = true;
586 
587         ulong previously_next = this.next_expiration_us;
588 
589         Expiry* expiry = this.expiry_tree.add(t);
590 
591         *this.expiry_to_client.put(expiry) = registration;
592 
593         debug ( TimeoutManager )
594         {
595             Stderr("----------- ");
596             this.printTime(now, false);
597             Stderr(" registered ")(registration.id)(" for ")(timeout_us)(" µs, times out at ");
598             this.printTime(t, false);
599             Stderr("\n\t")(this.expiry_tree.length)(" clients registered, first times out at ");
600             this.printTime(this.expiry_tree.first.key, false);
601             Stderr('\n');
602 
603             version (none) foreach (expiry, expire_time; this.expiry_tree.lessEqual(now + 20_000_000))
604             {
605                 IExpiryRegistration registration = this.expiry_to_client[expiry];
606 
607                 Stderr('\t')('\t')(registration.id)(" ");
608                 if ( expire_time <= now ) Stderr(" ** ");
609                 this.printTime(expire_time);
610             }
611         }
612 
613         this.setTimeout_(previously_next);
614 
615         return expiry;
616     }
617 
618     /***************************************************************************
619 
620         Unregisters the IExpiryRegistration instance corresponding to expiry.
621 
622         Params:
623             expiry = expiry token returned by register() when registering the
624                      IExpiryRegistration instance to unregister
625 
626         Throws:
627             Exception if no IExpiryRegistration instance corresponding to expiry
628             is currently registered.
629 
630     ***************************************************************************/
631 
632     protected void unregister ( ref Expiry expiry )
633     {
634         debug ( TimeoutManager ) this.next_expiration_us_called_from_internal = true;
635 
636         ulong previously_next = this.next_expiration_us;
637 
638         debug ulong t = expiry.key;
639 
640         try try
641         {
642             this.expiry_to_client.remove(&expiry);
643         }
644         finally
645         {
646             this.expiry_tree.remove(expiry);
647         }
648         finally
649         {
650             debug ( TimeoutManager )
651             {
652                 size_t n = this.expiry_tree.length;
653 
654                 Stderr("----------- ");
655                 this.printTime(now, false);
656                 Stderr(" unregistered ");
657                 this.printTime(t, false);
658                 Stderr("\n\t")(n)(" clients registered");
659                 if (n)
660                 {
661                     Stderr(", first times out at ");
662                     this.printTime(this.expiry_tree.first.key, false);
663                 }
664                 Stderr('\n');
665             }
666 
667             this.setTimeout_(previously_next);
668         }
669     }
670 
671     /***************************************************************************
672 
673         Called when the overall timeout needs to be set or changed.
674 
675         Params:
676             next_expiration_us = wall clock time when the first client times
677                                     out so that checkTimeouts() must be called.
678 
679     ***************************************************************************/
680 
681     protected void setTimeout ( ulong next_expiration_us ) { }
682 
683     /***************************************************************************
684 
685         Called when the last client has been unregistered so that the timer may
686         be disabled.
687 
688     ***************************************************************************/
689 
690     protected void stopTimeout ( ) { }
691 
692     /***************************************************************************
693 
694         Calls setTimeout() or stopTimeout() if required.
695 
696         Params:
697             previously_next = next expiration time before a client was
698                                  registered/unregistered
699 
700     ***************************************************************************/
701 
702     private void setTimeout_ ( ulong previously_next )
703     {
704         Expiry* expiry = this.expiry_tree.first;
705 
706         if (expiry)
707         {
708             ulong next_now = expiry.key;
709 
710             if (next_now != previously_next)
711             {
712                 this.setTimeout(next_now);
713             }
714         }
715         else if (previously_next < previously_next.max)
716         {
717             this.stopTimeout();
718         }
719     }
720 
721     /***************************************************************************
722 
723         TODO: Remove debugging output.
724 
725     ***************************************************************************/
726 
727     debug ( TimeoutManager ):
728 
729     bool next_expiration_us_called_from_internal;
730 
731     /***************************************************************************
732 
733         Prints the current wall clock time.
734 
735     ***************************************************************************/
736 
737     void printTime ( bool nl = true )
738     {
739         this.printTime(this.now, nl);
740     }
741 
742     /***************************************************************************
743 
744         Prints t.
745 
746         Params:
747             t = wall clock time as UNIX time in microseconds.
748 
749     ***************************************************************************/
750 
751     static void printTime ( ulong t, bool nl = true )
752     {
753         time_t s  = cast (time_t) (t / 1_000_000);
754         uint   us = cast (uint)   (t % 1_000_000);
755 
756         char* str = ctime(&s);
757 
758         Stderr(str[0 .. strlen(str) - 1])('.')(us);
759 
760         if (nl) Stderr('\n').flush();
761     }
762 }