Синглтон исчезает при разветвлении процессов единорога в Rails

У меня есть приложение rails, работающее под управлением ruby ​​2.4.4, использующее Unicorn в качестве веб-сервера, который использует синглтон для чтения из Kafka в фоновом потоке. Идея состоит в том, чтобы иметь один экземпляр синглтона для каждого процесса единорога. Итак, 4 процесса, 4 синглтона.

Я запускаю потребление kafka внутри хука after_fork в моей конфигурации единорога. Я могу успешно дождаться завершения потребления исторических сообщений (подтверждено, поставив подглядывание сразу после этого).

Однако, когда я добираюсь до точки обслуживания трафика, экземпляр singleton является а) другим экземпляром и б) пустым - ранее установленный ivar исчез.

Я подтвердил, что нахожусь внутри того же процесса и того же потока.

Настройка выглядит следующим образом:

# background_foo_consumer.rb
class BackgroundFooConsumer
  include Singleton

  attr_reader :background_consumer

  def add_background_consumer(consumer, topics, options: nil)
    @background_consumer ||= BackgroundKafkaConsumer.new(consumer, topics, options: options)
  end

  def processed_historical_messages?
    background_consumer&.consumer&.reached_head
  end
end


# config/unicorn.rb
after_worker_ready do |server, worker|
  BackgroundFooConsumer.instance.add_background_consumer(nil, ["foos"])
  BackgroundFooConsumer.instance.background_consumer.start

  BackgroundFooConsumer.instance.background_consumer.consumer.mutex.synchronize {
    BackgroundFooConsumer
    .instance.background_consumer.consumer.processed_historical_messages.wait(
      BackgroundFooConsumer.instance.background_consumer.consumer.mutex
    )
  }
  end
end

Я подтвердил, что нахожусь в том же процессе, даже в том же потоке, поскольку могу успешно передать правильный объект в приложение, заменив include Singleton пользовательской реализацией и локальными переменными потока следующим образом:

# config/unicorn.rb
after_worker_ready do |server, worker|
  # ... same as above

  Thread.current[:background_foo_consumer] = BackgroundFooConsumer.instance
end


# background_foo_consumer.rb
class BackgroundFooConsumer
  attr_reader :background_consumer

  def self.instance
    @instance ||= begin
                    Thread.current[:background_foo_consumer] || self.new
                  ensure
                    Thread.current[:background_foo_consumer] = nil
                  end
  end
end

В этой реализации, когда я перехожу к обслуживанию трафика из своего приложения, BackgroundFooConsumer.instance является правильным экземпляром, созданным в хуке after_fork, и существует независимый экземпляр для каждого процесса единорога, что подтверждается проверкой идентификатора объекта.

Я не верю, что это GC, по крайней мере, базовый объект не очищается, я подтвердил это, установив локальную переменную Thread в хуке after_fork, но затем используя include Singleton в моем потребительском классе. Я все еще получаю пустой/новый синглтон, но локальная переменная потока все еще присутствует, если я запрашиваю ее напрямую.

Моя текущая гипотеза заключается в том, что это как-то связано с копированием при записи, и, устанавливая локальные переменные потока, я каким-то образом заставляю ruby ​​создать мне синглтон только для этого процесса и сохранить его в этой переменной.

Итак, мой вопрос: как экземпляр singleton может исчезнуть внутри одного потока? И как я могу предотвратить это? Я бы предпочел не использовать эти локальные переменные потока, если я могу помочь.


person timpwbaker    schedule 02.06.2020    source источник
comment
Разветвление не означает разделение памяти на самом деле, все копируется.   -  person Joel Blum    schedule 02.06.2020
comment
Конечно, как только вы записываете что-либо в общую память, она копируется, но я не хочу делиться памятью. Я хочу, чтобы память выделялась под синглтон для каждого процесса независимо.   -  person timpwbaker    schedule 02.06.2020


Ответы (1)


Ответ на это оказался из-за довольно узкой конфигурации рельсов: cache_classes. Я запускал свой сервер единорогов локально, поэтому классы не кэшировались.

Rails (при работе в каком-либо режиме, отличном от производственного режима, который обычно используется как в промежуточном, так и в производственном, но не локальном) перезагружает объекты уровня класса, если они изменяются, которые в остальном являются статичными в производственном режиме.

По сути, рельсы видели некоторые изменения и перезагружали классы, поскольку это не позволяло программисту перезапустить сервер.

Это контролируется небольшим конфигом под названием cache_classes — я слышал об этом раньше, это причина, по которой вам нужно перезапустить сервер после запуска миграции в рабочей среде, чтобы любые изменения были доступны из объектов ActiveRecord. Я не стал складывать два и два в этом проявлении, так как не знал, что классы будут перезагружены. Я до сих пор не уверен, почему они считаются измененными и нуждаются в перезагрузке.

В конечном счете, я бы не увидел эту проблему, если бы не пытался запустить сервер единорога локально, и это можно предотвратить, установив config.cache_classes = true в development.rb

Документы здесь: https://guides.rubyonrails.org/configuring.html#rails-general-configuration

person timpwbaker    schedule 03.06.2020