arturo 0.2.3.7 → 0.2.3.8
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.
- data/app/models/arturo/feature.rb +5 -1
- data/lib/arturo/feature_caching.rb +75 -15
- data/lib/arturo/test_support.rb +2 -0
- metadata +90 -22
@@ -28,6 +28,10 @@ module Arturo
|
|
28
28
|
self.find(:first, :conditions => { :symbol => feature_or_symbol.to_s })
|
29
29
|
end
|
30
30
|
|
31
|
+
def self.find_feature(*args)
|
32
|
+
to_feature(*args)
|
33
|
+
end
|
34
|
+
|
31
35
|
# Create a new Feature
|
32
36
|
def initialize(attributes = {})
|
33
37
|
super(DEFAULT_ATTRIBUTES.merge(attributes || {}))
|
@@ -76,7 +80,7 @@ module Arturo
|
|
76
80
|
|
77
81
|
def passes_threshold?(feature_recipient)
|
78
82
|
threshold = self.deployment_percentage || 0
|
79
|
-
return false if threshold == 0
|
83
|
+
return false if threshold == 0 || !feature_recipient.id
|
80
84
|
return true if threshold == 100
|
81
85
|
(((feature_recipient.id + (self.id || 1) + 17) * 13) % 100) < threshold
|
82
86
|
end
|
@@ -16,13 +16,21 @@ module Arturo
|
|
16
16
|
# to use a shared cache like Memcached.
|
17
17
|
module FeatureCaching
|
18
18
|
|
19
|
+
# A marker in the cache to record the fact that the feature with the
|
20
|
+
# given symbol doesn't exist.
|
21
|
+
NO_SUCH_FEATURE = :NO_SUCH_FEATURE
|
22
|
+
|
19
23
|
def self.extended(base)
|
20
|
-
class <<base
|
24
|
+
class << base
|
21
25
|
alias_method_chain :to_feature, :caching
|
22
|
-
attr_accessor :cache_ttl, :feature_cache
|
26
|
+
attr_accessor :cache_ttl, :feature_cache, :feature_caching_strategy
|
27
|
+
end
|
28
|
+
base.send(:after_save) do |f|
|
29
|
+
f.class.feature_caching_strategy.expire(f.class.feature_cache, f.symbol.to_sym) if f.class.caches_features?
|
23
30
|
end
|
24
31
|
base.cache_ttl = 0
|
25
32
|
base.feature_cache = Arturo::FeatureCaching::Cache.new
|
33
|
+
base.feature_caching_strategy = AllStrategy
|
26
34
|
end
|
27
35
|
|
28
36
|
def caches_features?
|
@@ -31,28 +39,75 @@ module Arturo
|
|
31
39
|
|
32
40
|
# Wraps Arturo::Feature.to_feature with in-memory caching.
|
33
41
|
def to_feature_with_caching(feature_or_symbol)
|
34
|
-
if !
|
35
|
-
|
36
|
-
elsif
|
37
|
-
feature_cache.write(feature_or_symbol.symbol, feature_or_symbol, :expires_in => cache_ttl)
|
42
|
+
if !caches_features?
|
43
|
+
to_feature_without_caching(feature_or_symbol)
|
44
|
+
elsif feature_or_symbol.kind_of?(Arturo::Feature)
|
38
45
|
feature_or_symbol
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
else
|
47
|
+
symbol = feature_or_symbol.to_sym
|
48
|
+
feature = feature_caching_strategy.fetch(feature_cache, symbol) { to_feature_without_caching(symbol) }
|
49
|
+
feature unless feature == NO_SUCH_FEATURE
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def warm_cache!
|
54
|
+
warn "Deprecated, no longer necessary!"
|
55
|
+
end
|
56
|
+
|
57
|
+
class AllStrategy
|
58
|
+
class << self
|
59
|
+
def fetch(cache, symbol, &block)
|
60
|
+
features = cache.read("arturo.all")
|
61
|
+
|
62
|
+
unless cache_is_current?(cache, features)
|
63
|
+
features = Hash[Arturo::Feature.all.map { |f| [f.symbol.to_sym, f] }]
|
64
|
+
mark_as_current!(cache)
|
65
|
+
cache.write("arturo.all", features, :expires_in => Arturo::Feature.cache_ttl * 10)
|
66
|
+
end
|
67
|
+
|
68
|
+
features[symbol] || NO_SUCH_FEATURE
|
69
|
+
end
|
70
|
+
|
71
|
+
def expire(cache, symbol)
|
72
|
+
cache.delete("arturo.all")
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def cache_is_current?(cache, features)
|
78
|
+
return unless features
|
79
|
+
return true if cache.read("arturo.current")
|
80
|
+
return false if features.values.map(&:updated_at).compact.max != Arturo::Feature.maximum(:updated_at)
|
81
|
+
mark_as_current!(cache)
|
82
|
+
end
|
83
|
+
|
84
|
+
def mark_as_current!(cache)
|
85
|
+
cache.write("arturo.current", true, :expires_in => Arturo::Feature.cache_ttl)
|
86
|
+
end
|
44
87
|
end
|
45
88
|
end
|
46
89
|
|
47
|
-
|
90
|
+
class OneStrategy
|
91
|
+
def self.fetch(cache, symbol, &block)
|
92
|
+
if feature = cache.read("arturo.#{symbol}")
|
93
|
+
feature
|
94
|
+
else
|
95
|
+
cache.write("arturo.#{symbol}", yield || NO_SUCH_FEATURE, :expires_in => Arturo::Feature.cache_ttl)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.expire(cache, symbol)
|
100
|
+
cache.delete("arturo.#{symbol}")
|
101
|
+
end
|
102
|
+
end
|
48
103
|
|
49
104
|
# Quack like a Rails cache.
|
50
105
|
class Cache
|
51
106
|
def initialize
|
52
107
|
@data = {} # of the form {key => [value, expires_at or nil]}
|
53
108
|
end
|
109
|
+
|
54
110
|
def read(name, options = nil)
|
55
|
-
name = name.to_s
|
56
111
|
value, expires_at = *@data[name]
|
57
112
|
if value && (expires_at.blank? || expires_at > Time.now)
|
58
113
|
value
|
@@ -60,8 +115,12 @@ module Arturo
|
|
60
115
|
nil
|
61
116
|
end
|
62
117
|
end
|
118
|
+
|
119
|
+
def delete(name)
|
120
|
+
@data.delete(name)
|
121
|
+
end
|
122
|
+
|
63
123
|
def write(name, value, options = nil)
|
64
|
-
name = name.to_s
|
65
124
|
expires_at = if options && options.respond_to?(:[]) && options[:expires_in]
|
66
125
|
Time.now + options.delete(:expires_in)
|
67
126
|
else
|
@@ -71,6 +130,7 @@ module Arturo
|
|
71
130
|
@data[name] = [value, expires_at]
|
72
131
|
end
|
73
132
|
end
|
133
|
+
|
74
134
|
def clear
|
75
135
|
@data.clear
|
76
136
|
end
|
@@ -78,4 +138,4 @@ module Arturo
|
|
78
138
|
|
79
139
|
end
|
80
140
|
|
81
|
-
end
|
141
|
+
end
|
data/lib/arturo/test_support.rb
CHANGED
@@ -9,6 +9,7 @@ Arturo.instance_eval do
|
|
9
9
|
def enable_feature!(name)
|
10
10
|
feature = Arturo::Feature.to_feature(name)
|
11
11
|
if feature
|
12
|
+
feature = feature.class.find(feature.id) if feature.frozen?
|
12
13
|
feature.update_attributes(:deployment_percentage => 100)
|
13
14
|
else
|
14
15
|
Arturo::Feature.create!(:symbol => name, :deployment_percentage => 100)
|
@@ -23,6 +24,7 @@ Arturo.instance_eval do
|
|
23
24
|
# @param [Symbol, String] name the feature name
|
24
25
|
def disable_feature!(name)
|
25
26
|
if (feature = Arturo::Feature.to_feature(name))
|
27
|
+
feature = feature.class.find(feature.id) if feature.frozen?
|
26
28
|
feature.update_attributes(:deployment_percentage => 0)
|
27
29
|
end
|
28
30
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arturo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.3.
|
4
|
+
version: 0.2.3.8
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-10-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,31 @@ dependencies:
|
|
21
21
|
version: 2.3.8
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.3.8
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: mocha
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.13.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.13.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
28
49
|
none: false
|
29
50
|
requirements:
|
30
51
|
- - ! '>='
|
@@ -32,10 +53,15 @@ dependencies:
|
|
32
53
|
version: '0'
|
33
54
|
type: :development
|
34
55
|
prerelease: false
|
35
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
36
62
|
- !ruby/object:Gem::Dependency
|
37
|
-
name:
|
38
|
-
requirement:
|
63
|
+
name: bump
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
39
65
|
none: false
|
40
66
|
requirements:
|
41
67
|
- - ! '>='
|
@@ -43,21 +69,47 @@ dependencies:
|
|
43
69
|
version: '0'
|
44
70
|
type: :development
|
45
71
|
prerelease: false
|
46
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
47
78
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
49
|
-
requirement:
|
79
|
+
name: minitest
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
50
81
|
none: false
|
51
82
|
requirements:
|
52
|
-
- -
|
83
|
+
- - <
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '5'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - <
|
53
92
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
93
|
+
version: '5'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: minitest-rg
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
55
102
|
type: :development
|
56
103
|
prerelease: false
|
57
|
-
version_requirements:
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
58
110
|
- !ruby/object:Gem::Dependency
|
59
111
|
name: sqlite3-ruby
|
60
|
-
requirement:
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
61
113
|
none: false
|
62
114
|
requirements:
|
63
115
|
- - ~>
|
@@ -65,10 +117,15 @@ dependencies:
|
|
65
117
|
version: '1.3'
|
66
118
|
type: :development
|
67
119
|
prerelease: false
|
68
|
-
version_requirements:
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '1.3'
|
69
126
|
- !ruby/object:Gem::Dependency
|
70
127
|
name: factory_girl
|
71
|
-
requirement:
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
72
129
|
none: false
|
73
130
|
requirements:
|
74
131
|
- - ~>
|
@@ -76,10 +133,15 @@ dependencies:
|
|
76
133
|
version: '1.3'
|
77
134
|
type: :development
|
78
135
|
prerelease: false
|
79
|
-
version_requirements:
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ~>
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '1.3'
|
80
142
|
- !ruby/object:Gem::Dependency
|
81
143
|
name: timecop
|
82
|
-
requirement:
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
83
145
|
none: false
|
84
146
|
requirements:
|
85
147
|
- - ~>
|
@@ -87,7 +149,12 @@ dependencies:
|
|
87
149
|
version: '0.3'
|
88
150
|
type: :development
|
89
151
|
prerelease: false
|
90
|
-
version_requirements:
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ~>
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0.3'
|
91
158
|
description: Deploy features incrementally to your users
|
92
159
|
email: james.a.rosen@gmail.com
|
93
160
|
executables: []
|
@@ -147,8 +214,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
214
|
version: '0'
|
148
215
|
requirements: []
|
149
216
|
rubyforge_project:
|
150
|
-
rubygems_version: 1.8.
|
217
|
+
rubygems_version: 1.8.25
|
151
218
|
signing_key:
|
152
|
-
specification_version:
|
219
|
+
specification_version: 3
|
153
220
|
summary: Feature sliders, wrapped up in an engine
|
154
221
|
test_files: []
|
222
|
+
has_rdoc:
|