Rails: как обрабатывать данные Thread.current на однопоточном сервере, таком как Thin/Unicorn?

Поскольку Thin/Unicorn являются однопоточными, как вы обрабатываете хранилище Thread.current/per-request?

Просто провел простой тест - установил ключ в одном сеансе, прочитал его в другом - похоже, он все время пишет/читает из одного и того же места. Однако на WEBrick этого не происходит.

class TestController < ApplicationController
  def get
    render text: Thread.current[:xxx].inspect
  end

  def set
    Thread.current[:xxx] = 1

    render text: "SET to #{Thread.current[:xxx]}"
  end
end

Пробовал добавлять config.threadsafe! в application.rb, без изменений.

Как правильно хранить данные для каждого запроса?

Почему существуют гемы (включая сам Rails и наклон), которые используют Thread.current для хранения? Как они преодолевают эту проблему?

Может быть, Thread.current безопасен для запроса, но просто не очищается после запроса, и мне нужно сделать это самому?

  • Протестировано с Rails 3.2.9

Обновлять

Подводя итог приведенному ниже обсуждению с @skalee и @JesseWolgamott и моим выводам:

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

Есть некоторые известные жемчужины, которые используют Thread.current, такие как Rails, Tilt и draper. Думаю, если бы это было запрещено или небезопасно, они бы им не пользовались. Также кажется, что все они устанавливают значение перед использованием любого ключа в хеше (и даже возвращают исходное значение после завершения запроса).

Но в целом Thread.current — не лучшая практика для хранения по запросу. В большинстве случаев подойдет лучший дизайн, но в некоторых случаях может помочь использование env. Он доступен в контроллерах, а также в промежуточном программном обеспечении и может быть внедрен в любое место приложения.

Обновление 2. Похоже, что на данный момент draper неправильно использует Thread.current. См. https://github.com/drapergem/draper/issues/390.

Обновление 3: исправлена ​​ошибка с драйпером.


person elado    schedule 11.12.2012    source источник


Ответы (3)


Обычно вы хотите хранить вещи в сеансе. А если вам нужно что-то действительно недолговечное, см. вспышку Rails. Он очищается при каждом запросе. Любой метод, основанный на потоке, не будет работать последовательно на разных веб-серверах.

Другой вариант — изменить хэш env:

env['some_number'] = 5

Кстати, Unicorn не просто однопоточный, он разветвляется. Новый процесс запускается при каждом запросе (несмотря на то, что это звучит пугающе, в Linux он довольно эффективен). Поэтому, если вы установите что-либо в Unicorn, даже глобальную переменную, она не будет сохраняться для другого запроса.

person skalee    schedule 11.12.2012
comment
Сессия не обязательно находится в памяти (и не очищает каждый запрос), а флэш-память — это, по сути, сессия, которая очищается после следующего запроса. Нет ли способа хранить по одному запросу? Только что видел код warden, который использует Rack::Request для хранения вещей. Это безопаснее? Как я уже упоминал, существует множество драгоценных камней, включая rails, tilt и т. д., которые используют Thread.current — как так? - person elado; 12.12.2012
comment
Подход Warden тоже хорош, особенно если он заставляет вас чувствовать себя лучше :D На самом деле, в Rails вы можете получить доступ к этому хэшу, используя env (например, env['elados_number'] = 5. Warden не является геммом только для Rails, поэтому он не может полагаться на функции Rails. Кроме того, я не уверен, насколько хорош flash.now для хранения более сложных объектов.Я не знал, что Thread.current используется внутри Rails.Я не уверен, как это работает на разных веб-серверах, я бы придерживался env или flash.now. - person skalee; 12.12.2012
comment
И как мне получить доступ к этому env из кода, не являющегося контроллером? - person elado; 12.12.2012
comment
Почему вы хотите это сделать? Контроллеры предназначены для обработки запросов. Вы можете передать объект из контроллера в представление, просто назначив его переменной экземпляра контроллера. Вы скорее не хотите этого в моделях, потому что модели очень не связаны с запросами. Где вы хотите получить к нему доступ? Промежуточное ПО? - person skalee; 12.12.2012
comment
Допустим, у меня есть уважительная причина :) Я до сих пор не понимаю, сколько гемов и примеров кода используют Thread.current (например, для хранения current_user), хотя это совсем не безопасно. Они все неправильные или я неправильно понимаю? - person elado; 12.12.2012
comment
Может быть, Thread.current безопасен, но просто не очищается после запроса, и мне нужно сделать это самому? - person elado; 12.12.2012
comment
Да, ты был быстрее. Некоторые веб-серверы, например Unicorn, не пострадают от этого, потому что они разветвляются. Когда дело доходит до config.threadsafe!, это не имеет к этому никакого отношения. Обычно он предварительно загружает все, потому что автозагрузка Rails не является потокобезопасной. - person skalee; 12.12.2012
comment
Пожалуйста, приведите хотя бы один пример, где популярные гемы используют Thread.current... На самом деле это не сделано. - person Jesse Wolgamott; 12.12.2012
comment
Исходный код @JesseWolgamott Grepping tilt (косвенная зависимость от Rails): lib/tilt/template.rb. - person skalee; 12.12.2012
comment
@skalee --- Использование наклона - довольно черная магия и определенно не рекомендуется для случаев, когда вам не нужна экстремальная производительность. Трудно придумать вариант использования, где это хорошая идея. - person Jesse Wolgamott; 12.12.2012
comment
@JesseWolgamott Также Rails IdentityMap и его флаг. метод IdentityMap#use фактически устанавливает значение Thread.current, и когда это делается, он возвращает его к тому, что было. - person elado; 12.12.2012
comment
В итоге я создал промежуточное ПО, которое устанавливает переменную Thread.current и очищает ее после выполнения запроса. Интересно, считается ли это безопасным потоком или нет. Проверю и опубликую код. - person elado; 12.12.2012
comment
Мне пришло в голову еще одно ограничение: Thread.current не будет работать с событийным программированием, таким как em-synchrony. И, пожалуйста, подумайте еще раз, если вам действительно нужен доступ для запроса данных вне контроллера (а не просто передача в качестве параметра методам модели или чему-то еще), потому что это действительно пахнет плохим дизайном. - person skalee; 12.12.2012
comment
@skalee хорошо, чтобы уточнить, что я делаю - это API (на основе винограда), который показывает список пользователей и следит ли текущий пользователь за каждым (атрибут перспективы). Первое использование Thread.current — это сохранение/извлечение current_user. Затем вместо выполнения N запросов для current_user.following?(user) я собираю всех пользователей, которые есть в ответе, и запрашиваю один раз (в презентаторе драпировки), а затем назначаю состояния отслеживания за кулисами, прежде чем он перейдет к рендерингу. Можно сказать, что это что-то вроде IdentityMap с другим назначением. - person elado; 12.12.2012
comment
@elado - нет причин использовать Thread.current для этой цели. Вы действительно действительно можете иметь гораздо более простой дизайн, который является потокобезопасным. YMMV, но вас точно предупредили. - person Jesse Wolgamott; 12.12.2012
comment
@JesseWolgamott, независимо от того, что я делаю, вы говорите, что Thread.current опасен и никогда не должен использоваться, ни Rails, ни каким-либо другим, когда-либо? - person elado; 12.12.2012
comment
@elado Я говорю, что вы не должны использовать его, если у вас нет экстремального примера. В итоге вы получаете глобальную переменную, которую я бы также рекомендовал не использовать, и это общий запах кода. - person Jesse Wolgamott; 12.12.2012
comment
Теперь, когда я понимаю природу Thread.current, который относится к потоку, а не к запросу (например, HttpContext.Current в .NET), я буду искать другое, более чистое решение. Спасибо! - person elado; 13.12.2012
comment
@elado Решение от Warden, которое вы предложили, действительно хорошее. env — это хэш каждого запроса, Warden использует его для хранения произвольных объектов. env определен в контроллерах и промежуточном программном обеспечении Rails. Я считаю, что объект env определен и в Grape (по крайней мере, они написали это в файле readme). - person skalee; 13.12.2012
comment
@skalee env по-прежнему недоступен в контексте, отличном от контроллера. Кстати, гем Draper также использует Thread.current. Я согласен, что лучший дизайн поможет, но не понимаю, как его используют популярные драгоценные камни, если он якобы небезопасен. Возможно, единственная небезопасная проблема заключается в том, что значения не очищаются после каждого запроса, поэтому их лучше очищать перед каждым запросом. И тогда доступ к этому хэшу безопасен, потому что сервер следит за тем, чтобы два запроса не выполнялись одновременно с одним и тем же потоком. Поправьте меня если я ошибаюсь... - person elado; 13.12.2012
comment
@elado — доступно в промежуточном программном обеспечении, контроллере и Grape. потому что сервер следит за тем, чтобы два запроса не выполнялись одновременно с одним и тем же потоком. Поправьте меня, если я ошибаюсь — я не уверен, но думаю, что в Rails это правда. Это, конечно, не так в событийных приложениях, но в Ruby они довольно редки. Я не совсем уверен, что вы хотите сделать, попробуйте, если осмелитесь, я думаю, это сработает, но это очень непонятно и пахнет. - person skalee; 13.12.2012

Хотя люди по-прежнему предостерегают от использования Thread.current для хранения «глобальных» данных потока, возможно, правильным подходом к этому в Rails является очистка объекта Thread.current с помощью промежуточного программного обеспечения Rack. Стив Лабник написал гем request_store, чтобы сделать это легко. Исходный код драгоценного камня очень, очень мал, и я бы рекомендовал его прочитать.

Интересные части воспроизведены ниже.

module RequestStore
  def self.store
    Thread.current[:request_store] ||= {}
  end

  def self.clear!
    Thread.current[:request_store] = {}
  end
end


module RequestStore
  class Middleware
    def initialize(app)
      @app = app
    end

    def call(env)
      RequestStore.clear!
      @app.call(env)
    end
  end
end

Обратите внимание, что очищать все Thread.current не рекомендуется. По сути, request_store отслеживает ключи, которые ваше приложение хранит в Thread.current, и очищает их после завершения запроса.

person Saurabh Nanda    schedule 28.01.2013

Одним из предостережений при использовании Thread.current является то, что для серверов, которые повторно используют потоки или имеют пулы потоков, очень важно выполнять очистку после каждого запроса.

Это именно то, что предоставляет гем request_store, простой API, похожий на Thread.current, который заботится об очистке данных магазина. после каждого запроса.

RequestStore[:items] = []

Имейте в виду, однако, что гем использует Thread.current для сохранения Магазина, поэтому он не будет работать должным образом в многопоточной среде, где у вас есть более одного потока на запрос.

Чтобы обойти эту проблему, я реализовал хранилище, которое можно использовать между потоками для одного и того же запроса. Он называется request_store_rails и используется очень похоже:

RequestLocals[:items] = []
person Maximo Mussini    schedule 11.04.2015