Skip to content

Concurrent first use of ad_object classes deadlocks under multithreading (autoload of interdependent class graph) #235

Description

@scottgratton

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.forFacebookAds.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions