class PeriodSink(rx.linq.sink.Sink): def __init__(self, parent, observer, cancel): super(Timer.PeriodSink, self).__init__(observer, cancel) self.parent = parent self.pendingTickCount = Atomic(0) def run(self): if self.parent.isAbsolute: return self.parent.scheduler.scheduleWithAbsoluteAndState( None, self.parent.dueTime, self.invokeStart) else: dueTime = self.parent.dueTime if dueTime == self.parent.period: return self.parent.scheduler.schedulePeriodicWithState( 0, self.parent.period, self.tick) return self.parent.scheduler.scheduleWithRelativeAndState( None, dueTime, self.invokeStart) def tick(self, count): self.observer.onNext(count) return count + 1 def invokeStart(self, scheduler, state): # # Notice the first call to OnNext will introduce skew if it takes significantly long when # using the following naive implementation: # # Code: base._observer.OnNext(0L); # return self.SchedulePeriodicEmulated(1L, _period, (Func<long, long>)Tick); # # What we're saying here is that Observable.Timer(dueTime, period) is pretty much the same # as writing Observable.Timer(dueTime).Concat(Observable.Interval(period)). # # Expected: dueTime # | # 0--period--1--period--2--period--3--period--4--... # | # +-OnNext(0L)-| # # Actual: dueTime # | # 0------------#--period--1--period--2--period--3--period--4--... # | # +-OnNext(0L)-| # # Different solutions for this behavior have different problems: # # 1. Scheduling the periodic job first and using an AsyncLock to serialize the OnNext calls # has the drawback that InvokeStart may never return. This happens when every callback # doesn't meet the period's deadline, hence the periodic job keeps queueing stuff up. In # this case, InvokeStart stays the owner of the AsyncLock and the call to Wait will never # return, thus not allowing any interleaving of work on this scheduler's logical thread. # # 2. Scheduling the periodic job first and using a (blocking) synchronization primitive to # signal completion of the OnNext(0L) call to the Tick call requires quite a bit of state # and careful handling of the case when OnNext(0L) throws. What's worse is the blocking # behavior inside Tick. # # In order to avoid blocking behavior, we need a scheme much like SchedulePeriodic emulation # where work to dispatch OnNext(n + 1) is delegated to a catch up loop in case OnNext(n) was # still running. Because SchedulePeriodic emulation exhibits such behavior in all cases, we # only need to deal with the overlap of OnNext(0L) with future periodic OnNext(n) dispatch # jobs. In the worst case where every callback takes longer than the deadline implied by the # period, the periodic job will just queue up work that's dispatched by the tail-recursive # catch up loop. In the best case, all work will be dispatched on the periodic scheduler. # # # We start with one tick pending because we're about to start doing OnNext(0L). # self.pendingTickCount.value = 1 d = SingleAssignmentDisposable() self.periodic = d d.disposable = scheduler.schedulePeriodicWithState( 1, self.parent.period, self.tock) try: self.observer.onNext(0) except Exception as e: d.dispose() raise e # # If the periodic scheduling job already ran before we finished dispatching the OnNext(0L) # call, we'll find pendingTickCount to be > 1. In this case, we need to catch up by dispatching # subsequent calls to OnNext as fast as possible, but without running a loop in order to ensure # fair play with the scheduler. So, we run a tail-recursive loop in CatchUp instead. # if self.pendingTickCount.dec() > 0: c = SingleAssignmentDisposable() c.disposable = scheduler.scheduleRecursiveWithState( 1, self.catchUp) return CompositeDisposable(d, c) return d def tock(self, count): if self.pendingTickCount.inc() == 1: self.observer.onNext(count) self.pendingTickCount.dec() return count + 1 def catchUp(self, count, recurse): try: self.observer.onNext(count) except Exception as e: self.periodic.dispose() raise e # # We can simply bail out if we decreased the tick count to 0. In that case, the Tock # method will take over when it sees the 0 -> 1 transition. # if self.pendingTickCount.dec() > 0: recurse(count + 1)