Пользовательские сеансы недействительны после изменения пароля, но только с несколькими потоками

У меня возникла странная проблема с функцией в моем приложении Rails 4 + Devise 3.2, которая позволяет пользователям изменять свой пароль через AJAX POST на следующее действие, полученное из вики Devise Разрешить пользователям изменять свой пароль. Похоже, что после того, как пользователь изменит свой пароль и после одного или нескольких запросов позже, он будет принудительно выйти из системы и продолжит принудительный выход из системы после повторного входа.

# POST /update_my_password
def update_my_password
  @user = User.find(current_user.id)
  authorize! :update, @user ## CanCan check here as well

  if @user.valid_password?(params[:old_password])
    @user.password = params[:new_password]
    @user.password_confirmation = params[:new_password_conf]
    if @user.save
      sign_in @user, :bypass => true
      head :no_content
      return
    end
  else
    render :json => { "error_code" => "Incorrect password" }, :status => 401     
    return
  end

  render :json => { :errors => @user.errors }, :status => 422
end

Это действие на самом деле отлично работает при разработке, но не работает в производственной среде, когда я запускаю многопоточные, многопользовательские экземпляры Puma. Похоже, что происходит то, что пользователь останется в системе до тех пор, пока один из их запросов не попадет в другой поток, а затем они выйдут из системы как Unauthorized со статусом ответа 401. Проблема не возникает, если я запускаю Puma с одним потоком и одним рабочим. Единственный способ, которым я могу позволить пользователю снова оставаться в системе с несколькими потоками, - это перезапустить сервер (что не является решением). Это довольно странно, потому что я думал, что конфигурация хранилища сеансов, которая у меня есть, справится с этим правильно. Мой config/initializers/session_store.rb файл содержит следующее:

MyApp::Application.config.session_store(ActionDispatch::Session::CacheStore, :expire_after => 3.days)

Моя production.rb конфигурация содержит:

config.cache_store = :dalli_store, ENV["MEMCACHE_SERVERS"],
{ 
  :pool_size => (ENV['MEMCACHE_POOL_SIZE'] || 1),
  :compress => true,
  :socket_timeout => 0.75, 
  :socket_max_failures => 3, 
  :socket_failure_delay => 0.1,
  :down_retry_delay => 2.seconds,
  :keepalive => true,
  :failover => true
}

Я загружаю puma через bundle exec puma -p $PORT -C ./config/puma.rb. Мой puma.rb содержит:

threads ENV['PUMA_MIN_THREADS'] || 8, ENV['PUMA_MAX_THREADS'] || 16
workers ENV['PUMA_WORKERS'] || 2
preload_app!

on_worker_boot do
  ActiveSupport.on_load(:active_record) do
    config = Rails.application.config.database_configuration[Rails.env]
    config['reaping_frequency'] = ENV['DB_REAP_FREQ'] || 10 # seconds
    config['pool']              = ENV['DB_POOL'] || 16
    ActiveRecord::Base.establish_connection(config)
  end
end

Итак ... что здесь может быть не так? Как я могу обновить сеанс для всех потоков / рабочих при изменении пароля без перезапуска сервера?


person Mike Atlas    schedule 03.02.2014    source источник
comment
Как вы загружаете сервер Puma?   -  person Scott Feinberg    schedule 06.02.2014
comment
Добавлена ​​информация о загрузке puma @ScottFeinberg.   -  person Mike Atlas    schedule 07.02.2014
comment
Что произойдет, если вы выбросите 401, когда пользователь никогда не входил в систему (то есть пытается получить доступ к ресурсу logged_in, пока не вошел в систему). Я замечаю аналогичную проблему, при этом нормально работающую в однопоточной среде.   -  person Scott Feinberg    schedule 07.02.2014
comment
какой поставщик инфраструктуры вы используете? ОПЕРАЦИОННЫЕ СИСТЕМЫ?   -  person Scott Feinberg    schedule 07.02.2014
comment
Heroku, и я полагаю, что это общий Linux ... Я сомневаюсь, что это актуально.   -  person Mike Atlas    schedule 07.02.2014
comment
я думаю, ваша проблема здесь: .com / plataformatec / devise / blob / нужен ли вообще вызов sign_in?   -  person phoet    schedule 10.02.2014
comment
Да, @phoet, удаление вызова sign_in не решает проблемы.   -  person Mike Atlas    schedule 11.02.2014


Ответы (4)


Поскольку вы используете Dalli в качестве хранилища сеансов, вы можете столкнуться с этой проблемой.

Многопоточность Dalli

Со страницы:

«Если вы используете Puma или другой сервер потоковых приложений, начиная с Dalli 2.7, вы можете использовать пул клиентов Dalli с Rails, чтобы гарантировать, что синглтон Rails.cache не станет источником конфликта потоков».

person engineerDave    schedule 11.02.2014
comment
Я на самом деле тоже читал об этом и безуспешно тестировал с использованием настроек пула dalli. В качестве альтернативы в настоящее время я использую redis в качестве хранилища сеансов (чтобы изолировать dalli и memcached) и по-прежнему наблюдаю ту же проблему, которая, на мой взгляд, указывает на то, что проблема все еще в puma / threading / devise / warden. - person Mike Atlas; 12.02.2014
comment
Кроме того, похоже, что это не конфликт потоков - это будет означать, что rails будут пытаться подключиться к dalli из многих потоков и не смогут подключиться, что в любом случае не является моей проблемой. Это будет проявляться в том, что обычные пользователи выходят из системы из-за того, что не могут прочитать свой сеанс из конкурирующего соединения с memcached. - person Mike Atlas; 12.02.2014

Я подозреваю, что вы наблюдаете такое поведение из-за следующих проблем:

  • devise определяет вспомогательный метод current_user, используя переменную экземпляра, получающую значение от warden. в lib/devise/controllers/helpers.rb №58. Заменить пользователя для отображения

    def current_#{mapping}
      @current_#{mapping} ||= warden.authenticate(:scope => :#{mapping})
    end
    

Я сам не сталкивался с этим, это предположение, но, надеюсь, это в какой-то мере поможет. В многопоточном приложении каждый запрос направляется в поток, который может сохранять предыдущее значение current_user из-за кэширования либо в локальном хранилище потока, либо в стойке, которая может отслеживать данные для каждого потока.

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

person edk750    schedule 09.02.2014
comment
С тех пор, как я разместил свой вопрос, я сам посмотрел на этот источник (для разработки и надзирателя) и подозреваю так же, как и вы, в отношении причины проблемы. Я все еще пытаюсь найти лучший способ сообщить другим потокам, что им нужно корректно обновить. - person Mike Atlas; 11.02.2014
comment
пока вы не используете переменную класса для хранения данных, все будет в порядке. rails гарантирует, что вы получите новый экземпляр этого класса. - person phoet; 12.02.2014

Я бы посоветовал после того, как пользователь изменит свой пароль, выйдите из системы и очистите их сеансы, например:

  def update_password
    @user = User.find(current_user.id)
    if @user.update(user_params)
      sign_out @user # Let them sign-in again
      reset_session # This might not be needed?
      redirect_to root_path
    else
      render "edit"
    end
  end

Я считаю, что ваша основная проблема заключается в том, как sign_in обновляет сеанс в сочетании с многопоточностью, как вы упомянули.

person omarvelous    schedule 09.02.2014
comment
на самом деле, я не вижу причин, по которым вы бы позвонили sign_in в этом случае. пользователь все равно авторизован ?! - person phoet; 10.02.2014
comment
К сожалению, это, похоже, не решает проблему (я тестировал с помощью sign_out и reset_session). Спасибо за предложение. - person Mike Atlas; 11.02.2014

Это грубое, грубое решение, но оказалось, что другие потоки будут выполнять запрос ActiveRecord. кэширование моей User модели, и возвращенные устаревшие данные вызовут ошибку аутентификации.

Адаптировав метод, описанный в Обход кеша ActiveRecord, я добавил следующее к моему User.rb файлу:

# this default scope avoids query caching of the user,
# which can be a big problem when multithreaded user password changing
# happens. 
FIXNUM_MAX = (2**(0.size * 8 -2) -1)
default_scope { 
  r = Random.new.rand(FIXNUM_MAX)
  where("? = ?", r,r)
}

Я понимаю, что это влияет на производительность, распространяющуюся на все мое приложение, но, похоже, это единственный способ обойти проблему. Я попытался переопределить многие методы devise и warden, которые используют этот запрос, но безуспешно. Возможно, я скоро займусь рассмотрением сообщения об ошибке против разработки / надзирателя.

person Mike Atlas    schedule 27.02.2014