Project

General

Profile

Feature #19141

Updated by Eregon (Benoit Daloze) over 1 year ago

### 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 #<Thread:0x000000014a8eb228 demo.rb:8 run>, fiber #<Fiber:0x000000014a8eaf80 (resumed)>, locked true, owned true 
 # Thread #<Thread:0x000000014a8eb228 demo.rb:8 run>, fiber #<Fiber:0x000000014a8eacb0 demo.rb:13 (resumed)>, locked true, owned true 

 # ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21] 
 # Thread #<Thread:0x0000000102329a08 demo.rb:8 run>, fiber #<Fiber:0x0000000102329828 (resumed)>, locked true, owned true 
 # Thread #<Thread:0x0000000102329a08 demo.rb:8 run>, fiber #<Fiber:0x00000001023294e0 demo.rb:13 (resumed)>, 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.

Back