У меня есть приложение 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 может исчезнуть внутри одного потока? И как я могу предотвратить это? Я бы предпочел не использовать эти локальные переменные потока, если я могу помочь.