Repository navigation
Concurrent::Map is not modeled (only the concurrent-ruby thread pool is) #647
Description
The stdlib registry models concurrent-ruby's ThreadPoolExecutor, FixedThreadPool and CyclicBarrier (src/analyze/registry/stdlib.rs:391), but not Concurrent::Map. On main, a constant assigned Concurrent::Map.new is refused, so every use of it is an error.
Repro
# app/models/widget.rb
class Widget < ApplicationRecord
LOCKS = Concurrent::Map.new
def self.lock_for(id) = LOCKS.compute_if_absent(id) { Mutex.new }
def self.forget_locks = LOCKS.clear
endIn Rails, Widget.lock_for(1) returns the same Mutex on every call, and Widget.forget_locks empties the map.
roundhouse check --continue . in the app root, main a3acfb4 (the absolute path of the temporary app is shortened to ./):
./app/models/widget.rb:4:27: error[unsupported]: constant not supported (all targets): Widget::LOCKS
./app/models/widget.rb:4:51: warning[unresolved_type]: local read `id` has unresolved type
./app/models/widget.rb:5:27: error[unsupported]: constant not supported (all targets): Widget::LOCKS
roundhouse-check: 2 gems: 1 framework, 1 infrastructure, 0 unknown
roundhouse-check: . — 0 parse error(s), 2 error(s), 1 warning(s), 0 gap-attributed note(s), 0 survey gap(s)
The release 2026.9.18 reports the method calls instead: no known method `compute_if_absent` on Concurrent::Map at 4:27 and no known method `clear` on Concurrent::Map at 5:27.
Self-contained repro.rb (bundler/inline; exits 1 while the bug is present)
ruby repro.rb needs network access for bundler/inline and the roundhouse binary (on PATH, in $ROUNDHOUSE, or at /tmp/roundhouse-bin/roundhouse). It writes the app into a temporary directory, boots it in Rails and calls Widget.lock_for twice and Widget.forget_locks, generates Gemfile.lock with bundle lock --local, then runs roundhouse check --continue . on the same files.
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails", "~> 8.0"
gem "sqlite3"
end
require "fileutils"
require "tmpdir"
APP = {
"Gemfile" => <<~RUBY,
source "https://rubygems.org"
gem "rails", "~> 8.0"
gem "sqlite3"
RUBY
"config/application.rb" => <<~RUBY,
require "rails/all"
module Repro
class Application < Rails::Application
config.load_defaults 8.0
end
end
RUBY
"config/routes.rb" => <<~RUBY,
Rails.application.routes.draw do
end
RUBY
"db/schema.rb" => <<~RUBY,
ActiveRecord::Schema[8.0].define(version: 1) do
create_table "widgets", force: :cascade do |t|
t.string "name", null: false
t.timestamps
end
end
RUBY
"app/models/application_record.rb" => <<~RUBY,
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
RUBY
"app/models/widget.rb" => <<~RUBY
class Widget < ApplicationRecord
LOCKS = Concurrent::Map.new
def self.lock_for(id) = LOCKS.compute_if_absent(id) { Mutex.new }
def self.forget_locks = LOCKS.clear
end
RUBY
}
root = Dir.mktmpdir("roundhouse-repro")
at_exit { FileUtils.remove_entry(root) }
APP.each do |path, source|
FileUtils.mkdir_p(File.dirname(File.join(root, path)))
File.write(File.join(root, path), source)
end
puts "== Rails #{Rails.version} runs the app"
ENV["RAILS_ENV"] = "test"
ENV["DATABASE_URL"] = "sqlite3::memory:"
Dir.chdir(root) { require "./config/application" }
Rails.application.config.eager_load = false
Rails.application.initialize!
ActiveRecord::Schema.verbose = false
load File.join(root, "db/schema.rb")
same_lock = Widget.lock_for(1).equal?(Widget.lock_for(1))
Widget.forget_locks
puts "Widget.lock_for(1) twice -> same Mutex: #{same_lock}; after forget_locks size: #{Widget::LOCKS.size}"
raise "unexpected result" unless same_lock && Widget::LOCKS.size.zero?
puts "== roundhouse check --continue"
Bundler.with_unbundled_env { system("bundle", "lock", "--local", chdir: root, out: File::NULL, exception: true) }
roundhouse = ENV["ROUNDHOUSE"] ||
ENV["PATH"].split(File::PATH_SEPARATOR).map { File.join(_1, "roundhouse") }.find { File.executable?(_1) } ||
"/tmp/roundhouse-bin/roundhouse"
output = IO.popen([roundhouse, "check", "--continue", "."], chdir: root, err: [:child, :out], &:read)
puts output
exit 1 if output.match?(/no known method `\w+` on Concurrent::Map/) ||
output.include?("constant not supported (all targets): Widget::LOCKS")Impact
In a Rails 8.0 app, 3 of the 335 errors on main are constant not supported (all targets): Concurrent::Map, at three @x ||= Concurrent::Map.new readers in two models. The maps hold per-record mutexes and process-local delivery state.
Expected
Concurrent::Map is typed like a Hash ([], []=, fetch, delete, clear, key?, compute_if_absent, put_if_absent, each_pair). It is the standard process-local, thread-safe cache in Rails code, and Rails uses it internally. On a target without threads it can lower to Hash.
I found no existing issue or PR for Concurrent::Map. #503 (merged 2026-10-06) is why main refuses the constant instead of the method calls.
roundhouse 2026.9.18 (2e286e6), x86_64-unknown-linux-musl release binary, and main a3acfb4 built from source with cargo build --release --locked --bin roundhouse. Ruby 3.3.10, Rails 8.1.4, sqlite3 2.9.6, Linux x86_64, 2026-10-08.
Written with Claude Code (AI assistant) on behalf of @iberianpig.
Activity
Type and run Concurrent::Map on the ruby family and Spinel