2017-08-10 19:04:45 +00:00
|
|
|
#lang syndicate/core
|
2015-03-05 14:54:12 +00:00
|
|
|
;; Timer driver.
|
|
|
|
|
|
|
|
;; Uses mutable state internally, but because the scope of the
|
|
|
|
;; mutation is limited to each timer process alone, it's easy to show
|
|
|
|
;; correct linear use of the various pointers.
|
|
|
|
|
|
|
|
(require racket/set)
|
|
|
|
(require data/heap)
|
2016-07-30 17:02:07 +00:00
|
|
|
(require syndicate/protocol/advertise)
|
2015-03-05 14:54:12 +00:00
|
|
|
|
|
|
|
(struct pending-timer (deadline label) #:transparent)
|
2017-10-03 09:49:48 +00:00
|
|
|
(struct timer-interrupt (counter now sealed-labels) #:transparent)
|
2015-03-05 14:54:12 +00:00
|
|
|
|
|
|
|
(provide (struct-out set-timer)
|
|
|
|
(struct-out timer-expired)
|
|
|
|
spawn-timer-driver)
|
|
|
|
|
|
|
|
(struct set-timer (label msecs kind) #:prefab)
|
|
|
|
(struct timer-expired (label msecs) #:prefab)
|
|
|
|
|
2017-10-03 09:49:48 +00:00
|
|
|
(define timer-interrupt-projection (inbound (?! (timer-interrupt ? ? ?))))
|
|
|
|
|
|
|
|
(struct timer-state (active-count interrupt-counter) #:transparent)
|
Switch from timer-expired /messages/ to /assertions/ at ground level.
Previously, the timer driver caused the background thread to call
send-ground-message to indicate that a timer had expired. However,
this can lead to a race! In cases where a timer expires very soon, the
channel-put of the set-timer instruction leads shortly thereafter to a
send-ground-message which then races the establishment of the
metalevel-1 subscription to the timer-expired events that are coming
from the background thread.
The race cannot occur in the sequential implementation because the
network makes sure to enqueue the transition actions resulting from
the set-timer message delivery ahead of any enqueueing of the
timer-expired ground message, so that by the time the ground message
is processed, the relevant subscription always exists.
In a looser implementation, however, this level of synchronised
activity may not exist, and the ground message may overtake the
subscription establishment.
Therefore, I've changed the driver to instead use ground /assertions/
to signal expired timers. Upon processing of such an assertion, the
driver cleans it up. This is very similar to hardware interrupts,
where the driver has to "clear the interrupt" in order to let the
system continue properly.
2016-01-21 22:38:12 +00:00
|
|
|
|
2015-03-05 14:54:12 +00:00
|
|
|
(define (spawn-timer-driver)
|
|
|
|
(define control-ch (make-channel))
|
|
|
|
(thread (lambda () (timer-driver-thread-main control-ch)))
|
2017-10-03 09:49:48 +00:00
|
|
|
(define (timer-driver e state)
|
|
|
|
(match-define (timer-state old-active-count old-interrupt-counter) state)
|
2015-03-05 14:54:12 +00:00
|
|
|
(match e
|
Switch from timer-expired /messages/ to /assertions/ at ground level.
Previously, the timer driver caused the background thread to call
send-ground-message to indicate that a timer had expired. However,
this can lead to a race! In cases where a timer expires very soon, the
channel-put of the set-timer instruction leads shortly thereafter to a
send-ground-message which then races the establishment of the
metalevel-1 subscription to the timer-expired events that are coming
from the background thread.
The race cannot occur in the sequential implementation because the
network makes sure to enqueue the transition actions resulting from
the set-timer message delivery ahead of any enqueueing of the
timer-expired ground message, so that by the time the ground message
is processed, the relevant subscription always exists.
In a looser implementation, however, this level of synchronised
activity may not exist, and the ground message may overtake the
subscription establishment.
Therefore, I've changed the driver to instead use ground /assertions/
to signal expired timers. Upon processing of such an assertion, the
driver cleans it up. This is very similar to hardware interrupts,
where the driver has to "clear the interrupt" in order to let the
system continue properly.
2016-01-21 22:38:12 +00:00
|
|
|
[(patch added _removed)
|
2017-10-03 09:49:48 +00:00
|
|
|
;; 21 Jan 2016.
|
|
|
|
;;
|
Switch from timer-expired /messages/ to /assertions/ at ground level.
Previously, the timer driver caused the background thread to call
send-ground-message to indicate that a timer had expired. However,
this can lead to a race! In cases where a timer expires very soon, the
channel-put of the set-timer instruction leads shortly thereafter to a
send-ground-message which then races the establishment of the
metalevel-1 subscription to the timer-expired events that are coming
from the background thread.
The race cannot occur in the sequential implementation because the
network makes sure to enqueue the transition actions resulting from
the set-timer message delivery ahead of any enqueueing of the
timer-expired ground message, so that by the time the ground message
is processed, the relevant subscription always exists.
In a looser implementation, however, this level of synchronised
activity may not exist, and the ground message may overtake the
subscription establishment.
Therefore, I've changed the driver to instead use ground /assertions/
to signal expired timers. Upon processing of such an assertion, the
driver cleans it up. This is very similar to hardware interrupts,
where the driver has to "clear the interrupt" in order to let the
system continue properly.
2016-01-21 22:38:12 +00:00
|
|
|
;; Previously, this driver caused the background thread to call
|
|
|
|
;; send-ground-message to indicate that a timer had expired.
|
|
|
|
;; However, this can lead to a race! In cases where a timer
|
|
|
|
;; expires very soon, the channel-put of the set-timer
|
|
|
|
;; instruction leads shortly thereafter to a
|
|
|
|
;; send-ground-message which then races the establishment of
|
|
|
|
;; the metalevel-1 subscription to the timer-expired events
|
|
|
|
;; that are coming from the background thread.
|
|
|
|
;;
|
|
|
|
;; The race cannot occur in the sequential implementation
|
2016-04-07 07:42:54 +00:00
|
|
|
;; because the dataspace makes sure to enqueue the transition
|
Switch from timer-expired /messages/ to /assertions/ at ground level.
Previously, the timer driver caused the background thread to call
send-ground-message to indicate that a timer had expired. However,
this can lead to a race! In cases where a timer expires very soon, the
channel-put of the set-timer instruction leads shortly thereafter to a
send-ground-message which then races the establishment of the
metalevel-1 subscription to the timer-expired events that are coming
from the background thread.
The race cannot occur in the sequential implementation because the
network makes sure to enqueue the transition actions resulting from
the set-timer message delivery ahead of any enqueueing of the
timer-expired ground message, so that by the time the ground message
is processed, the relevant subscription always exists.
In a looser implementation, however, this level of synchronised
activity may not exist, and the ground message may overtake the
subscription establishment.
Therefore, I've changed the driver to instead use ground /assertions/
to signal expired timers. Upon processing of such an assertion, the
driver cleans it up. This is very similar to hardware interrupts,
where the driver has to "clear the interrupt" in order to let the
system continue properly.
2016-01-21 22:38:12 +00:00
|
|
|
;; actions resulting from the set-timer message delivery ahead
|
|
|
|
;; of any enqueueing of the timer-expired ground message, so
|
|
|
|
;; that by the time the ground message is processed, the
|
|
|
|
;; relevant subscription always exists.
|
|
|
|
;;
|
|
|
|
;; In a looser implementation, however, this level of
|
|
|
|
;; synchronised activity may not exist, and the ground message
|
|
|
|
;; may overtake the subscription establishment.
|
|
|
|
;;
|
|
|
|
;; Therefore, I've changed the driver to instead use ground
|
|
|
|
;; /assertions/ to signal expired timers. Upon processing of
|
|
|
|
;; such an assertion, the driver cleans it up. This is very
|
|
|
|
;; similar to hardware interrupts, where the driver has to
|
|
|
|
;; "clear the interrupt" in order to let the system continue
|
|
|
|
;; properly.
|
2017-10-03 09:49:48 +00:00
|
|
|
;;
|
|
|
|
;; 3 Oct 2017.
|
|
|
|
;;
|
|
|
|
;; An additional kind of race can happen, even with ground
|
|
|
|
;; patches! If enough external ground-events spam the queue,
|
|
|
|
;; AND some other party is setting timers fast enough, AND some
|
|
|
|
;; listener for timer-expired at a particular label is
|
|
|
|
;; persistent, THEN this actor will observe a "stale"
|
|
|
|
;; interrupt, leading to duplicate timer-expired messages. This
|
|
|
|
;; happens because our clearing of the interrupt assertions is
|
|
|
|
;; delayed for long enough for the set-timer message to come in
|
|
|
|
;; and reestablish interest in interrupt assertions, before the
|
|
|
|
;; old ones have had a change to be retracted, so they appear
|
|
|
|
;; again.
|
|
|
|
;;
|
|
|
|
;; The fix relies on (a) no patch coalescing and (b) no
|
|
|
|
;; reordering of events. Each interrupt is given a number which
|
|
|
|
;; monotonically increases. We remember the most recent counter
|
|
|
|
;; we have processed, and ignore interrupts numbered at or
|
|
|
|
;; below it.
|
|
|
|
;;
|
|
|
|
;; This could be expressed using range matching if our tries
|
|
|
|
;; supported that: we'd express interest in ALL counter values,
|
|
|
|
;; retracting interest in them one at a time LOCALLY at the
|
|
|
|
;; same time we sent our (potentially heavily delayed) ground
|
|
|
|
;; patch on its way. We'd be relying on compression of adjacent
|
|
|
|
;; sequence numbers to keep the tries trim.
|
|
|
|
;;
|
|
|
|
;; Absent that, our hacky depend-on-no-coalescing-or-reordering
|
|
|
|
;; solution will do.
|
|
|
|
;;
|
|
|
|
(define-values (active-count interrupt-counter actions interrupt-clearing-patch)
|
|
|
|
(for/fold [(active-count old-active-count)
|
|
|
|
(interrupt-counter old-interrupt-counter)
|
|
|
|
(actions '())
|
2016-03-12 16:54:31 +00:00
|
|
|
(interrupt-clearing-patch patch-empty)]
|
2017-10-03 09:49:48 +00:00
|
|
|
[(interrupt (trie-project/set/single added timer-interrupt-projection))]
|
|
|
|
(match-define (timer-interrupt candidate-interrupt-counter now sealed-labels) interrupt)
|
|
|
|
(define labels (seal-contents sealed-labels))
|
|
|
|
(if (> candidate-interrupt-counter old-interrupt-counter)
|
|
|
|
(values (- active-count (length labels))
|
|
|
|
(max candidate-interrupt-counter interrupt-counter)
|
|
|
|
(cons actions (for/list [(label labels)]
|
|
|
|
(message (timer-expired label now))))
|
|
|
|
(patch-seq interrupt-clearing-patch (retract interrupt)))
|
|
|
|
(values active-count interrupt-counter actions interrupt-clearing-patch))))
|
Switch from timer-expired /messages/ to /assertions/ at ground level.
Previously, the timer driver caused the background thread to call
send-ground-message to indicate that a timer had expired. However,
this can lead to a race! In cases where a timer expires very soon, the
channel-put of the set-timer instruction leads shortly thereafter to a
send-ground-message which then races the establishment of the
metalevel-1 subscription to the timer-expired events that are coming
from the background thread.
The race cannot occur in the sequential implementation because the
network makes sure to enqueue the transition actions resulting from
the set-timer message delivery ahead of any enqueueing of the
timer-expired ground message, so that by the time the ground message
is processed, the relevant subscription always exists.
In a looser implementation, however, this level of synchronised
activity may not exist, and the ground message may overtake the
subscription establishment.
Therefore, I've changed the driver to instead use ground /assertions/
to signal expired timers. Upon processing of such an assertion, the
driver cleans it up. This is very similar to hardware interrupts,
where the driver has to "clear the interrupt" in order to let the
system continue properly.
2016-01-21 22:38:12 +00:00
|
|
|
(send-ground-patch interrupt-clearing-patch)
|
2017-10-03 09:49:48 +00:00
|
|
|
(transition (timer-state active-count interrupt-counter)
|
|
|
|
(cons actions (when (zero? active-count)
|
|
|
|
(unsub (inbound (timer-interrupt ? ? ?))))))]
|
2015-03-05 14:54:12 +00:00
|
|
|
[(message (and instruction (set-timer _ _ _)))
|
|
|
|
(channel-put control-ch instruction)
|
2017-10-03 09:49:48 +00:00
|
|
|
(transition (timer-state (+ old-active-count 1) old-interrupt-counter)
|
|
|
|
(when (= old-active-count 0) (sub (inbound (timer-interrupt ? ? ?)))))]
|
2015-03-05 14:54:12 +00:00
|
|
|
[_ #f]))
|
2017-02-15 23:18:19 +00:00
|
|
|
(actor #:name 'drivers/timer
|
2016-07-30 17:02:07 +00:00
|
|
|
timer-driver
|
2017-10-03 09:49:48 +00:00
|
|
|
(timer-state 0 -1)
|
2015-12-03 20:53:07 +00:00
|
|
|
(patch-seq (sub (set-timer ? ? ?))
|
|
|
|
(pub (timer-expired ? ?)))))
|
2015-03-05 14:54:12 +00:00
|
|
|
|
|
|
|
(define (timer-driver-thread-main control-ch)
|
|
|
|
(define heap (make-timer-heap))
|
2017-10-03 09:49:48 +00:00
|
|
|
(let loop ((interrupt-counter 0))
|
2015-03-05 14:54:12 +00:00
|
|
|
(sync (match (next-timer heap)
|
|
|
|
[#f never-evt]
|
2017-10-03 09:49:48 +00:00
|
|
|
[t (handle-evt (alarm-evt (pending-timer-deadline t))
|
|
|
|
(lambda (_dummy)
|
|
|
|
(define now (current-inexact-milliseconds))
|
|
|
|
(define labels (fire-timers! heap now))
|
Switch from timer-expired /messages/ to /assertions/ at ground level.
Previously, the timer driver caused the background thread to call
send-ground-message to indicate that a timer had expired. However,
this can lead to a race! In cases where a timer expires very soon, the
channel-put of the set-timer instruction leads shortly thereafter to a
send-ground-message which then races the establishment of the
metalevel-1 subscription to the timer-expired events that are coming
from the background thread.
The race cannot occur in the sequential implementation because the
network makes sure to enqueue the transition actions resulting from
the set-timer message delivery ahead of any enqueueing of the
timer-expired ground message, so that by the time the ground message
is processed, the relevant subscription always exists.
In a looser implementation, however, this level of synchronised
activity may not exist, and the ground message may overtake the
subscription establishment.
Therefore, I've changed the driver to instead use ground /assertions/
to signal expired timers. Upon processing of such an assertion, the
driver cleans it up. This is very similar to hardware interrupts,
where the driver has to "clear the interrupt" in order to let the
system continue properly.
2016-01-21 22:38:12 +00:00
|
|
|
(send-ground-patch
|
2017-10-03 09:49:48 +00:00
|
|
|
(assert (timer-interrupt interrupt-counter now (seal labels))))
|
|
|
|
(loop (+ interrupt-counter 1))))])
|
2015-03-05 14:54:12 +00:00
|
|
|
(handle-evt control-ch
|
|
|
|
(match-lambda
|
|
|
|
[(set-timer label msecs 'relative)
|
|
|
|
(install-timer! heap label (+ (current-inexact-milliseconds) msecs))
|
2017-10-03 09:49:48 +00:00
|
|
|
(loop interrupt-counter)]
|
2015-03-05 14:54:12 +00:00
|
|
|
[(set-timer label msecs 'absolute)
|
|
|
|
(install-timer! heap label msecs)
|
2017-10-03 09:49:48 +00:00
|
|
|
(loop interrupt-counter)]
|
2015-03-05 14:54:12 +00:00
|
|
|
['quit (void)])))))
|
|
|
|
|
|
|
|
(define (make-timer-heap)
|
|
|
|
(make-heap (lambda (t1 t2) (<= (pending-timer-deadline t1) (pending-timer-deadline t2)))))
|
|
|
|
|
|
|
|
(define (next-timer heap)
|
|
|
|
(and (positive? (heap-count heap))
|
|
|
|
(heap-min heap)))
|
|
|
|
|
|
|
|
(define (fire-timers! heap now)
|
|
|
|
(if (zero? (heap-count heap))
|
|
|
|
'()
|
|
|
|
(let ((m (heap-min heap)))
|
|
|
|
(if (<= (pending-timer-deadline m) now)
|
|
|
|
(begin (heap-remove-min! heap)
|
2017-10-03 09:49:48 +00:00
|
|
|
(cons (pending-timer-label m)
|
2015-03-05 14:54:12 +00:00
|
|
|
(fire-timers! heap now)))
|
|
|
|
'()))))
|
|
|
|
|
|
|
|
(define (install-timer! heap label deadline)
|
|
|
|
(heap-add! heap (pending-timer deadline label)))
|
|
|
|
|
2016-07-12 19:05:56 +00:00
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
|
|
|
(spawn-timer-driver)
|