Skip to content

ConcurrentModificationException in subclasses map #8602

@headius

Description

@headius

Discovered by @kalenp while attempting to deploy 9.4.11.0 to their test environments.

It appears at least one call path into the new subclasses collections (Map.remove) was not properly synchronized, which can lead to ConcurrentModificationException under concurrent load and modification of the class hierarchy.

The following test is sufficient to reproduce the exception most of the time. There are many different types of class hierarchy modification here and several attempts to force traversal of all subclasses, so I'm unsure which operations specifically can trigger this error. The test is intended to try everything under reasonably high concurrent load.

require 'test/unit'

class TestClassSubclasses < Test::Unit::TestCase
  def test_concurrent_modification
    c = Class.new
    c2 = Class.new(c)
    go = false

    thread_count = 100
    threads = thread_count.times.map {
      Thread.new {
        Thread.pass until go
        c3 = Class.new(c2)
        o = c3.new
        c.class_eval {
          def foo; end
        }
        class << o
          extend Enumerable
          prepend Comparable
        end
        c.class_eval {
          def foo; end
        }
        c3.class_eval {
          include Enumerable
          prepend Comparable
        }
        c.class_eval {
          def foo; end
        }
        :success
      }
    }

    go = true
    assert_equal(threads.map(&:value), [:success] * thread_count)
  end
end

A high percentage of runs of this test will produce output similar to the following:

$ jruby test/jruby/test_class_subclasses.rb
Loaded suite test/jruby/test_class_subclasses
Started
java.util.ConcurrentModificationException
	at java.base/java.util.WeakHashMap.forEach(WeakHashMap.java:1037)
	at org.jruby.dist/org.jruby.RubyClass$SubclassesWeakMap.forEach(RubyClass.java:1191)
	at org.jruby.dist/org.jruby.RubyClass.invalidateCacheDescendants(RubyClass.java:1255)
	at org.jruby.dist/org.jruby.RubyClass.lambda$invalidateCacheDescendants$5(RubyClass.java:1255)
	at java.base/java.util.WeakHashMap.forEach(WeakHashMap.java:java.util.ConcurrentModificationException
	at java.base/java.util.WeakHashMap.forEach(WeakHashMap.java:1037)
	at org.jruby.dist/org.jruby.RubyClass$SubclassesWeakMap.forEach(RubyClass.java:1191)
	at org.jruby.dist/org.jruby.RubyClass.invalidateCacheDescendants(RubyClass.java:1255)
	at org.jruby.dist/org.jruby.RubyClass.lambda$invalidateCacheDescendants$5(RubyClass.java:1255)
...many more lines of similar output
	at org.jruby.dist/org.jruby.runtime.CompiledIRBlockBody.callDirect(CompiledIRBlockBody.java:141)
	at org.jruby.dist/org.jruby.runtime.IRBlockBody.call(IRBlockBody.java:64)
	at org.jruby.dist/org.jruby.runtime.IRBlockBody.call(IRBlockBody.java:58)
	at org.jruby.dist/org.jruby.runtime.Block.call(Block.java:144)
	at org.jruby.dist/org.jruby.RubyProc.call(RubyProc.java:354)
	at org.jruby.dist/org.jruby.internal.runtime.RubyRunnable.run(RubyRunnable.java:111)
	at java.base/java.lang.Thread.run(Thread.java:1583)
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 tests, 0 assertions, 0 failures, 1 errors, 0 pendings, 0 omissions, 0 notifications
0% passed
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
17.62 tests/s, 0.00 assertions/s

I have a fix in progress.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions