rollout 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +95 -0
- data/.gitignore +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +20 -0
- data/Gemfile +5 -0
- data/LICENSE +20 -0
- data/README.md +201 -0
- data/Rakefile +7 -0
- data/lib/rollout.rb +227 -0
- data/lib/rollout/feature.rb +131 -0
- data/lib/rollout/logging.rb +198 -0
- data/lib/rollout/version.rb +5 -0
- data/rollout.gemspec +31 -0
- data/spec/rollout/logging_spec.rb +143 -0
- data/spec/rollout_spec.rb +730 -0
- data/spec/spec_helper.rb +27 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 37e56080ba30fb08aaa7e718e22fae3cab0745e84a91537e4e5520c2843a63a2
|
4
|
+
data.tar.gz: 2b2f8c5a6e7a3ddd8275d818b4a168e7573204c2e259e323ca7a2ea9996062d9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a31905463a16ca7413566460c9a693ab5b9063e74547c05aafe2cf40d5e8f8d83ecb9e85316dd48c3e6b5e5b63311ac222343b485228a695ff5b72f8b0d82a2c
|
7
|
+
data.tar.gz: 33fc542bdfe0def2b13a1b3697ba59c1e5f83e813a7c67740d94bf0a9725e17003fdc3fecfa923874883730dde589e0906b5d49ab19ff49c36943e259c28f134
|
@@ -0,0 +1,95 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
workflows:
|
4
|
+
main:
|
5
|
+
jobs:
|
6
|
+
- ruby27
|
7
|
+
- ruby26
|
8
|
+
- ruby25
|
9
|
+
- ruby24
|
10
|
+
- ruby23
|
11
|
+
|
12
|
+
executors:
|
13
|
+
ruby27:
|
14
|
+
docker:
|
15
|
+
- image: circleci/ruby:2.7
|
16
|
+
- image: circleci/redis:alpine
|
17
|
+
ruby26:
|
18
|
+
docker:
|
19
|
+
- image: circleci/ruby:2.6
|
20
|
+
- image: circleci/redis:alpine
|
21
|
+
ruby25:
|
22
|
+
docker:
|
23
|
+
- image: circleci/ruby:2.5
|
24
|
+
- image: circleci/redis:alpine
|
25
|
+
ruby24:
|
26
|
+
docker:
|
27
|
+
- image: circleci/ruby:2.4
|
28
|
+
- image: circleci/redis:alpine
|
29
|
+
ruby23:
|
30
|
+
docker:
|
31
|
+
- image: circleci/ruby:2.3
|
32
|
+
- image: circleci/redis:alpine
|
33
|
+
|
34
|
+
commands:
|
35
|
+
test:
|
36
|
+
steps:
|
37
|
+
- restore_cache:
|
38
|
+
keys:
|
39
|
+
- bundler-{{ checksum "Gemfile.lock" }}
|
40
|
+
|
41
|
+
- run:
|
42
|
+
name: Bundle Install
|
43
|
+
command: bundle check --path vendor/bundle || bundle install
|
44
|
+
|
45
|
+
- save_cache:
|
46
|
+
key: bundler-{{ checksum "Gemfile.lock" }}
|
47
|
+
paths:
|
48
|
+
- vendor/bundle
|
49
|
+
|
50
|
+
- run:
|
51
|
+
name: Run rspec
|
52
|
+
command: |
|
53
|
+
bundle exec rspec --format documentation --format RspecJunitFormatter --out test_results/rspec.xml
|
54
|
+
|
55
|
+
jobs:
|
56
|
+
ruby27:
|
57
|
+
executor: ruby27
|
58
|
+
steps:
|
59
|
+
- checkout
|
60
|
+
- test
|
61
|
+
|
62
|
+
- run:
|
63
|
+
name: Report Test Coverage
|
64
|
+
command: |
|
65
|
+
wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 -O cc-test-reporter
|
66
|
+
chmod +x cc-test-reporter
|
67
|
+
./cc-test-reporter format-coverage -t simplecov -o coverage/codeclimate.json coverage/.resultset.json
|
68
|
+
./cc-test-reporter upload-coverage -i coverage/codeclimate.json
|
69
|
+
|
70
|
+
- store_test_results:
|
71
|
+
path: test_results
|
72
|
+
|
73
|
+
ruby26:
|
74
|
+
executor: ruby26
|
75
|
+
steps:
|
76
|
+
- checkout
|
77
|
+
- test
|
78
|
+
|
79
|
+
ruby25:
|
80
|
+
executor: ruby25
|
81
|
+
steps:
|
82
|
+
- checkout
|
83
|
+
- test
|
84
|
+
|
85
|
+
ruby24:
|
86
|
+
executor: ruby24
|
87
|
+
steps:
|
88
|
+
- checkout
|
89
|
+
- test
|
90
|
+
|
91
|
+
ruby23:
|
92
|
+
executor: ruby23
|
93
|
+
steps:
|
94
|
+
- checkout
|
95
|
+
- test
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
pkg
|
20
|
+
*.gem
|
21
|
+
gemfiles/*.lock
|
22
|
+
.rspec_status
|
23
|
+
|
24
|
+
## PROJECT::SPECIFIC
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
language: ruby
|
2
|
+
cache: bundler
|
3
|
+
sudo: false
|
4
|
+
services:
|
5
|
+
- redis-server
|
6
|
+
rvm:
|
7
|
+
- 2.6
|
8
|
+
- 2.5
|
9
|
+
- 2.4
|
10
|
+
- 2.3
|
11
|
+
- 2.2
|
12
|
+
- 2.1
|
13
|
+
- 2.0
|
14
|
+
env:
|
15
|
+
- USE_REAL_REDIS=true
|
16
|
+
gemfile:
|
17
|
+
- gemfiles/redis_3.gemfile
|
18
|
+
- gemfiles/redis_4.gemfile
|
19
|
+
script:
|
20
|
+
- bundle exec rspec
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010-InfinityAndBeyond BitLove, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# rollout
|
2
|
+
|
3
|
+
Fast feature flags based on Redis.
|
4
|
+
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/rollout.svg)](https://badge.fury.io/rb/rollout)
|
6
|
+
[![CircleCI](https://circleci.com/gh/fetlife/rollout.svg?style=svg)](https://circleci.com/gh/fetlife/rollout)
|
7
|
+
[![Code Climate](https://codeclimate.com/github/FetLife/rollout/badges/gpa.svg)](https://codeclimate.com/github/FetLife/rollout)
|
8
|
+
[![Test Coverage](https://codeclimate.com/github/FetLife/rollout/badges/coverage.svg)](https://codeclimate.com/github/FetLife/rollout/coverage)
|
9
|
+
|
10
|
+
## Install it
|
11
|
+
|
12
|
+
```bash
|
13
|
+
gem install rollout
|
14
|
+
```
|
15
|
+
|
16
|
+
## How it works
|
17
|
+
|
18
|
+
Initialize a rollout object. I assign it to a global var.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
require 'redis'
|
22
|
+
|
23
|
+
$redis = Redis.new
|
24
|
+
$rollout = Rollout.new($redis)
|
25
|
+
```
|
26
|
+
|
27
|
+
or even simpler
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'redis'
|
31
|
+
$rollout = Rollout.new(Redis.current) # Will use REDIS_URL env var or default redis url
|
32
|
+
```
|
33
|
+
|
34
|
+
|
35
|
+
Update data specific to a feature:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
$rollout.set_feature_data(:chat, description: 'foo', release_date: 'bar', whatever: 'baz')
|
39
|
+
```
|
40
|
+
|
41
|
+
Check whether a feature is active for a particular user:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
$rollout.active?(:chat, User.first) # => true/false
|
45
|
+
```
|
46
|
+
|
47
|
+
Check whether a feature is active globally:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
$rollout.active?(:chat)
|
51
|
+
```
|
52
|
+
|
53
|
+
You can activate features using a number of different mechanisms.
|
54
|
+
|
55
|
+
## Groups
|
56
|
+
|
57
|
+
Rollout ships with one group by default: "all", which does exactly what it
|
58
|
+
sounds like.
|
59
|
+
|
60
|
+
You can activate the all group for the chat feature like this:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
$rollout.activate_group(:chat, :all)
|
64
|
+
```
|
65
|
+
|
66
|
+
You might also want to define your own groups. We have one for our caretakers:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
$rollout.define_group(:caretakers) do |user|
|
70
|
+
user.caretaker?
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
You can activate multiple groups per feature.
|
75
|
+
|
76
|
+
Deactivate groups like this:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
$rollout.deactivate_group(:chat, :all)
|
80
|
+
```
|
81
|
+
|
82
|
+
Groups need to be defined every time your app starts. The logic is not persisted
|
83
|
+
anywhere.
|
84
|
+
|
85
|
+
## Specific Users
|
86
|
+
|
87
|
+
You might want to let a specific user into a beta test or something. If that
|
88
|
+
user isn't part of an existing group, you can let them in specifically:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
$rollout.activate_user(:chat, @user)
|
92
|
+
```
|
93
|
+
|
94
|
+
Deactivate them like this:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
$rollout.deactivate_user(:chat, @user)
|
98
|
+
```
|
99
|
+
|
100
|
+
## User Percentages
|
101
|
+
|
102
|
+
If you're rolling out a new feature, you might want to test the waters by
|
103
|
+
slowly enabling it for a percentage of your users.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
$rollout.activate_percentage(:chat, 20)
|
107
|
+
```
|
108
|
+
|
109
|
+
The algorithm for determining which users get let in is this:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
CRC32(user.id) < (2**32 - 1) / 100.0 * percentage
|
113
|
+
```
|
114
|
+
|
115
|
+
So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users
|
116
|
+
would remain in as the percentage increases.
|
117
|
+
|
118
|
+
Deactivate all percentages like this:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
$rollout.deactivate_percentage(:chat)
|
122
|
+
```
|
123
|
+
|
124
|
+
_Note that activating a feature for 100% of users will also make it active
|
125
|
+
"globally". That is when calling Rollout#active? without a user object._
|
126
|
+
|
127
|
+
In some cases you might want to have a feature activated for a random set of
|
128
|
+
users. It can come specially handy when using Rollout for split tests.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
$rollout = Rollout.new($redis, randomize_percentage: true)
|
132
|
+
```
|
133
|
+
|
134
|
+
When on `randomize_percentage` will make sure that 50% of users for feature A
|
135
|
+
are selected independently from users for feature B.
|
136
|
+
|
137
|
+
## Global actions
|
138
|
+
|
139
|
+
While groups can come in handy, the actual global setter for a feature does not require a group to be passed.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
$rollout.activate(:chat)
|
143
|
+
```
|
144
|
+
|
145
|
+
In that case you can check the global availability of a feature using the following
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
$rollout.active?(:chat)
|
149
|
+
```
|
150
|
+
|
151
|
+
And if something is wrong you can set a feature off for everybody using
|
152
|
+
|
153
|
+
Deactivate everybody at once:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
$rollout.deactivate(:chat)
|
157
|
+
```
|
158
|
+
|
159
|
+
For many of our features, we keep track of error rates using redis, and
|
160
|
+
deactivate them automatically when a threshold is reached to prevent service
|
161
|
+
failures from cascading. See https://github.com/jamesgolick/degrade for the
|
162
|
+
failure detection code.
|
163
|
+
|
164
|
+
## Namespacing
|
165
|
+
|
166
|
+
Rollout separates its keys from other keys in the data store using the
|
167
|
+
"feature" keyspace.
|
168
|
+
|
169
|
+
If you're using redis, you can namespace keys further to support multiple
|
170
|
+
environments by using the
|
171
|
+
[redis-namespace](https://github.com/resque/redis-namespace) gem.
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
$ns = Redis::Namespace.new(Rails.env, redis: $redis)
|
175
|
+
$rollout = Rollout.new($ns)
|
176
|
+
$rollout.activate_group(:chat, :all)
|
177
|
+
```
|
178
|
+
|
179
|
+
This example would use the "development:feature:chat:groups" key.
|
180
|
+
|
181
|
+
## Frontend / UI
|
182
|
+
|
183
|
+
* [Rollout-Dashboard](https://github.com/fiverr/rollout_dashboard/)
|
184
|
+
|
185
|
+
## Implementations in other languages
|
186
|
+
|
187
|
+
* Python: https://github.com/asenchi/proclaim
|
188
|
+
* PHP: https://github.com/opensoft/rollout
|
189
|
+
* Clojure: https://github.com/yeller/shoutout
|
190
|
+
* Perl: https://metacpan.org/pod/Toggle
|
191
|
+
|
192
|
+
|
193
|
+
## Contributors
|
194
|
+
|
195
|
+
* James Golick - Creator - https://github.com/jamesgolick
|
196
|
+
* Eric Rafaloff - Maintainer - https://github.com/EricR
|
197
|
+
|
198
|
+
|
199
|
+
## Copyright
|
200
|
+
|
201
|
+
Copyright (c) 2010-InfinityAndBeyond BitLove, Inc. See LICENSE for details.
|
data/Rakefile
ADDED
data/lib/rollout.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rollout/feature'
|
4
|
+
require 'rollout/logging'
|
5
|
+
require 'rollout/version'
|
6
|
+
require 'zlib'
|
7
|
+
require 'set'
|
8
|
+
require 'json'
|
9
|
+
require 'observer'
|
10
|
+
|
11
|
+
class Rollout
|
12
|
+
include Observable
|
13
|
+
|
14
|
+
RAND_BASE = (2**32 - 1) / 100.0
|
15
|
+
|
16
|
+
attr_reader :options, :storage
|
17
|
+
|
18
|
+
def initialize(storage, opts = {})
|
19
|
+
@storage = storage
|
20
|
+
@options = opts
|
21
|
+
@groups = { all: ->(_user) { true } }
|
22
|
+
|
23
|
+
extend(Logging) if opts[:logging]
|
24
|
+
end
|
25
|
+
|
26
|
+
def groups
|
27
|
+
@groups.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def activate(feature)
|
31
|
+
with_feature(feature) do |f|
|
32
|
+
f.percentage = 100
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def deactivate(feature)
|
37
|
+
with_feature(feature, &:clear)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(feature)
|
41
|
+
features = (@storage.get(features_key) || '').split(',')
|
42
|
+
features.delete(feature.to_s)
|
43
|
+
@storage.set(features_key, features.join(','))
|
44
|
+
@storage.del(key(feature))
|
45
|
+
|
46
|
+
if respond_to?(:logging)
|
47
|
+
logging.delete(feature)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def set(feature, desired_state)
|
52
|
+
with_feature(feature) do |f|
|
53
|
+
if desired_state
|
54
|
+
f.percentage = 100
|
55
|
+
else
|
56
|
+
f.clear
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def activate_group(feature, group)
|
62
|
+
with_feature(feature) do |f|
|
63
|
+
f.add_group(group)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def deactivate_group(feature, group)
|
68
|
+
with_feature(feature) do |f|
|
69
|
+
f.remove_group(group)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def activate_user(feature, user)
|
74
|
+
with_feature(feature) do |f|
|
75
|
+
f.add_user(user)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def deactivate_user(feature, user)
|
80
|
+
with_feature(feature) do |f|
|
81
|
+
f.remove_user(user)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def activate_users(feature, users)
|
86
|
+
with_feature(feature) do |f|
|
87
|
+
users.each { |user| f.add_user(user) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def deactivate_users(feature, users)
|
92
|
+
with_feature(feature) do |f|
|
93
|
+
users.each { |user| f.remove_user(user) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def set_users(feature, users)
|
98
|
+
with_feature(feature) do |f|
|
99
|
+
f.users = []
|
100
|
+
users.each { |user| f.add_user(user) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def define_group(group, &block)
|
105
|
+
@groups[group.to_sym] = block
|
106
|
+
end
|
107
|
+
|
108
|
+
def active?(feature, user = nil)
|
109
|
+
feature = get(feature)
|
110
|
+
feature.active?(self, user)
|
111
|
+
end
|
112
|
+
|
113
|
+
def user_in_active_users?(feature, user = nil)
|
114
|
+
feature = get(feature)
|
115
|
+
feature.user_in_active_users?(user)
|
116
|
+
end
|
117
|
+
|
118
|
+
def inactive?(feature, user = nil)
|
119
|
+
!active?(feature, user)
|
120
|
+
end
|
121
|
+
|
122
|
+
def activate_percentage(feature, percentage)
|
123
|
+
with_feature(feature) do |f|
|
124
|
+
f.percentage = percentage
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def deactivate_percentage(feature)
|
129
|
+
with_feature(feature) do |f|
|
130
|
+
f.percentage = 0
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def active_in_group?(group, user)
|
135
|
+
f = @groups[group.to_sym]
|
136
|
+
f&.call(user)
|
137
|
+
end
|
138
|
+
|
139
|
+
def get(feature)
|
140
|
+
string = @storage.get(key(feature))
|
141
|
+
Feature.new(feature, string, @options)
|
142
|
+
end
|
143
|
+
|
144
|
+
def set_feature_data(feature, data)
|
145
|
+
with_feature(feature) do |f|
|
146
|
+
f.data.merge!(data) if data.is_a? Hash
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def clear_feature_data(feature)
|
151
|
+
with_feature(feature) do |f|
|
152
|
+
f.data = {}
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def multi_get(*features)
|
157
|
+
return [] if features.empty?
|
158
|
+
|
159
|
+
feature_keys = features.map { |feature| key(feature) }
|
160
|
+
@storage.mget(*feature_keys).map.with_index { |string, index| Feature.new(features[index], string, @options) }
|
161
|
+
end
|
162
|
+
|
163
|
+
def features
|
164
|
+
(@storage.get(features_key) || '').split(',').map(&:to_sym)
|
165
|
+
end
|
166
|
+
|
167
|
+
def feature_states(user = nil)
|
168
|
+
multi_get(*features).each_with_object({}) do |f, hash|
|
169
|
+
hash[f.name] = f.active?(self, user)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def active_features(user = nil)
|
174
|
+
multi_get(*features).select do |f|
|
175
|
+
f.active?(self, user)
|
176
|
+
end.map(&:name)
|
177
|
+
end
|
178
|
+
|
179
|
+
def clear!
|
180
|
+
features.each do |feature|
|
181
|
+
with_feature(feature, &:clear)
|
182
|
+
@storage.del(key(feature))
|
183
|
+
end
|
184
|
+
|
185
|
+
@storage.del(features_key)
|
186
|
+
end
|
187
|
+
|
188
|
+
def exists?(feature)
|
189
|
+
# since redis-rb v4.2, `#exists?` replaces `#exists` which now returns integer value instead of boolean
|
190
|
+
# https://github.com/redis/redis-rb/pull/918
|
191
|
+
if @storage.respond_to?(:exists?)
|
192
|
+
@storage.exists?(key(feature))
|
193
|
+
else
|
194
|
+
@storage.exists(key(feature))
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def with_feature(feature)
|
199
|
+
f = get(feature)
|
200
|
+
|
201
|
+
if count_observers > 0
|
202
|
+
before = Marshal.load(Marshal.dump(f))
|
203
|
+
yield(f)
|
204
|
+
save(f)
|
205
|
+
changed
|
206
|
+
notify_observers(:update, before, f)
|
207
|
+
else
|
208
|
+
yield(f)
|
209
|
+
save(f)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def key(name)
|
216
|
+
"feature:#{name}"
|
217
|
+
end
|
218
|
+
|
219
|
+
def features_key
|
220
|
+
'feature:__features__'
|
221
|
+
end
|
222
|
+
|
223
|
+
def save(feature)
|
224
|
+
@storage.set(key(feature.name), feature.serialize)
|
225
|
+
@storage.set(features_key, (features | [feature.name.to_sym]).join(','))
|
226
|
+
end
|
227
|
+
end
|