request_store_rails 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +89 -193
- data/lib/request_locals.rb +2 -2
- data/lib/request_store_rails/middleware.rb +3 -2
- data/lib/request_store_rails/railtie.rb +3 -2
- data/lib/request_store_rails/version.rb +1 -2
- metadata +2 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67dd4d34bad2c45e0df9cc7ddc2f97a9733a65d2
|
4
|
+
data.tar.gz: f55c26ea388c9b440c63c2b594f501f3577c1c9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b85018a8b58bb67f032d234cc7f4390d0b60df2c177ab5bb28c9f063c65bca06a937019ad5e2855b27d577cf5dbc29eca45dad6bb0d11cc198826d6aa06f33b
|
7
|
+
data.tar.gz: a4a9b62d4546f8700867489abdf9afe6696f42305dfbeba3ead5a3b9d9c164e05aac0f56765db87f57599ba7e16267a78ff55c4c1cd40490c0f167636c6ec0f2
|
data/README.md
CHANGED
@@ -1,249 +1,145 @@
|
|
1
|
-
|
1
|
+
RequestLocals
|
2
2
|
=====================
|
3
|
-
[](https://github.com/ElMassimo/
|
9
|
-
<!-- [](https://coveralls.io/r/ElMassimo/queryable) -->
|
3
|
+
[](http://badge.fury.io/rb/request_store_rails)
|
4
|
+
[](https://travis-ci.org/ElMassimo/request_store_rails)
|
5
|
+
[](https://codeclimate.com/github/ElMassimo/request_store_rails)
|
6
|
+
[](https://codeclimate.com/github/ElMassimo/request_store_rails)
|
7
|
+
[](http://inch-ci.org/github/ElMassimo/request_store_rails)
|
8
|
+
[](https://github.com/ElMassimo/request_store_rails/blob/master/LICENSE.txt)
|
10
9
|
|
11
|
-
|
10
|
+
If you have ever needed to use a global variable in Rails, you know it sucks.
|
12
11
|
|
13
|
-
|
12
|
+
One of the usual tricks is to go for `Thread.current`, or if you have done your
|
13
|
+
homework, to use the awesome [`request_store`](https://github.com/steveklabnik/request_store).
|
14
14
|
|
15
|
-
Scopes serve to encapsulate reusable business rules, a method is defined with
|
16
|
-
the selected name and block (or proc)
|
17
15
|
```ruby
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
scope(:recent) { desc(:logged_in_at) }
|
22
|
-
|
23
|
-
scope :active, ->{ where(status: 'active') }
|
24
|
-
|
25
|
-
scope :favourite_brand do |product, brand|
|
26
|
-
where("favourites.#{product}": brand)
|
27
|
-
end
|
28
|
-
|
29
|
-
def current
|
30
|
-
recent.active
|
31
|
-
end
|
32
|
-
|
33
|
-
def miller_fans
|
34
|
-
favourite_brand(:beer, :Miller)
|
35
|
-
end
|
16
|
+
# Using Thread.current
|
17
|
+
def self.foo
|
18
|
+
Thread.current[:foo] ||= 0
|
36
19
|
end
|
37
20
|
|
38
|
-
|
39
|
-
|
40
|
-
```
|
41
|
-
|
42
|
-
### Delegation
|
43
|
-
|
44
|
-
By default most Array methods are delegated to the internal query. It's possible
|
45
|
-
to delegate extra methods to the query by calling `delegate`.
|
46
|
-
```ruby
|
47
|
-
class CustomersQuery
|
48
|
-
include Queryable
|
49
|
-
|
50
|
-
delegate :update_all, :destroy_all, :exists?
|
21
|
+
def self.foo=(value)
|
22
|
+
Thread.current[:foo] = value
|
51
23
|
end
|
52
|
-
```
|
53
24
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
working with the query object like if you were calling scopes.
|
58
|
-
|
59
|
-
You can achieve that using `delegate_and_chain`, which will delegate the method
|
60
|
-
call, assign the return value as the internal query, and return the query object.
|
61
|
-
|
62
|
-
```ruby
|
63
|
-
class CustomersQuery
|
64
|
-
include Queryable
|
65
|
-
|
66
|
-
delegate_and_chain :where, :order_by
|
25
|
+
# Using RequestStore
|
26
|
+
def self.foo
|
27
|
+
RequestStore.fetch(:foo) { 0 }
|
67
28
|
end
|
68
|
-
```
|
69
|
-
|
70
|
-
## Advantages
|
71
|
-
|
72
|
-
* Query objects are easy to understand.
|
73
|
-
* You can inherit, mixin, and chain queries in a very natural way.
|
74
|
-
* Increased testability, pretty close to being ORM/ODM agnostic.
|
75
29
|
|
76
|
-
|
77
|
-
|
78
|
-
If you are using Mongoid or ActiveRecord, you might want to try the
|
79
|
-
`Queryable::Mongoid` and `Queryable::ActiveRecord` modules that already take
|
80
|
-
care of delegating and chaining most of the methods in the underlying queries.
|
81
|
-
|
82
|
-
```ruby
|
83
|
-
class CustomersQuery
|
84
|
-
include Queryable::Mongoid
|
30
|
+
def self.foo=(value)
|
31
|
+
RequestStore.store[:foo] = value
|
85
32
|
end
|
86
|
-
|
87
|
-
CustomersQuery.new.where(:amount_purchased.gt => 2).active.asc(:logged_in_at)
|
88
33
|
```
|
89
34
|
|
90
|
-
|
91
|
-
of the other modules you can follow the approach in the [Notes](https://github.com/ElMassimo/queryable#notes) section.
|
35
|
+
### The problem
|
92
36
|
|
93
|
-
|
94
|
-
|
95
|
-
These modules would need to be manually required during app initialization or
|
96
|
-
wherever necessary (in Rails, config/initializers).
|
37
|
+
- Using `Thread.current`, values can stick around even after the request is over,
|
38
|
+
since some servers have a pool of Threads that they reuse, which [can cause bugs](https://github.com/steveklabnik/request_store#the-problem).
|
97
39
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
40
|
+
- Using `request_store`, the storage is _*not actually*_ request local. Variables
|
41
|
+
are stored in `Thread.current`, except that the storage is cleared after each
|
42
|
+
request. However, this does not work when you need to use multiple threads per
|
43
|
+
request, _different_ threads access _different_ stores.
|
102
44
|
|
103
|
-
|
104
|
-
require 'queryable/default_query'
|
45
|
+
### The solution
|
105
46
|
|
106
|
-
|
107
|
-
include Queryable
|
108
|
-
include Queryable::DefaultQuery
|
109
|
-
end
|
110
|
-
|
111
|
-
def OldCustomersQuery < CustomersQuery
|
112
|
-
queryable ArchivedCustomers
|
113
|
-
end
|
114
|
-
|
115
|
-
CustomersQuery.new.queryable == Customer.all
|
116
|
-
OldCustomersQuery.new.queryable == ArchivedCustomers.all
|
117
|
-
```
|
118
|
-
If you want to use common base objects for your queries, you may want want to
|
119
|
-
delay the automatic inference:
|
47
|
+
Add this line to your Gemfile:
|
120
48
|
|
121
49
|
```ruby
|
122
|
-
|
123
|
-
include Queryable
|
124
|
-
include Queryable::DefaultQuery
|
125
|
-
|
126
|
-
queryable false
|
127
|
-
end
|
128
|
-
|
129
|
-
class CustomersQuery < BaseQuery
|
130
|
-
end
|
131
|
-
|
132
|
-
CustomersQuery.new.queryable == Customer.all
|
50
|
+
gem 'request_store_rails'
|
133
51
|
```
|
134
52
|
|
135
|
-
|
136
|
-
Allows to define default scopes in query objects, and inherit them in query
|
137
|
-
object subclasses.
|
53
|
+
And change the code to this:
|
138
54
|
|
139
55
|
```ruby
|
140
|
-
|
141
|
-
|
142
|
-
def CustomersQuery
|
143
|
-
include Queryable
|
144
|
-
include Queryable::DefaultScope
|
145
|
-
include Queryable::DefaultQuery
|
146
|
-
|
147
|
-
default_scope :active
|
148
|
-
scope :active, -> { where(:last_purchase.gt => 7.days.ago) }
|
56
|
+
def self.foo
|
57
|
+
RequestLocals.fetch(:foo) { 0 }
|
149
58
|
end
|
150
59
|
|
151
|
-
def
|
152
|
-
|
153
|
-
scope :big_spender, -> { where(:total_expense.gt => 9999999) }
|
60
|
+
def self.foo=(value)
|
61
|
+
RequestLocals.store[:foo] = value
|
154
62
|
end
|
155
|
-
|
156
|
-
CustomersQuery.new.queryable == Customer.where(:last_purchase.gt => 7.days.ago)
|
157
|
-
|
158
|
-
BigCustomersQuery.new.queryable ==
|
159
|
-
Customer.where(:last_purchase.gt => 7.days.ago, :total_expense.gt => 9999999)
|
160
63
|
```
|
161
64
|
|
162
|
-
|
65
|
+
Oh yeah, everywhere you used `Thread.current` or `RequestStore.store` just
|
66
|
+
change it to `RequestLocals.store`. Now your variables will actually be stored
|
67
|
+
in a true _request-local_ way.
|
163
68
|
|
164
|
-
|
165
|
-
the block executes in the context of the internal query, so methods, constants,
|
166
|
-
and variables of the Queryable are not accessible.
|
69
|
+
### No Rails? No Problem!
|
167
70
|
|
168
|
-
|
169
|
-
|
170
|
-
|
71
|
+
A Railtie is added that configures the Middleware for you, but if you're not
|
72
|
+
using Rails, no biggie! Just use the Middleware yourself, however you need.
|
73
|
+
You'll probably have to shove this somewhere:
|
171
74
|
|
172
75
|
```ruby
|
173
|
-
|
174
|
-
|
175
|
-
include Queryable::Chainable
|
76
|
+
use RequestStoreRails::Middleware
|
77
|
+
```
|
176
78
|
|
177
|
-
|
79
|
+
## Multi-Threading
|
80
|
+
The middleware in the gem sets a thread-local variable `:request_id` in
|
81
|
+
`Thread.current` for the main thread that is executing the request.
|
178
82
|
|
179
|
-
|
180
|
-
|
181
|
-
|
83
|
+
If you need to spawn threads within a server that is already using thread-based
|
84
|
+
concurrency, all you need to do is to make sure that the `:request_id`
|
85
|
+
variable is set for your threads, and you will be able to access the
|
86
|
+
`RequestLocals` as usual.
|
182
87
|
|
183
|
-
|
184
|
-
queryable.desc(:logged_in_at)
|
185
|
-
end
|
88
|
+
A good way to apply this pattern is by encapsulating it into a helper class:
|
186
89
|
|
187
|
-
|
188
|
-
|
189
|
-
|
90
|
+
```ruby
|
91
|
+
# Public: Custom thread class that allows us to preserve the request context.
|
92
|
+
class ThreadWithContext
|
93
|
+
|
94
|
+
# Public: Returns a new Thread that preserves the context of the current request.
|
95
|
+
def ThreadWithContext.new(*args)
|
96
|
+
request_id = Thread.current[:request_id]
|
97
|
+
Thread.new {
|
98
|
+
Thread.current[:request_id] = request_id
|
99
|
+
yield *args
|
190
100
|
}
|
191
101
|
end
|
192
|
-
|
193
|
-
def search_in_active(field_values)
|
194
|
-
search(field_values).active
|
195
|
-
end
|
196
102
|
end
|
197
103
|
|
104
|
+
RequestLocals[:foo] = 1
|
198
105
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
### Notes
|
203
|
-
To avoid repetition, it's a good idea to create a `BaseQuery` object
|
204
|
-
to contain both the modules inclusion, and common scopes you may reuse.
|
205
|
-
|
206
|
-
```ruby
|
207
|
-
require 'queryable/chainable'
|
208
|
-
require 'queryable/default_scope'
|
209
|
-
require 'queryable/default_query'
|
210
|
-
|
211
|
-
def BaseQuery
|
212
|
-
include Queryable
|
213
|
-
include Queryable::Chainable
|
214
|
-
include Queryable::DefaultScope
|
215
|
-
include Queryable::DefaultQuery
|
216
|
-
|
217
|
-
# If you want to be concise:
|
218
|
-
include Queryable::DefaultQuery, Queryable::DefaultScope, Queryable::Chainable, Queryable
|
219
|
-
|
220
|
-
queryable false
|
221
|
-
|
222
|
-
scope :recent, ->{ where(:created_at.gt => 1.week.ago) }
|
223
|
-
end
|
224
|
-
|
225
|
-
def CustomersQuery < BaseQuery
|
226
|
-
...
|
227
|
-
end
|
106
|
+
ThreadWithContext.new {
|
107
|
+
puts RequestLocals[:foo] # => 1
|
108
|
+
}
|
228
109
|
```
|
110
|
+
The gem does not provide such construct to avoid name collisions, you are free
|
111
|
+
to reuse the snippet above and adjust it to match your use case.
|
229
112
|
|
230
|
-
|
113
|
+
If you are feeling adventurous, you could try using this [fire and forget script](https://gist.github.com/ElMassimo/e2f99848db6a415f1aaa) and make all of your threads request aware, or
|
114
|
+
should I say _prepend and forget_ :smile:? Probably not something to be used in
|
115
|
+
a production environment, but whatever floats your boat :boat:
|
231
116
|
|
232
|
-
|
233
|
-
|
234
|
-
|
117
|
+
### Atomicity
|
118
|
+
Have in mind that the `RequestLocals.fetch(:foo) { 'default' }` operation is
|
119
|
+
[atomic](https://github.com/ElMassimo/request_store_rails/blob/master/lib/request_locals.rb#L62),
|
120
|
+
while `RequestLocal[:foo] ||= 'default'` is not. In most scenarios, there is not
|
121
|
+
a lot of difference, but if you are in a concurrent environment make sure to
|
122
|
+
use the one that is more suitable for your use case :wink:
|
235
123
|
|
236
|
-
##
|
124
|
+
## Special Thanks
|
125
|
+
The inspiration for this gem, tests, and a big part of the readme were borrowed
|
126
|
+
from the really cool [`request_store`](https://github.com/steveklabnik/request_store) gem.
|
127
|
+
Thanks [Steve](https://github.com/steveklabnik) :smiley:
|
237
128
|
|
238
|
-
|
129
|
+
## Contributing
|
239
130
|
|
240
|
-
|
131
|
+
1. Fork it
|
132
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
133
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
134
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
135
|
+
5. Create new Pull Request
|
241
136
|
|
137
|
+
Don't forget to run the tests with `rake`.
|
242
138
|
|
243
139
|
License
|
244
140
|
--------
|
245
141
|
|
246
|
-
Copyright (c)
|
142
|
+
Copyright (c) 2015 Máximo Mussini
|
247
143
|
|
248
144
|
Permission is hereby granted, free of charge, to any person obtaining
|
249
145
|
a copy of this software and associated documentation files (the
|
data/lib/request_locals.rb
CHANGED
@@ -59,7 +59,7 @@ class RequestLocals
|
|
59
59
|
# Returns an existing value for the key is found, otherwise it returns the
|
60
60
|
# value yielded by the block.
|
61
61
|
def fetch(key, &block)
|
62
|
-
store.
|
62
|
+
store.compute_if_absent(key, &block)
|
63
63
|
end
|
64
64
|
|
65
65
|
protected
|
@@ -69,7 +69,7 @@ protected
|
|
69
69
|
#
|
70
70
|
# Returns a ThreadSafe::Cache.
|
71
71
|
def store
|
72
|
-
@cache.
|
72
|
+
@cache.compute_if_absent(current_request_id) { new_store }
|
73
73
|
end
|
74
74
|
|
75
75
|
# Internal: The current request is inferred from the current thread. It's very
|
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
|
3
|
-
# Public: Middleware that takes care of setting the thread-local variable
|
4
|
-
# :request_id, which enables RequestLocals to associate threads and requests.
|
5
3
|
module RequestStoreRails
|
4
|
+
|
5
|
+
# Public: Middleware that takes care of setting the thread-local variable
|
6
|
+
# :request_id, which enables RequestLocals to associate threads and requests.
|
6
7
|
class Middleware
|
7
8
|
|
8
9
|
def initialize(app)
|
@@ -1,6 +1,7 @@
|
|
1
|
-
# Internal: Inserts the middleware to manage request_ids and cleaning up after
|
2
|
-
# the request is complete.
|
3
1
|
module RequestStoreRails
|
2
|
+
|
3
|
+
# Internal: Inserts the middleware to manage request_ids and cleaning up after
|
4
|
+
# the request is complete.
|
4
5
|
class Railtie < ::Rails::Railtie
|
5
6
|
|
6
7
|
initializer 'request_store_rails.insert_middleware' do |app|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: request_store_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Máximo Mussini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-04-
|
11
|
+
date: 2015-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -58,20 +58,6 @@ dependencies:
|
|
58
58
|
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: 0.3.5
|
61
|
-
- !ruby/object:Gem::Dependency
|
62
|
-
name: rails
|
63
|
-
requirement: !ruby/object:Gem::Requirement
|
64
|
-
requirements:
|
65
|
-
- - ">="
|
66
|
-
- !ruby/object:Gem::Version
|
67
|
-
version: '3.2'
|
68
|
-
type: :runtime
|
69
|
-
prerelease: false
|
70
|
-
version_requirements: !ruby/object:Gem::Requirement
|
71
|
-
requirements:
|
72
|
-
- - ">="
|
73
|
-
- !ruby/object:Gem::Version
|
74
|
-
version: '3.2'
|
75
61
|
description: RequestLocals gives you per-request global storage in Rails
|
76
62
|
email:
|
77
63
|
- maximomussini@gmail.com
|