Closed nateberkopec closed 1 year ago
This took a while to track down (but was a lot of fun too! 😄)
The root cause is that the code block of CacheCleanupThread
is scheduled to run as soon as super
is called in CacheCleanupThread#initialize
. This results in a race condition where the thread may run before the thread class instance is fully instantiated. While this seems counterintuitive, I was tipped off to the possibility here https://www.ruby-forum.com/t/thread-super-should-be-first-line-or-last-line/150617 I later proved this was happening by the code changes that fixed the problem.
There are actually two separate race conditions
CacheCleanupThread.new
. When this happens t
is undefined inside the thread block. This results in the error "undefined method `sleepy_run' for nil:NilClass" that we see in the test outputdef initialize_cleanup_thread(args = {})
cleanup_interval = args.fetch(:cleanup_interval) { CLEANUP_INTERVAL }
cleanup_cycle = args.fetch(:cleanup_cycle) { CLEANUP_CYCLE }
t = CacheCleanupThread.new(cleanup_interval, cleanup_cycle, self) do
until Thread.current[:should_exit] do
t.sleepy_run # <===== t may be undefined here!
end
end
at_exit { t[:should_exit] = true }
end
This can be fixed by changing t.sleepy_run
to Thread.current.sleepy_run
CacheCleanupThread#initialize
are defined. This results in "undefined method '*' for nil:NilClass" in should_cleanup?
(this error only occurs after the first race condition is fixed)def should_cleanup?
@cycle_count * @interval >= @cycle
end
This happens because @cycle_count
, @interval
, and @cycle
may be undefined when thread execution starts. It can be fixed by moving super
to the bottom of CacheCleanupThread#initialize
def initialize(interval, cycle, store)
@store = store
@interval = interval
@cycle = cycle
@cycle_count = 1
super
end
The race conditions causing this intermittent test output are also present in the production code, but I don't know if they actually manifest themselves?
I will open a PR shortly with the fixes