Skip to content

Commit

Permalink
add a sliding window where entries slide off after a period of time
Browse files Browse the repository at this point in the history
  • Loading branch information
RyanAD committed Apr 2, 2020
1 parent 421f5b5 commit d5fb106
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 0 deletions.
88 changes: 88 additions & 0 deletions lib/semian/time_sliding_window.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'thread'

module Semian
module Simple

class TimeSlidingWindow #:nodoc:
extend Forwardable

def_delegators :@window, :size, :empty?, :length
attr_reader :time_window_millis

Pair = Struct.new(:head, :tail)

# A sliding window is a structure that stores the most recent entries that were pushed within the last slice of time
def initialize(time_window)
@time_window_millis = time_window * 1000
@window = []
end

def count(arg)
vals = @window.map { |pair| pair.tail}
vals.count(arg)
end

def push(value)
remove_old # make room
@window << Pair.new(current_time, value)
self
end

alias_method :<<, :push

def clear
@window.clear
self
end

def last
@window.last&.tail
end

def remove_old
midtime = current_time - time_window_millis
# special case, everything is too old
@window.clear if !@window.empty? and @window.last.head < midtime
# otherwise we find the index position where the cutoff is
idx = (0...@window.size).bsearch { |n| @window[n].head >= midtime }
@window.slice!(0, idx) if idx
end

alias_method :destroy, :clear

private

def current_time
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
end
end
end

module ThreadSafe
class TimeSlidingWindow < Simple::TimeSlidingWindow
def initialize(*)
super
@lock = Mutex.new
end

# #size, #last, and #clear are not wrapped in a mutex. For the first two,
# the worst-case is a thread-switch at a timing where they'd receive an
# out-of-date value--which could happen with a mutex as well.
#
# As for clear, it's an all or nothing operation. Doesn't matter if we
# have the lock or not.

def count(*)
@lock.synchronize { super }
end

def remove_old
@lock.synchronize { super }
end

def push(*)
@lock.synchronize { super }
end
end
end
end
64 changes: 64 additions & 0 deletions test/time_sliding_window_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require 'test_helper'

class TestTimeSlidingWindow < Minitest::Test
def setup
@sliding_window = ::Semian::ThreadSafe::TimeSlidingWindow.new(0.5) # Timecop doesn't work with a monotonic clock
@sliding_window.clear
end

def teardown
@sliding_window.destroy
end

def test_sliding_window_push
assert_equal(0, @sliding_window.size)
@sliding_window << 1
assert_sliding_window(@sliding_window, [1], 500)
@sliding_window << 5
assert_sliding_window(@sliding_window, [1, 5], 500)
end

def test_special_everything_too_old
@sliding_window << 0 << 1
sleep(0.501)
assert_sliding_window(@sliding_window, [], 500)
end


def test_sliding_window_edge_falloff
assert_equal(0, @sliding_window.size)
@sliding_window << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7
assert_sliding_window(@sliding_window, [0, 1, 2, 3, 4, 5, 6, 7], 500)
sleep(0.251)
@sliding_window << 8 << 9 << 10
sleep(0.251)
assert_sliding_window(@sliding_window, [8, 9, 10], 500)
@sliding_window << 11
sleep(0.251)
assert_sliding_window(@sliding_window, [11], 500)
end

def test_sliding_window_count
@sliding_window << true << false << true << false << true << true << true
assert_equal(5, @sliding_window.count(true))
assert_equal(2, @sliding_window.count(false))
end

def test_issue
@window = @sliding_window.instance_variable_get("@window")
@window << ::Semian::ThreadSafe::TimeSlidingWindow::Pair.new(338019700.707, true)
@window << ::Semian::ThreadSafe::TimeSlidingWindow::Pair.new(338019701.707, true)
@sliding_window << false
puts('break')
end

private

def assert_sliding_window(sliding_window, array, time_window_millis)
# Get private member, the sliding_window doesn't expose the entire array
sliding_window.remove_old
data = sliding_window.instance_variable_get("@window").map { |pair| pair.tail }
assert_equal(array, data)
assert_equal(time_window_millis, sliding_window.time_window_millis)
end
end

0 comments on commit d5fb106

Please sign in to comment.