Summary
On facebookbusiness 25.0.2 (Ruby 3.3), the first concurrent reference to the SDK's ad_object classes permanently deadlocks the process on Ruby's autoload/require lock. No exception is raised — the threads hang forever — so it's a silent failure that hits any multithreaded host (Sidekiq, Puma, parallel test runners).
Reproduction
One command — run as plain Ruby, not inside a Rails console (Rails' own autoloader wraps require and masks the gem-level behavior):
gem install facebookbusiness
ruby -e '
$stdout.sync = true
require "facebook_ads"
n = %w[AdAccount CustomAudience Campaign AdSet Ad AdCreative Business Page Event Photo Album Group User AdsPixel AdVideo Comment AdLabel ProductCatalog]
t = n.map { |c| Thread.new { FacebookAds.const_get(c) } }
sleep 5
puts "stuck #{t.count(&:alive?)}/#{t.size} (deadlock if > 0)"
exit!
'
On 25.0.2 + Ruby 3.3 this prints, every run:
stuck 18/18 (deadlock if > 0)
All 18 threads are wedged in the autoload — backtraces are parked at:
.../facebook_ads/field_types.rb:36:in `const_get'
.../facebook_ads/field_types.rb:36:in `for'
.../facebook_ads/param_set.rb:16:in `has_param'
.../facebook_ads/fields.rb:26:in `field'
Warming the classes single-threaded first — one n.each { |c| FacebookAds.const_get(c) } pass before the threads — prints stuck 0/18. So it's the concurrent first load that deadlocks, not the classes themselves:
ruby -e '
$stdout.sync = true
require "facebook_ads"
n = %w[AdAccount CustomAudience Campaign AdSet Ad AdCreative Business Page Event Photo Album Group User AdsPixel AdVideo Comment AdLabel ProductCatalog]
n.each { |c| FacebookAds.const_get(c) } # warm the autoload graph, single-threaded
t = n.map { |c| Thread.new { FacebookAds.const_get(c) } }
sleep 5
puts "stuck #{t.count(&:alive?)}/#{t.size} (deadlock if > 0)"
exit!
'
(The exit! is only there so the script terminates instead of hanging on the wedged threads at exit.)
Root cause
lib/facebook_ads.rb registers every ad_object class with Ruby autoload.
- A class body's
field / has_param declarations resolve their types at definition time via FieldTypes.for → FacebookAds.const_get(type_spec) (field_types.rb:36), which triggers the autoload of the referenced class — whose body resolves its fields, autoloading more. The graph is large (~1000 classes) and circularly interdependent.
- Ruby
autoload is not safe for concurrent loading of interdependent files: two threads autoloading constants whose files transitively require each other each hold one in-progress load and block on the other — a circular wait on the global require lock that never resolves.
Because a deadlocked thread raises nothing, there's no error or alert; the process just stops making progress on anything touching the SDK.
Impact
Any multithreaded Ruby host that first-references SDK classes concurrently. It's a cold-start effect — a fresh process is vulnerable only until the graph is fully loaded — so it surfaces right after boots/deploys and looks intermittent (only processes that lose the race wedge).
Suggested fix
Provide a public FacebookAds.eager_load! (or load the ad_object graph at require time) so applications can warm it single-threaded at boot; or serialize FieldTypes.for / autoload resolution behind a mutex.
Workaround
Eager-load the graph once at boot, single-threaded, before spawning worker threads:
gem_dir = Gem::Specification.find_by_name("facebookbusiness").gem_dir
Dir[File.join(gem_dir, "lib/facebook_ads/ad_objects/**/*.rb")].sort.each do |f|
require f
rescue ScriptError, StandardError => e
warn "skipped #{File.basename(f)}: #{e.class}"
end
Minor, separate: that sweep also surfaces 6 files that ship with SyntaxErrors and appear deprecated: ad_campaign_group.rb, ad_campaign_placement.rb, adgroup.rb, articles_and_publications_item.rb, product_catalog_apps_and_software_item.rb, product_catalog_media_title.rb.
Summary
On
facebookbusiness25.0.2 (Ruby 3.3), the first concurrent reference to the SDK'sad_objectclasses permanently deadlocks the process on Ruby's autoload/require lock. No exception is raised — the threads hang forever — so it's a silent failure that hits any multithreaded host (Sidekiq, Puma, parallel test runners).Reproduction
One command — run as plain Ruby, not inside a Rails console (Rails' own autoloader wraps
requireand masks the gem-level behavior):On 25.0.2 + Ruby 3.3 this prints, every run:
All 18 threads are wedged in the autoload — backtraces are parked at:
Warming the classes single-threaded first — one
n.each { |c| FacebookAds.const_get(c) }pass before the threads — printsstuck 0/18. So it's the concurrent first load that deadlocks, not the classes themselves:(The
exit!is only there so the script terminates instead of hanging on the wedged threads at exit.)Root cause
lib/facebook_ads.rbregisters everyad_objectclass with Rubyautoload.field/has_paramdeclarations resolve their types at definition time viaFieldTypes.for→FacebookAds.const_get(type_spec)(field_types.rb:36), which triggers the autoload of the referenced class — whose body resolves its fields, autoloading more. The graph is large (~1000 classes) and circularly interdependent.autoloadis not safe for concurrent loading of interdependent files: two threads autoloading constants whose files transitively require each other each hold one in-progress load and block on the other — a circular wait on the global require lock that never resolves.Because a deadlocked thread raises nothing, there's no error or alert; the process just stops making progress on anything touching the SDK.
Impact
Any multithreaded Ruby host that first-references SDK classes concurrently. It's a cold-start effect — a fresh process is vulnerable only until the graph is fully loaded — so it surfaces right after boots/deploys and looks intermittent (only processes that lose the race wedge).
Suggested fix
Provide a public
FacebookAds.eager_load!(or load thead_objectgraph atrequiretime) so applications can warm it single-threaded at boot; or serializeFieldTypes.for/ autoload resolution behind a mutex.Workaround
Eager-load the graph once at boot, single-threaded, before spawning worker threads:
Minor, separate: that sweep also surfaces 6 files that ship with
SyntaxErrors and appear deprecated:ad_campaign_group.rb,ad_campaign_placement.rb,adgroup.rb,articles_and_publications_item.rb,product_catalog_apps_and_software_item.rb,product_catalog_media_title.rb.