Skip to content

Instantly share code, notes, and snippets.

@bdevel
Last active January 20, 2025 20:42
Show Gist options
  • Save bdevel/4955559603388e083d682f50365283bb to your computer and use it in GitHub Desktop.
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.
# 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