From: "Eregon (Benoit Daloze)" Date: 2022-11-22T11:18:47+00:00 Subject: [ruby-core:110853] [Ruby master Feature#19141] Add thread-owned Monitor to protect thread-local resources Issue #19141 has been updated by Eregon (Benoit Daloze). It's not only an issue with Fiber scheduler although that is the most obvious way to illustrate. After all Fiber scheduler is not the only usage of `resume`/`transfer`/`yield`, any gem or app might use those as well and not always in clearly-defined places / in a structured concurrency manner. Suppose we have a Thread::Monitor, in the example below (with no Fiber scheduler) we end up in two bad cases: * holding the lock after the .synchronize{} returned * unlocking the lock preemptively, potentially breaking the resource How do we address this with Thread::Monitor? With Fiber-based `Monitor` there is a clear exception as soon as entering `f`. ```ruby M = Thread::Monitor.new f = Fiber.new { M.synchronize { # using protected resource Fiber.yield # or other_fiber.transfer # using protected resource } } def transaction(m) t = Transaction.new M.synchronize { yield } ensure t.commit_or_abort end transaction { f.resume } # M is still held by f or by main Fiber, that could cause confusing deadlocks transaction { f.resume # M is not held here! # now another Fiber/Thread could corrupt the resource some_more_logic } ``` ---------------------------------------- Feature #19141: Add thread-owned Monitor to protect thread-local resources https://bugs.ruby-lang.org/issues/19141#change-100211 * Author: wildmaples (Maple Ong) * Status: Open * Priority: Normal ---------------------------------------- ### Background In Ruby v3.0.2, Monitor was modified to be owned by fibers instead of threads [for reasons as described in this issue](https://bugs.ruby-lang.org/issues/17827) and so it is also consistent with Mutex. Before the change to Monitor, Mutex was modified to per-fiber instead of thread ([issue](https://bugs.ruby-lang.org/issues/16792), [PR](https://github.com/ruby/ruby/commit/178c1b0922dc727897d81d7cfe9c97d5ffa97fd9)) which caused some problems (See: [comment](https://bugs.ruby-lang.org/issues/17827#note-1)). ### Problem We are now encountering a problem where using Enumerator (implemented transparently using Fiber, so the user is not aware) within a Fiber-owned process, which causes a deadlock. That means any framework using Monitor is incompatible to be used with Enumerator. In general, there are many types of thread-local resources (connections for example), so it would make sense to have a thread-owned monitor to protect them. I think few resources are really fiber-owned. #### Specifics * Concurrent Ruby is still designed with per-thread locking, which causes similar incompatibilities. (Read: [issue](https://github.com/ruby-concurrency/concurrent-ruby/issues/962)) * Systems test in Rails implements locking using Monitor, resulting in deadlock in these known cases: * when cache clearing (Read: [issue](https://github.com/rails/rails/issues/45994)) * database transactions when used with Enumerator (Read: [comment](https://github.com/rails/rails/issues/45994#issuecomment-1304306575)) ### Demo ```ruby # ruby 2.7.6p219 (2022-04-12 revision c9c2245c0a) [arm64-darwin21] # Thread #, fiber #, locked true, owned true # Thread #, fiber #, locked true, owned true # ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21] # Thread #, fiber #, locked true, owned true # Thread #, fiber #, locked true, owned false require 'fiber' require 'monitor' puts RUBY_DESCRIPTION # We have a single monitor - we're pretending it protects some thread-local resources m = Monitor.new # We'll create an explicit thread t = Thread.new do # Lock to protect our thread-local resource m.enter puts "Thread #{Thread.current}, fiber #{Fiber.current}, locked #{m.mon_locked?}, owned #{m.mon_owned?}" # The Enumerator here creates a fiber, which runs on the same thread, so would want to use the same thread-local resource e = Enumerator.new do |y| # In 2.7 this is fine, in 3.0 it's not, as the fiber thinks it doesn't have the lock puts "Thread #{Thread.current}, fiber #{Fiber.current}, locked #{m.mon_locked?}, owned #{m.mon_owned?}" # This would deadlock # m.enter y.yield 1 end e.next end t.join ``` ### Possible Solutions * Allow `Monitor` to be per thread or fiber through a flag * Having `Thread::Monitor` and `Fiber::Monitor` as two separate classes. Leave `Monitor` as it is right now. However, this may not be possible due to the `Thread::Mutex` alias These options would give us more flexibility in which type of Monitor to use. -- https://bugs.ruby-lang.org/ Unsubscribe: