Last active
January 20, 2025 20:42
-
-
Save bdevel/4955559603388e083d682f50365283bb to your computer and use it in GitHub Desktop.
A Ruby debounce utility that does not wait around and does not use threads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A debounce utility that does not wait around and does not use threads. | |
# | |
# Use DebounceFlusher.flush_on_finish{} to wrap any code | |
# that might call DebounceFlusher.run{} | |
# When the flush_on_finish block is complete, then any queued #run calls | |
# will be de-duplicated by the name provided to #run. | |
# The last call to #run for a specific name will be executed. | |
# | |
# NOTE, only one Thread will be able to execute this at a time due to the queue scope. | |
# Other threads will wait. | |
# | |
# Also, any spawned thread that calls #run after the queue is closed | |
# will be executed immediately with a warning. | |
# The app should have waited for the thread to complete | |
# instead of sending it off without calling .join | |
# | |
# Test with: DebounceFlusher.demo | |
class DebounceFlusher | |
def self.memory | |
# Store on the main thread since it's using class methods | |
# that can be called from anywhere, it cannot be sure which nested thread | |
# is running the flush_on_finish() block, where the queue variable would be in scope. | |
Thread.main[self.to_s] ||= {mutex: Thread::Mutex.new, | |
queue: Thread::Queue.new()} | |
end | |
def self.flush_on_finish(&block) | |
if memory[:mutex].locked? | |
block.call | |
else | |
memory[:mutex].synchronize do | |
begin | |
block.call # load up the queue with #runs | |
ensure | |
# If an exception is thrown in the block call, | |
# then this will ensure that all the calls to run | |
# will be executed. | |
# here, any thread still running that calls #run will be executed immediately | |
memory[:queue].close | |
# group the queue items by name so last proc is used | |
table = {} | |
while i = memory[:queue].pop #.each do |name, to_debounce| | |
name, xproc = i | |
table[name] = xproc | |
end | |
# call each run proc | |
table.each do |name, xproc| | |
xproc.call | |
end | |
# reset | |
memory[:queue] = Thread::Queue.new | |
end | |
end | |
end | |
end | |
def self.run(name, &block) | |
if !memory[:mutex].locked? | |
# run now if not inside of a flush block | |
block.call | |
elsif memory[:queue].closed? | |
# Don't warn if it was called from within the flush_on_finish queue execution. | |
traces = Thread.current.backtrace | |
unless traces.any?{|t|t.include?('flush_on_finish')} | |
msg = "Method #{self.to_s}#run with name=#{name.inspect} was called while the queue is closed. Will run proc now but the Thread should have called .join #{Thread.current}. Trace:\n" + traces.take(10).join("\n") | |
if Object.const_defined?('Rails') | |
Rails.logger.warn(msg) | |
else | |
puts "WARNING: #{msg}" | |
end | |
end | |
block.call | |
else | |
memory[:queue] << [name, block] | |
end | |
end | |
# ================================================================================ | |
def self.demo | |
DebounceFlusher.run('t'){puts "t0 outside running"} | |
DebounceFlusher.flush_on_finish do | |
DebounceFlusher.flush_on_finish do | |
DebounceFlusher.run('t'){puts "t0 running"} | |
end | |
threads = [] | |
threads << Thread.new do | |
sleep 1 | |
puts 't1 adding' | |
DebounceFlusher.run('t'){puts "t1 running"} | |
end | |
threads << Thread.new do | |
sleep 2 | |
puts 't2 adding' | |
DebounceFlusher.run('t'){puts "t2 running"} | |
end | |
threads << Thread.new do | |
sleep 3 | |
t = Thread.new do | |
sleep 1.5; | |
puts 't3 adding'; | |
DebounceFlusher.run('t') do | |
puts "t3 running" | |
DebounceFlusher.run('t'){puts 't3-sub running!'} | |
end | |
end | |
t.join | |
end | |
threads.each &:join | |
puts 'done' | |
end | |
puts 'completed' | |
end | |
end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment