├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── request_store.rb └── request_store │ ├── middleware.rb │ ├── railtie.rb │ └── version.rb ├── request_store.gemspec └── test ├── middleware_test.rb ├── request_store_test.rb └── test_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby: 15 | - '3.0' 16 | - 3.1 17 | - 3.2 18 | - 3.3 19 | - ruby-head 20 | - jruby-9.1 21 | - jruby-9.2 22 | - jruby-9.3 23 | - jruby-head 24 | - truffleruby-head 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true # 'bundle install' and cache 32 | - name: Test 33 | run: bundle exec rake 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in request_store.gemspec 4 | gemspec 5 | 6 | case Gem::Version.new(RUBY_VERSION.dup) 7 | when ->(ruby_version) { ruby_version >= Gem::Version.new('2.2.0') } 8 | gem 'rake', '~> 13' 9 | when ->(ruby_version) { ruby_version >= Gem::Version.new('2.0.0') } 10 | gem 'rake', '~> 12.3.3' 11 | else 12 | gem 'rake', '~> 11' 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Steve Klabnik 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RequestStore [![CI](https://github.com/steveklabnik/request_store/actions/workflows/ci.yml/badge.svg)](https://github.com/steveklabnik/request_store/actions/workflows/ci.yml) [![Code Climate](https://codeclimate.com/github/steveklabnik/request_store.svg)](https://codeclimate.com/github/steveklabnik/request_store) 2 | 3 | Ever needed to use a global variable in Rails? Ugh, that's the worst. If you 4 | need global state, you've probably reached for `Thread.current`. Like this: 5 | 6 | ```ruby 7 | def self.foo 8 | Thread.current[:foo] ||= 0 9 | end 10 | 11 | def self.foo=(value) 12 | Thread.current[:foo] = value 13 | end 14 | ``` 15 | 16 | Ugh! I hate it. But you gotta do what you gotta do... 17 | 18 | ### The problem 19 | 20 | Everyone's worrying about concurrency these days. So people are using those 21 | fancy threaded web servers, like Thin or Puma. But if you use `Thread.current`, 22 | and you use one of those servers, watch out! Values can stick around longer 23 | than you'd expect, and this can cause bugs. For example, if we had this in 24 | our controller: 25 | 26 | ```ruby 27 | def index 28 | Thread.current[:counter] ||= 0 29 | Thread.current[:counter] += 1 30 | 31 | render :text => Thread.current[:counter] 32 | end 33 | ``` 34 | 35 | If we ran this on MRI with Webrick, you'd get `1` as output, every time. But if 36 | you run it with Thin, you get `1`, then `2`, then `3`... 37 | 38 | ### The solution 39 | 40 | Add this line to your application's Gemfile: 41 | 42 | ```ruby 43 | gem 'request_store' 44 | ``` 45 | 46 | And change the code to this: 47 | 48 | ```ruby 49 | def index 50 | RequestStore.store[:foo] ||= 0 51 | RequestStore.store[:foo] += 1 52 | 53 | render :text => RequestStore.store[:foo] 54 | end 55 | ``` 56 | 57 | Yep, everywhere you used `Thread.current` just change it to 58 | `RequestStore.store`. Now no matter what server you use, you'll get `1` every 59 | time: the storage is local to that request. 60 | 61 | ### API 62 | 63 | The `fetch` method returns the stored value if it already exists. If no stored value exists, it uses the provided block to add a new stored value. 64 | 65 | ```ruby 66 | top_posts = RequestStore.fetch(:top_posts) do 67 | # code to obtain the top posts 68 | end 69 | ``` 70 | 71 | ### Rails 2 compatibility 72 | 73 | The gem includes a Railtie that will configure everything properly for Rails 3+ 74 | apps, but if your app is tied to an older (2.x) version, you will have to 75 | manually add the middleware yourself. Typically this should just be a matter 76 | of adding: 77 | 78 | ```ruby 79 | config.middleware.use RequestStore::Middleware 80 | ``` 81 | 82 | into your config/environment.rb. 83 | 84 | ### No Rails? No Problem! 85 | 86 | A Railtie is added that configures the Middleware for you, but if you're not 87 | using Rails, no biggie! Just use the Middleware yourself, however you need. 88 | You'll probably have to shove this somewhere: 89 | 90 | ```ruby 91 | use RequestStore::Middleware 92 | ``` 93 | 94 | #### No Rails + Rack::Test 95 | 96 | In order to have `RequestStore` storage cleared between requests, add it to the 97 | `app`: 98 | 99 | ```ruby 100 | # spec_helper.rb 101 | 102 | def app 103 | Rack::Builder.new do 104 | use RequestStore::Middleware 105 | run MyApp 106 | end 107 | end 108 | ``` 109 | 110 | ## Using with Sidekiq 111 | 112 | This gem uses a Rack middleware to clear the store object after every request, 113 | but that doesn't translate well to background processing with 114 | [Sidekiq](https://github.com/mperham/sidekiq). 115 | 116 | A companion library, 117 | [request_store-sidekiq](https://rubygems.org/gems/request_store-sidekiq) 118 | creates a Sidekiq middleware that will ensure the store is cleared after each 119 | job is processed, for security and consistency with how this is done in Rack. 120 | 121 | ## Semantic Versioning 122 | 123 | This project conforms to [semver](http://semver.org/). As a result of this 124 | policy, you can (and should) specify a dependency on this gem using the 125 | [Pessimistic Version Constraint](http://guides.rubygems.org/patterns/) with 126 | two digits of precision. For example: 127 | 128 | ```ruby 129 | spec.add_dependency 'request_store', '~> 1.0' 130 | ``` 131 | 132 | This means your project is compatible with request_store 1.0 up until 2.0. 133 | You can also set a higher minimum version: 134 | 135 | ```ruby 136 | spec.add_dependency 'request_store', '~> 1.1' 137 | ``` 138 | 139 | ## Contributing 140 | 141 | 1. Fork it 142 | 2. Create your feature branch (`git checkout -b my-new-feature`) 143 | 3. Commit your changes (`git commit -am 'Add some feature'`) 144 | 4. Push to the branch (`git push origin my-new-feature`) 145 | 5. Create new Pull Request 146 | 147 | Don't forget to run the tests with `rake`. 148 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rake/testtask' 4 | 5 | Rake::TestTask.new do |t| 6 | t.libs << "lib" 7 | t.test_files = FileList['test/*_test.rb'] 8 | t.ruby_opts = ['-r./test/test_helper.rb'] 9 | t.verbose = true 10 | end 11 | 12 | task :default => :test 13 | -------------------------------------------------------------------------------- /lib/request_store.rb: -------------------------------------------------------------------------------- 1 | require "request_store/version" 2 | require "request_store/middleware" 3 | require "request_store/railtie" if defined?(Rails::Railtie) 4 | 5 | module RequestStore 6 | def self.store 7 | Thread.current[:request_store] ||= {} 8 | end 9 | 10 | def self.store=(store) 11 | Thread.current[:request_store] = store 12 | end 13 | 14 | def self.clear! 15 | Thread.current[:request_store] = {} 16 | end 17 | 18 | def self.begin! 19 | Thread.current[:request_store_active] = true 20 | end 21 | 22 | def self.end! 23 | Thread.current[:request_store_active] = false 24 | end 25 | 26 | def self.active? 27 | Thread.current[:request_store_active] || false 28 | end 29 | 30 | def self.read(key) 31 | store[key] 32 | end 33 | 34 | def self.[](key) 35 | store[key] 36 | end 37 | 38 | def self.write(key, value) 39 | store[key] = value 40 | end 41 | 42 | def self.[]=(key, value) 43 | store[key] = value 44 | end 45 | 46 | def self.exist?(key) 47 | store.key?(key) 48 | end 49 | 50 | def self.fetch(key) 51 | store[key] = yield unless exist?(key) 52 | store[key] 53 | end 54 | 55 | def self.delete(key, &block) 56 | store.delete(key, &block) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/request_store/middleware.rb: -------------------------------------------------------------------------------- 1 | require 'rack/body_proxy' 2 | 3 | # A middleware that ensures the RequestStore stays around until 4 | # the last part of the body is rendered. This is useful when 5 | # using streaming. 6 | # 7 | # Uses Rack::BodyProxy, adapted from Rack::Lock's usage of the 8 | # same pattern. 9 | 10 | module RequestStore 11 | class Middleware 12 | def initialize(app) 13 | @app = app 14 | end 15 | 16 | def call(env) 17 | RequestStore.begin! 18 | 19 | status, headers, body = @app.call(env) 20 | 21 | body = Rack::BodyProxy.new(body) do 22 | RequestStore.end! 23 | RequestStore.clear! 24 | end 25 | 26 | returned = true 27 | 28 | [status, headers, body] 29 | ensure 30 | unless returned 31 | RequestStore.end! 32 | RequestStore.clear! 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/request_store/railtie.rb: -------------------------------------------------------------------------------- 1 | module RequestStore 2 | class Railtie < ::Rails::Railtie 3 | initializer "request_store.insert_middleware" do |app| 4 | if ActionDispatch.const_defined? :RequestId 5 | app.config.middleware.insert_after ActionDispatch::RequestId, RequestStore::Middleware 6 | else 7 | app.config.middleware.insert_after Rack::MethodOverride, RequestStore::Middleware 8 | end 9 | 10 | if ActiveSupport.const_defined?(:Reloader) && ActiveSupport::Reloader.respond_to?(:to_complete) 11 | ActiveSupport::Reloader.to_complete do 12 | RequestStore.clear! 13 | end 14 | elsif ActionDispatch.const_defined?(:Reloader) && ActionDispatch::Reloader.respond_to?(:to_cleanup) 15 | ActionDispatch::Reloader.to_cleanup do 16 | RequestStore.clear! 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/request_store/version.rb: -------------------------------------------------------------------------------- 1 | module RequestStore 2 | VERSION = "1.7.0" 3 | end 4 | -------------------------------------------------------------------------------- /request_store.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'request_store/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "request_store" 8 | gem.version = RequestStore::VERSION 9 | gem.authors = ["Steve Klabnik"] 10 | gem.email = ["steve@steveklabnik.com"] 11 | gem.description = %q{RequestStore gives you per-request global storage.} 12 | gem.summary = %q{RequestStore gives you per-request global storage.} 13 | gem.homepage = "https://github.com/steveklabnik/request_store" 14 | gem.licenses = ["MIT"] 15 | 16 | gem.files = `git ls-files`.split($/) 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_dependency "rack", ">= 1.4" 21 | 22 | gem.add_development_dependency "rake" 23 | gem.add_development_dependency "minitest", "~> 5.0" 24 | end 25 | -------------------------------------------------------------------------------- /test/middleware_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/test' 2 | require 'minitest/autorun' 3 | 4 | require 'request_store' 5 | 6 | class MiddlewareTest < Minitest::Test 7 | def setup 8 | @app = RackApp.new 9 | @middleware = RequestStore::Middleware.new(@app) 10 | end 11 | 12 | def call_middleware(opts = {}) 13 | _, _, proxy = @middleware.call(opts) 14 | proxy.close 15 | proxy 16 | end 17 | 18 | def test_middleware_resets_store 19 | 2.times do 20 | call_middleware 21 | end 22 | 23 | assert_equal 1, @app.last_value 24 | assert_equal({}, RequestStore.store) 25 | end 26 | 27 | def test_middleware_does_not_mutate_response_and_does_not_overflow_stack 28 | 10000.times do 29 | call_middleware 30 | end 31 | 32 | resp = call_middleware 33 | assert resp.is_a?(::Rack::BodyProxy) 34 | assert_equal ["response"], resp.to_a 35 | assert_equal ["response"], resp.instance_variable_get(:@body) 36 | end 37 | 38 | def test_middleware_resets_store_on_error 39 | e = assert_raises RuntimeError do 40 | call_middleware({:error => true}) 41 | end 42 | 43 | assert_equal 'FAIL', e.message 44 | assert_equal({}, RequestStore.store) 45 | end 46 | 47 | def test_middleware_begins_store 48 | call_middleware 49 | assert_equal true, @app.store_active 50 | end 51 | 52 | def test_middleware_ends_store 53 | call_middleware 54 | 55 | assert_equal false, RequestStore.active? 56 | end 57 | 58 | def test_middleware_ends_store_on_error 59 | assert_raises RuntimeError do 60 | call_middleware({:error => true}) 61 | end 62 | 63 | assert_equal false, RequestStore.active? 64 | end 65 | 66 | def test_middleware_stores_until_proxy_closes 67 | _, _, proxy = @middleware.call({}) 68 | 69 | assert_equal 1, @app.last_value 70 | assert RequestStore.active? 71 | 72 | proxy.close 73 | 74 | refute RequestStore.active? 75 | refute RequestStore.store[:foo] 76 | end 77 | end 78 | 79 | class MiddlewareWithConstResponseTest < Minitest::Test 80 | def setup 81 | @app = RackAppWithConstResponse.new 82 | @middleware = RequestStore::Middleware.new(@app) 83 | end 84 | 85 | def call_middleware(opts = {}) 86 | _, _, proxy = @middleware.call(opts) 87 | proxy.close 88 | proxy 89 | end 90 | 91 | def test_middleware_does_not_mutate_response_and_does_not_overflow_stack 92 | 10000.times do 93 | call_middleware 94 | end 95 | 96 | resp = call_middleware 97 | assert resp.is_a?(::Rack::BodyProxy) 98 | assert_equal ["response"], resp.to_a 99 | assert_equal ["response"], resp.instance_variable_get(:@body) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/request_store_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | 3 | require 'request_store' 4 | 5 | class RequestStoreTest < Minitest::Test 6 | def setup 7 | RequestStore.clear! 8 | end 9 | 10 | def teardown 11 | RequestStore.clear! 12 | end 13 | 14 | def test_initial_state 15 | Thread.current[:request_store] = nil 16 | assert_equal RequestStore.store, Hash.new 17 | end 18 | 19 | def test_init_with_hash 20 | assert_equal Hash.new, RequestStore.store 21 | end 22 | 23 | def test_assign_store 24 | store_obj = { test_key: 'test' } 25 | RequestStore.store = store_obj 26 | assert_equal 'test', RequestStore.store[:test_key] 27 | assert_equal store_obj, RequestStore.store 28 | end 29 | 30 | def test_clear 31 | RequestStore.store[:foo] = 1 32 | RequestStore.clear! 33 | assert_equal Hash.new, RequestStore.store 34 | end 35 | 36 | def test_quacks_like_hash 37 | RequestStore.store[:foo] = 1 38 | assert_equal 1, RequestStore.store[:foo] 39 | assert_equal 1, RequestStore.store.fetch(:foo) 40 | end 41 | 42 | def test_read 43 | RequestStore.store[:foo] = 1 44 | assert_equal 1, RequestStore.read(:foo) 45 | assert_equal 1, RequestStore[:foo] 46 | end 47 | 48 | def test_write 49 | RequestStore.write(:foo, 1) 50 | assert_equal 1, RequestStore.store[:foo] 51 | RequestStore[:foo] = 2 52 | assert_equal 2, RequestStore.store[:foo] 53 | end 54 | 55 | def test_fetch 56 | assert_equal 2, RequestStore.fetch(:foo) { 1 + 1 } 57 | assert_equal 2, RequestStore.fetch(:foo) { 2 + 2 } 58 | end 59 | 60 | def test_delete 61 | assert_equal 2, RequestStore.fetch(:foo) { 1 + 1 } 62 | assert_equal 2, RequestStore.delete(:foo) { 2 + 2 } 63 | assert_equal 4, RequestStore.delete(:foo) { 2 + 2 } 64 | end 65 | 66 | def test_delegates_to_thread 67 | RequestStore.store[:foo] = 1 68 | assert_equal 1, Thread.current[:request_store][:foo] 69 | end 70 | 71 | def test_active_state 72 | assert_equal false, RequestStore.active? 73 | 74 | RequestStore.begin! 75 | assert_equal true, RequestStore.active? 76 | 77 | RequestStore.end! 78 | assert_equal false, RequestStore.active? 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | class RackApp 2 | attr_reader :last_value, :store_active 3 | 4 | def call(env) 5 | RequestStore.store[:foo] ||= 0 6 | RequestStore.store[:foo] += 1 7 | @last_value = RequestStore.store[:foo] 8 | @store_active = RequestStore.active? 9 | raise 'FAIL' if env[:error] 10 | 11 | [200, {}, ["response"]] 12 | end 13 | end 14 | 15 | class RackAppWithConstResponse 16 | RESPONSE = [200, {}, ["response"]] 17 | 18 | attr_reader :last_value, :store_active 19 | 20 | def call(env) 21 | RequestStore.store[:foo] ||= 0 22 | RequestStore.store[:foo] += 1 23 | @last_value = RequestStore.store[:foo] 24 | @store_active = RequestStore.active? 25 | raise 'FAIL' if env[:error] 26 | 27 | RESPONSE 28 | end 29 | end 30 | --------------------------------------------------------------------------------