fluidfeatures 0.5.0 → 0.6.0
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/.travis.yml +0 -1
- data/README.md +2 -2
- data/lib/fluidfeatures/app/reporter.rb +72 -34
- data/lib/fluidfeatures/app/state.rb +95 -35
- data/lib/fluidfeatures/app/transaction.rb +1 -1
- data/lib/fluidfeatures/client.rb +4 -0
- data/lib/fluidfeatures/config.rb +41 -8
- data/lib/fluidfeatures/exceptions.rb +16 -0
- data/lib/fluidfeatures/persistence/buckets.rb +4 -4
- data/lib/fluidfeatures/persistence/features.rb +9 -9
- data/lib/fluidfeatures/persistence/storage.rb +6 -9
- data/lib/fluidfeatures/version.rb +1 -1
- data/lib/fluidfeatures.rb +5 -5
- data/spec/app/reporter_spec.rb +15 -4
- data/spec/app/state_spec.rb +18 -7
- data/spec/config_spec.rb +27 -21
- data/spec/fixtures/fluidfeatures.yml +4 -4
- data/spec/integration/fluidfeatures_spec.rb +23 -5
- data/spec/persistence/features_spec.rb +2 -2
- data/spec/spec_helper.rb +0 -5
- data/spec/support/api_helpers.rb +3 -3
- data/spec/support/polling_loop_shared_examples.rb +3 -9
- metadata +3 -2
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -35,8 +35,8 @@ Call `app` on the `FluidFeatures` module to instantiate this object.
|
|
35
35
|
require 'fluidfeatures'
|
36
36
|
|
37
37
|
config = {
|
38
|
-
"
|
39
|
-
"
|
38
|
+
"base_uri" => "https://www.fluidfeatures.com/service"
|
39
|
+
"app_id" => "1vu33ki6emqe3"
|
40
40
|
"secret" = "sssseeecrrreeetttt"
|
41
41
|
}
|
42
42
|
|
@@ -27,13 +27,26 @@ module FluidFeatures
|
|
27
27
|
|
28
28
|
def initialize(app)
|
29
29
|
raise "app invalid : #{app}" unless app.is_a? ::FluidFeatures::App
|
30
|
+
@sending = false
|
30
31
|
configure(app)
|
31
|
-
run_loop
|
32
32
|
at_exit do
|
33
33
|
buckets_storage.append(@buckets)
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
def start_sending
|
38
|
+
return if @sending
|
39
|
+
@sending = true
|
40
|
+
run_loop
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop_sending(wait=false)
|
44
|
+
@sending = false
|
45
|
+
if wait
|
46
|
+
@loop_thread.join if @loop_thread and @loop_thread.alive?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
37
50
|
def buckets_storage
|
38
51
|
@buckets_storage ||= FluidFeatures::Persistence::Buckets.create(FluidFeatures.config["cache"])
|
39
52
|
end
|
@@ -96,48 +109,66 @@ module FluidFeatures
|
|
96
109
|
features_storage.replace_unknown(@unknown_features)
|
97
110
|
end
|
98
111
|
|
112
|
+
start_sending unless @sending
|
99
113
|
end
|
100
114
|
|
101
115
|
def run_loop
|
102
|
-
Thread.new do
|
103
|
-
while true
|
104
|
-
begin
|
105
116
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
117
|
+
return unless @sending
|
118
|
+
return if @loop_thread and @loop_thread.alive?
|
119
|
+
|
120
|
+
@loop_thread = Thread.new do
|
121
|
+
while @sending
|
122
|
+
run_loop_iteration(
|
123
|
+
WAIT_BETWEEN_QUEUE_EMTPY_CHECKS,
|
124
|
+
WAIT_BETWEEN_SEND_SUCCESS_NONE_WAITING,
|
125
|
+
WAIT_BETWEEN_SEND_SUCCESS_NEXT_WAITING,
|
126
|
+
WAIT_BETWEEN_SEND_FAILURES
|
127
|
+
)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
110
131
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
# If service is down, then slow our requests
|
123
|
-
# within this thread
|
124
|
-
sleep WAIT_BETWEEN_SEND_FAILURES
|
125
|
-
end
|
132
|
+
def run_loop_iteration(
|
133
|
+
wait_between_queue_emtpy_checks,
|
134
|
+
wait_between_send_success_none_waiting,
|
135
|
+
wait_between_send_success_next_waiting,
|
136
|
+
wait_between_send_failures)
|
137
|
+
begin
|
138
|
+
|
139
|
+
unless transactions_queued?
|
140
|
+
sleep wait_between_queue_emtpy_checks
|
141
|
+
return
|
142
|
+
end
|
126
143
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
144
|
+
success = send_transactions
|
145
|
+
|
146
|
+
if success
|
147
|
+
# Unless we have a full bucket waiting do not make
|
148
|
+
# more than N requests per second.
|
149
|
+
if bucket_count <= 1
|
150
|
+
sleep wait_between_send_success_none_waiting
|
151
|
+
else
|
152
|
+
sleep wait_between_send_success_next_waiting
|
132
153
|
end
|
154
|
+
else
|
155
|
+
# If service is down, then slow our requests
|
156
|
+
# within this thread
|
157
|
+
sleep wait_between_send_failures
|
133
158
|
end
|
159
|
+
|
160
|
+
rescue Exception => err
|
161
|
+
# catch errors, so that we do not affect the rest of the application
|
162
|
+
app.logger.error "[FF] send_transactions failed : #{err.message}\n#{err.backtrace.join("\n")}"
|
163
|
+
# hold off for a little while and try again
|
164
|
+
sleep wait_between_send_failures
|
134
165
|
end
|
135
166
|
end
|
136
167
|
|
137
168
|
@private
|
138
169
|
def transactions_queued?
|
139
170
|
have_transactions = false
|
140
|
-
|
171
|
+
buckets_lock_synchronize do
|
141
172
|
if @buckets.size == 1
|
142
173
|
@current_bucket_lock.synchronize do
|
143
174
|
if @current_bucket.size > 0
|
@@ -163,7 +194,7 @@ module FluidFeatures
|
|
163
194
|
end
|
164
195
|
|
165
196
|
remaining_buckets_stats = nil
|
166
|
-
|
197
|
+
buckets_lock_synchronize do
|
167
198
|
remaining_buckets_stats = @buckets.map { |b| b.size }
|
168
199
|
end
|
169
200
|
|
@@ -205,7 +236,7 @@ module FluidFeatures
|
|
205
236
|
@private
|
206
237
|
def bucket_count
|
207
238
|
num_buckets = 0
|
208
|
-
|
239
|
+
buckets_lock_synchronize do
|
209
240
|
num_buckets = @buckets.size
|
210
241
|
end
|
211
242
|
num_buckets
|
@@ -214,7 +245,7 @@ module FluidFeatures
|
|
214
245
|
@private
|
215
246
|
def new_bucket
|
216
247
|
bucket = []
|
217
|
-
|
248
|
+
buckets_lock_synchronize do
|
218
249
|
@buckets << bucket
|
219
250
|
if @buckets.size > MAX_BUCKETS
|
220
251
|
#offload to storage
|
@@ -229,7 +260,7 @@ module FluidFeatures
|
|
229
260
|
@private
|
230
261
|
def remove_bucket
|
231
262
|
removed_bucket = nil
|
232
|
-
|
263
|
+
buckets_lock_synchronize do
|
233
264
|
#try to get buckets from storage first
|
234
265
|
if @buckets.empty? && !buckets_storage.empty?
|
235
266
|
@buckets = buckets_storage.fetch(MAX_BUCKETS)
|
@@ -251,7 +282,7 @@ module FluidFeatures
|
|
251
282
|
@private
|
252
283
|
def unremove_bucket(bucket)
|
253
284
|
success = false
|
254
|
-
|
285
|
+
buckets_lock_synchronize do
|
255
286
|
if @buckets.size <= MAX_BUCKETS
|
256
287
|
@buckets.unshift bucket
|
257
288
|
success = true
|
@@ -292,5 +323,12 @@ module FluidFeatures
|
|
292
323
|
end
|
293
324
|
end
|
294
325
|
|
326
|
+
@private
|
327
|
+
def buckets_lock_synchronize
|
328
|
+
@buckets_lock.synchronize do
|
329
|
+
yield
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
295
333
|
end
|
296
334
|
end
|
@@ -4,6 +4,7 @@ require "thread"
|
|
4
4
|
|
5
5
|
require "fluidfeatures/const"
|
6
6
|
require "fluidfeatures/app/transaction"
|
7
|
+
require "fluidfeatures/exceptions"
|
7
8
|
|
8
9
|
module FluidFeatures
|
9
10
|
class AppState
|
@@ -29,70 +30,121 @@ module FluidFeatures
|
|
29
30
|
|
30
31
|
def initialize(app)
|
31
32
|
raise "app invalid : #{app}" unless app.is_a? ::FluidFeatures::App
|
33
|
+
@receiving = false
|
32
34
|
configure(app)
|
33
|
-
run_loop
|
34
35
|
end
|
35
36
|
|
36
37
|
def configure(app)
|
37
38
|
@app = app
|
38
|
-
@features =
|
39
|
+
@features = nil
|
39
40
|
@features_lock = ::Mutex.new
|
40
41
|
end
|
41
42
|
|
43
|
+
def start_receiving
|
44
|
+
return if @receiving
|
45
|
+
@receiving = true
|
46
|
+
run_loop
|
47
|
+
end
|
48
|
+
|
49
|
+
def stop_receiving(wait=false)
|
50
|
+
@receiving = false
|
51
|
+
if wait
|
52
|
+
@loop_thread.join if @loop_thread and @loop_thread.alive?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
42
56
|
def features_storage
|
43
57
|
@features_storage ||= FluidFeatures::Persistence::Features.create(FluidFeatures.config["cache"])
|
44
58
|
end
|
45
59
|
|
46
60
|
def features
|
47
61
|
f = nil
|
48
|
-
@
|
49
|
-
|
62
|
+
if @receiving
|
63
|
+
# use features loaded in background
|
64
|
+
features_lock_synchronize do
|
65
|
+
f = @features
|
66
|
+
end
|
67
|
+
end
|
68
|
+
unless f
|
69
|
+
# we have not loaded features yet.
|
70
|
+
# load in foreground but do not use caching (etags)
|
71
|
+
success, state = load_state(use_cache=false)
|
72
|
+
if success
|
73
|
+
unless state
|
74
|
+
# Since we did not use etag caching, state should never
|
75
|
+
# be nil if success was true.
|
76
|
+
raise FFeaturesAppStateLoadFailure.new("Unexpected nil state returned from successful load_state(use_cache=false).")
|
77
|
+
end
|
78
|
+
self.features = f = state
|
79
|
+
else
|
80
|
+
# fluidfeatures API must be down.
|
81
|
+
# load persisted features from disk.
|
82
|
+
self.features = f = features_storage.list
|
83
|
+
end
|
84
|
+
end
|
85
|
+
# we should never return nil
|
86
|
+
unless f
|
87
|
+
# If we still could not load state then croak
|
88
|
+
raise FFeaturesAppStateLoadFailure.new("Could not load features state from API: #{state}")
|
89
|
+
end
|
90
|
+
unless @receiving
|
91
|
+
# start background receiver loop
|
92
|
+
start_receiving
|
50
93
|
end
|
51
94
|
f
|
52
95
|
end
|
53
96
|
|
54
97
|
def features= f
|
55
98
|
return unless f.is_a? Hash
|
56
|
-
|
57
|
-
features_storage.replace(f)
|
99
|
+
features_lock_synchronize do
|
100
|
+
features_storage.replace(f)
|
58
101
|
@features = f
|
59
102
|
end
|
103
|
+
f
|
60
104
|
end
|
61
105
|
|
62
106
|
def run_loop
|
63
|
-
Thread.new do
|
64
|
-
while true
|
65
|
-
begin
|
66
|
-
|
67
|
-
success, state = load_state
|
68
|
-
|
69
|
-
# Note, success could be true, but state might be nil.
|
70
|
-
# This occurs with 304 (no change)
|
71
|
-
if success and state
|
72
|
-
# switch out current state with new one
|
73
|
-
self.features = state
|
74
|
-
elsif not success
|
75
|
-
# If service is down, then slow our requests
|
76
|
-
# within this thread
|
77
|
-
sleep WAIT_BETWEEN_FETCH_FAILURES
|
78
|
-
end
|
79
107
|
|
80
|
-
|
81
|
-
|
82
|
-
sleep WAIT_BETWEEN_FETCH_SUCCESS
|
108
|
+
return unless @receiving
|
109
|
+
return if @loop_thread and @loop_thread.alive?
|
83
110
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
111
|
+
@loop_thread = Thread.new do
|
112
|
+
while @receiving
|
113
|
+
run_loop_iteration(WAIT_BETWEEN_FETCH_SUCCESS, WAIT_BETWEEN_FETCH_FAILURES)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def run_loop_iteration(wait_between_fetch_success, wait_between_fetch_failures)
|
119
|
+
begin
|
120
|
+
|
121
|
+
success, state = load_state
|
122
|
+
|
123
|
+
# Note, success could be true, but state might be nil.
|
124
|
+
# This occurs with 304 (no change)
|
125
|
+
if success and state
|
126
|
+
# switch out current state with new one
|
127
|
+
self.features = state
|
128
|
+
elsif not success
|
129
|
+
# If service is down, then slow our requests
|
130
|
+
# within this thread
|
131
|
+
sleep wait_between_fetch_failures
|
90
132
|
end
|
133
|
+
|
134
|
+
# What ever happens never make more than N requests
|
135
|
+
# per second
|
136
|
+
sleep wait_between_fetch_success
|
137
|
+
|
138
|
+
rescue Exception => err
|
139
|
+
# catch errors, so that we do not affect the rest of the application
|
140
|
+
app.logger.error "load_state failed : #{err.message}\n#{err.backtrace.join("\n")}"
|
141
|
+
# hold off for a little while and try again
|
142
|
+
sleep wait_between_fetch_failures
|
91
143
|
end
|
92
144
|
end
|
93
145
|
|
94
|
-
def load_state
|
95
|
-
success, state = app.get("/features", { :verbose => true, :etag_wait => ETAG_WAIT },
|
146
|
+
def load_state(use_cache=true)
|
147
|
+
success, state = app.get("/features", { :verbose => true, :etag_wait => ETAG_WAIT }, use_cache)
|
96
148
|
if success and state
|
97
149
|
state.each_pair do |feature_name, feature|
|
98
150
|
feature["versions"].each_pair do |version_name, version|
|
@@ -109,8 +161,6 @@ module FluidFeatures
|
|
109
161
|
version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
|
110
162
|
raise "version_name invalid : #{version_name}" unless version_name.is_a? String
|
111
163
|
|
112
|
-
#assert(isinstance(user_id, basestring))
|
113
|
-
|
114
164
|
user_attributes ||= {}
|
115
165
|
user_attributes["user"] = user_id.to_s
|
116
166
|
if user_id.is_a? Integer
|
@@ -123,7 +173,10 @@ module FluidFeatures
|
|
123
173
|
enabled = false
|
124
174
|
|
125
175
|
feature = features[feature_name]
|
176
|
+
return false unless feature
|
126
177
|
version = feature["versions"][version_name]
|
178
|
+
return false unless version
|
179
|
+
|
127
180
|
modulus = user_id_hash % feature["num_parts"]
|
128
181
|
enabled = version["parts"].include? modulus
|
129
182
|
|
@@ -150,5 +203,12 @@ module FluidFeatures
|
|
150
203
|
enabled
|
151
204
|
end
|
152
205
|
|
206
|
+
@private
|
207
|
+
def features_lock_synchronize
|
208
|
+
@features_lock.synchronize do
|
209
|
+
yield
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
153
213
|
end
|
154
214
|
end
|
data/lib/fluidfeatures/client.rb
CHANGED
@@ -7,6 +7,7 @@ require "uuid"
|
|
7
7
|
require "json"
|
8
8
|
|
9
9
|
require "fluidfeatures/app"
|
10
|
+
require 'fluidfeatures/exceptions'
|
10
11
|
|
11
12
|
module FluidFeatures
|
12
13
|
class Client
|
@@ -20,6 +21,9 @@ module FluidFeatures
|
|
20
21
|
|
21
22
|
def initialize(base_uri, logger)
|
22
23
|
|
24
|
+
raise FFeaturesBadParam.new("base_uri is not String, is #{base_uri.class}") \
|
25
|
+
unless base_uri.is_a? String
|
26
|
+
|
23
27
|
@uuid = UUID.new.generate
|
24
28
|
@logger = logger
|
25
29
|
@base_uri = base_uri
|
data/lib/fluidfeatures/config.rb
CHANGED
@@ -1,24 +1,57 @@
|
|
1
1
|
|
2
2
|
require 'yaml'
|
3
|
-
require '
|
3
|
+
require 'fluidfeatures/exceptions'
|
4
4
|
|
5
5
|
module FluidFeatures
|
6
6
|
class Config
|
7
|
+
|
7
8
|
attr_accessor :vars
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
|
10
|
+
def initialize(source, environment=nil)
|
11
|
+
if source.is_a? String
|
12
|
+
init_from_file(source, environment)
|
13
|
+
elsif source.is_a? Hash
|
14
|
+
init_from_hash(source)
|
15
|
+
else
|
16
|
+
raise FFeaturesConfigInvalid.new(
|
17
|
+
"Invalid 'source' given. Expected file path String or Hash. Got #{source.class}"
|
18
|
+
)
|
19
|
+
end
|
20
|
+
if @vars["cache"] and @vars["cache"]["limit"]
|
21
|
+
@vars["cache"]["limit"] = self.class.parse_file_size(vars["cache"]["limit"])
|
22
|
+
end
|
13
23
|
end
|
14
24
|
|
15
25
|
def [](name)
|
16
26
|
@vars[name.to_s]
|
17
27
|
end
|
18
28
|
|
29
|
+
private
|
30
|
+
|
31
|
+
def init_from_file path, environment
|
32
|
+
unless File.exists? path
|
33
|
+
raise FFeaturesConfigFileNotExists.new("File not found : #{path}")
|
34
|
+
end
|
35
|
+
environments = YAML.load_file path
|
36
|
+
unless environments.is_a? Hash
|
37
|
+
raise FFeaturesConfigInvalid.new("Config is invalid : #{path}")
|
38
|
+
end
|
39
|
+
@vars = (environments["common"] || {}).clone
|
40
|
+
@vars.update environments[environment] if environments[environment]
|
41
|
+
end
|
42
|
+
|
43
|
+
def init_from_hash hash
|
44
|
+
@vars = hash.clone
|
45
|
+
end
|
46
|
+
|
19
47
|
def self.parse_file_size(size)
|
20
|
-
return size if
|
21
|
-
|
48
|
+
return size if size.is_a? Numeric
|
49
|
+
return size.to_i if size.is_a? String and size.match /^\d+$/
|
50
|
+
unless size.is_a? String and (/^(\d+)\s*(k|m|g)b$/i).match(size)
|
51
|
+
raise FFeaturesConfigInvalid.new("Invalid file size string in config : '#{size}'")
|
52
|
+
end
|
53
|
+
$1.to_i * 1024 ** ("kmg".index($2.downcase) + 1)
|
22
54
|
end
|
55
|
+
|
23
56
|
end
|
24
57
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
class FFeaturesException < Exception
|
3
|
+
end
|
4
|
+
|
5
|
+
class FFeaturesConfigInvalid < FFeaturesException
|
6
|
+
end
|
7
|
+
|
8
|
+
class FFeaturesConfigFileNotExists < FFeaturesConfigInvalid
|
9
|
+
end
|
10
|
+
|
11
|
+
class FFeaturesBadParam < FFeaturesException
|
12
|
+
end
|
13
|
+
|
14
|
+
class FFeaturesAppStateLoadFailure < FFeaturesException
|
15
|
+
end
|
16
|
+
|
@@ -3,14 +3,14 @@ module FluidFeatures
|
|
3
3
|
class Buckets < Storage
|
4
4
|
attr_accessor :limit
|
5
5
|
|
6
|
-
def self.create(config)
|
6
|
+
def self.create(config, logger=nil)
|
7
7
|
return NullBuckets.new unless config && config["dir"] && config["enable"] && config["limit"] > 0
|
8
|
-
new(config)
|
8
|
+
new(config, logger)
|
9
9
|
end
|
10
10
|
|
11
|
-
def initialize(config)
|
11
|
+
def initialize(config, logger=nil)
|
12
12
|
self.limit = config["limit"]
|
13
|
-
super
|
13
|
+
super
|
14
14
|
end
|
15
15
|
|
16
16
|
def fetch(n = 1)
|
@@ -2,18 +2,17 @@ module FluidFeatures
|
|
2
2
|
module Persistence
|
3
3
|
class Features < Storage
|
4
4
|
|
5
|
-
def self.create(config)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
super config
|
5
|
+
def self.create(config, logger=nil)
|
6
|
+
if config && config["dir"] && config["enable"]
|
7
|
+
new(config, logger)
|
8
|
+
else
|
9
|
+
NullFeatures.new
|
10
|
+
end
|
12
11
|
end
|
13
12
|
|
14
13
|
def list
|
15
14
|
store.transaction(true) do
|
16
|
-
return
|
15
|
+
return nil unless store && store["features"]
|
17
16
|
store["features"]
|
18
17
|
end
|
19
18
|
end
|
@@ -25,6 +24,7 @@ module FluidFeatures
|
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
27
|
+
# TODO: remove. we do not care about persisting these
|
28
28
|
def list_unknown
|
29
29
|
store.transaction(true) do
|
30
30
|
return {} unless store && store["unknown_features"]
|
@@ -53,7 +53,7 @@ module FluidFeatures
|
|
53
53
|
end
|
54
54
|
|
55
55
|
class NullFeatures
|
56
|
-
def list;
|
56
|
+
def list; nil end
|
57
57
|
def list_unknown; {} end
|
58
58
|
def replace(*args); false end
|
59
59
|
def replace_unknown(*args); false end
|
@@ -5,10 +5,10 @@ module FluidFeatures
|
|
5
5
|
class Storage
|
6
6
|
attr_accessor :dir, :file_name, :store, :logger
|
7
7
|
|
8
|
-
def initialize(config)
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
def initialize(config, logger=nil)
|
9
|
+
@dir = config["dir"]
|
10
|
+
@logger = logger || Logger.new(STDERR)
|
11
|
+
@file_name = "#{self.class.to_s.split('::').last.downcase}.pstore"
|
12
12
|
FileUtils.mkpath(dir) unless Dir.exists?(dir)
|
13
13
|
end
|
14
14
|
|
@@ -21,11 +21,8 @@ module FluidFeatures
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def file_size
|
24
|
-
|
25
|
-
|
26
|
-
rescue Exception => _
|
27
|
-
return 0
|
28
|
-
end
|
24
|
+
# TODO: rescue should return nil here
|
25
|
+
File.size(path) rescue 0
|
29
26
|
end
|
30
27
|
end
|
31
28
|
end
|
data/lib/fluidfeatures.rb
CHANGED
@@ -15,10 +15,10 @@ module FluidFeatures
|
|
15
15
|
attr_accessor :config
|
16
16
|
end
|
17
17
|
|
18
|
-
def self.app(config)
|
19
|
-
|
18
|
+
def self.app(config, logger=nil)
|
19
|
+
logger ||= ::Logger.new(STDERR)
|
20
20
|
self.config = config
|
21
|
-
client = ::FluidFeatures::Client.new(config["
|
22
|
-
::FluidFeatures::App.new(client, config["
|
21
|
+
client = ::FluidFeatures::Client.new(config["base_uri"], logger)
|
22
|
+
::FluidFeatures::App.new(client, config["app_id"], config["secret"], logger)
|
23
23
|
end
|
24
|
-
end
|
24
|
+
end
|
data/spec/app/reporter_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe FluidFeatures::AppReporter do
|
4
4
|
|
5
|
-
it_should_behave_like "polling loop"
|
5
|
+
it_should_behave_like "polling loop" do
|
6
6
|
before(:each) { described_class.any_instance.stub(:transactions_queued?).and_return(true) }
|
7
7
|
end
|
8
8
|
|
@@ -15,15 +15,15 @@ describe FluidFeatures::AppReporter do
|
|
15
15
|
let(:transaction) { mock('transaction', url: 'http://example.com/source.html', duration: 999, user: user, unknown_features: [], features_hit: %w[feature], goals_hit: %w[goal]) }
|
16
16
|
|
17
17
|
before(:each) do
|
18
|
-
described_class.any_instance.stub(:run_loop)
|
19
18
|
FluidFeatures.stub(:config).and_return(config)
|
20
19
|
end
|
21
20
|
|
22
21
|
describe "#features_storage" do
|
23
22
|
before(:each) do
|
24
23
|
described_class.any_instance.stub(:configure)
|
25
|
-
FluidFeatures::Persistence::Features
|
26
|
-
|
24
|
+
FluidFeatures::Persistence::Features
|
25
|
+
.should_receive(:create).with(config["cache"])
|
26
|
+
.and_return(FluidFeatures::Persistence::NullFeatures.new)
|
27
27
|
end
|
28
28
|
|
29
29
|
it "should create features storage" do
|
@@ -184,6 +184,17 @@ describe FluidFeatures::AppReporter do
|
|
184
184
|
end
|
185
185
|
end
|
186
186
|
|
187
|
+
describe "#start_sending" do
|
188
|
+
it "#start_sending should call send_transactions" do
|
189
|
+
reporter.should_receive(:transactions_queued?).and_return(true)
|
190
|
+
# send_transactions needs to return success=true to prevent throttling
|
191
|
+
reporter.should_receive(:send_transactions).and_return(true)
|
192
|
+
reporter.start_sending
|
193
|
+
sleep(0.001) # wait for thread to start
|
194
|
+
reporter.stop_sending(wait=true)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
187
198
|
describe "#send_transactions" do
|
188
199
|
let(:unknown_features) { { "feature" => { "a" => true } } }
|
189
200
|
|
data/spec/app/state_spec.rb
CHANGED
@@ -4,7 +4,7 @@ require "fluidfeatures/persistence/features"
|
|
4
4
|
|
5
5
|
describe FluidFeatures::AppState do
|
6
6
|
|
7
|
-
it_should_behave_like "polling loop"
|
7
|
+
it_should_behave_like "polling loop"
|
8
8
|
|
9
9
|
context do
|
10
10
|
let(:state) { described_class.new(app) }
|
@@ -16,10 +16,14 @@ describe FluidFeatures::AppState do
|
|
16
16
|
before(:each) do
|
17
17
|
app.stub!(:is_a?).and_return(false)
|
18
18
|
app.stub!(:is_a?).with(FluidFeatures::App).and_return(true)
|
19
|
-
described_class.any_instance.stub(:run_loop)
|
20
19
|
FluidFeatures.stub(:config).and_return(config)
|
21
20
|
end
|
22
21
|
|
22
|
+
after(:each) do
|
23
|
+
# ensure the loop is shutdown
|
24
|
+
state.stop_receiving(wait=true)
|
25
|
+
end
|
26
|
+
|
23
27
|
describe "#features_storage" do
|
24
28
|
before(:each) do
|
25
29
|
described_class.any_instance.stub(:configure)
|
@@ -48,19 +52,26 @@ describe FluidFeatures::AppState do
|
|
48
52
|
features = mock("features")
|
49
53
|
features_storage.should_receive(:list).and_return(features)
|
50
54
|
state.configure(app)
|
51
|
-
state.
|
55
|
+
state.should_receive(:run_loop)
|
56
|
+
state.should_receive(:load_state).with(false).and_return([false,nil])
|
57
|
+
state.features.should == features
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
55
61
|
describe "#features=" do
|
56
|
-
it "should replace features
|
62
|
+
it "should replace features" do
|
57
63
|
state.features_storage.should_receive(:replace).with({foo: "bar"})
|
58
64
|
state.features = {foo: "bar"}
|
59
65
|
end
|
66
|
+
end
|
60
67
|
|
61
|
-
|
62
|
-
|
63
|
-
|
68
|
+
describe "#start_receiving" do
|
69
|
+
it "#start_receiving should call load_state" do
|
70
|
+
# load_state needs to return success=true to prevent throttling
|
71
|
+
state.should_receive(:load_state).and_return([true,{}])
|
72
|
+
state.start_receiving
|
73
|
+
sleep(0.001) # wait for thread to start
|
74
|
+
state.stop_receiving(wait=true)
|
64
75
|
end
|
65
76
|
end
|
66
77
|
|
data/spec/config_spec.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
require "spec_helper"
|
2
|
+
require 'fluidfeatures/exceptions'
|
2
3
|
|
3
4
|
describe FluidFeatures::Config do
|
4
|
-
let(:
|
5
|
+
let(:source) { "#{File.dirname(__FILE__)}/fixtures/fluidfeatures.yml" }
|
5
6
|
|
6
7
|
let(:env) { "development" }
|
7
8
|
|
8
|
-
let(:
|
9
|
-
|
10
|
-
let(:config) { FluidFeatures::Config.new(path, env, replacements) }
|
9
|
+
let(:config) { FluidFeatures::Config.new(source, env) }
|
11
10
|
|
12
11
|
context "parse file size" do
|
13
|
-
[nil,
|
14
|
-
it "should
|
15
|
-
described_class.parse_file_size(input)
|
12
|
+
[nil, "foo", Object.new, Object, [], {} ].each do |input|
|
13
|
+
it "should raise exception for non-matching #{input.class.to_s} #{input||'nil'}" do
|
14
|
+
expect { described_class.parse_file_size(input) }
|
15
|
+
.to raise_error(FFeaturesConfigInvalid)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -21,6 +21,12 @@ describe FluidFeatures::Config do
|
|
21
21
|
described_class.parse_file_size(i).should == o
|
22
22
|
end
|
23
23
|
end
|
24
|
+
|
25
|
+
{ "123" => 123, "456" => 456 }.each do |i, o|
|
26
|
+
it "should read '#{i}' as #{o}" do
|
27
|
+
described_class.parse_file_size(i).should == o
|
28
|
+
end
|
29
|
+
end
|
24
30
|
end
|
25
31
|
|
26
32
|
%w{test development production}.each do |env_name|
|
@@ -29,25 +35,25 @@ describe FluidFeatures::Config do
|
|
29
35
|
|
30
36
|
it "should load environment configuration from yml file and merge common section" do
|
31
37
|
config.vars.should == {
|
32
|
-
"
|
38
|
+
"base_uri" => "base_uri",
|
33
39
|
"cache" => { "enable" => false, "dir" => "cache_dir", "limit" => 2097152 },
|
34
|
-
"
|
40
|
+
"app_id" => "#{env_name}_app_id",
|
35
41
|
"secret" => "#{env_name}_secret"
|
36
42
|
}
|
37
43
|
end
|
44
|
+
end
|
45
|
+
end
|
38
46
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
end
|
50
|
-
end
|
47
|
+
context "with hash config" do
|
48
|
+
let(:source) { { "base_uri" => "env_base_uri", "app_id" => "env_app_id", "secret" => "env_secret" } }
|
49
|
+
|
50
|
+
it "should update variables with passed hash" do
|
51
|
+
config.vars.should == {
|
52
|
+
"base_uri" => "env_base_uri",
|
53
|
+
"app_id" => "env_app_id",
|
54
|
+
"secret" => "env_secret"
|
55
|
+
}
|
51
56
|
end
|
52
57
|
end
|
58
|
+
|
53
59
|
end
|
@@ -1,18 +1,18 @@
|
|
1
1
|
common:
|
2
|
-
|
2
|
+
base_uri: base_uri
|
3
3
|
cache:
|
4
4
|
enable: false
|
5
5
|
dir: cache_dir
|
6
6
|
limit: 2mb
|
7
7
|
|
8
8
|
development:
|
9
|
-
|
9
|
+
app_id: development_app_id
|
10
10
|
secret: development_secret
|
11
11
|
|
12
12
|
test:
|
13
|
-
|
13
|
+
app_id: test_app_id
|
14
14
|
secret: test_secret
|
15
15
|
|
16
16
|
production:
|
17
|
-
|
17
|
+
app_id: production_app_id
|
18
18
|
secret: production_secret
|
@@ -4,12 +4,12 @@ describe "FluidFeatures" do
|
|
4
4
|
|
5
5
|
describe "App initialization" do
|
6
6
|
it "should raise 'host not set' without valid base uri" do
|
7
|
-
expect { FluidFeatures.app(config.remove("
|
8
|
-
to raise_error(
|
7
|
+
expect { FluidFeatures.app(config.remove("base_uri")) }.
|
8
|
+
to raise_error(FFeaturesBadParam, /base_uri/)
|
9
9
|
end
|
10
10
|
|
11
11
|
it "should raise 'app_id invalid' without valid app id" do
|
12
|
-
expect { FluidFeatures.app(config.remove("
|
12
|
+
expect { FluidFeatures.app(config.remove("app_id")) }.
|
13
13
|
to raise_error(StandardError, /app_id invalid/)
|
14
14
|
end
|
15
15
|
|
@@ -19,7 +19,8 @@ describe "FluidFeatures" do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
it "should set @config class variable to passed config" do
|
22
|
-
FluidFeatures::Client.stub!(:new)
|
22
|
+
FluidFeatures::Client.stub!(:new)
|
23
|
+
FluidFeatures::App.stub!(:new)
|
23
24
|
config = mock("config", "[]" => nil, "[]=" => nil)
|
24
25
|
FluidFeatures.app(config)
|
25
26
|
FluidFeatures.config.should == config
|
@@ -32,12 +33,28 @@ describe "FluidFeatures" do
|
|
32
33
|
|
33
34
|
let(:feature) { app.features.pop[feature_name] }
|
34
35
|
|
36
|
+
before(:each) do
|
37
|
+
app.state.should_receive(:run_loop).once do
|
38
|
+
# only iterate once and not in a thread
|
39
|
+
app.state.run_loop_iteration(0,0)
|
40
|
+
end
|
41
|
+
app.reporter.should_receive(:run_loop).once do
|
42
|
+
# only iterate once and not in a thread
|
43
|
+
app.reporter.run_loop_iteration(0,0,0,0)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
after(:each) do
|
48
|
+
# ensure the loop is shutdown
|
49
|
+
app.state.stop_receiving(wait=true)
|
50
|
+
app.reporter.stop_sending(wait=true)
|
51
|
+
end
|
52
|
+
|
35
53
|
specify "#feature_enabled? should create feature" do
|
36
54
|
VCR.use_cassette('feature') do
|
37
55
|
transaction.feature_enabled?(feature_name, "a", true)
|
38
56
|
transaction.feature_enabled?(feature_name, "b", true)
|
39
57
|
commit transaction
|
40
|
-
sleep abit
|
41
58
|
feature["name"].should == feature_name
|
42
59
|
feature["versions"].size.should == 2
|
43
60
|
end
|
@@ -51,5 +68,6 @@ describe "FluidFeatures" do
|
|
51
68
|
sleep abit
|
52
69
|
end
|
53
70
|
end
|
71
|
+
|
54
72
|
end
|
55
73
|
end
|
@@ -26,7 +26,7 @@ describe FluidFeatures::Persistence::Features do
|
|
26
26
|
context "null instance" do
|
27
27
|
let(:storage) { described_class.create(nil) }
|
28
28
|
|
29
|
-
specify { storage.list.should ==
|
29
|
+
specify { storage.list.should == nil }
|
30
30
|
|
31
31
|
specify { storage.list_unknown.should == {} }
|
32
32
|
|
@@ -34,4 +34,4 @@ describe FluidFeatures::Persistence::Features do
|
|
34
34
|
|
35
35
|
specify { storage.replace_unknown([["bucket0"], ["bucket1"]]).should == false }
|
36
36
|
end
|
37
|
-
end
|
37
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/api_helpers.rb
CHANGED
@@ -3,9 +3,9 @@ module FluidFeatures
|
|
3
3
|
def config
|
4
4
|
{
|
5
5
|
"cache" => { "enable" => true, "dir" => "spec/tmp", "limit" => 1024 ** 2 },
|
6
|
-
"
|
7
|
-
"
|
8
|
-
"secret" =>
|
6
|
+
"base_uri" => "https://www.fluidfeatures.com/service",
|
7
|
+
"app_id" => "1vu33ki6emqe3",
|
8
|
+
"secret" => "secret",
|
9
9
|
"logger" => Logger.new("/dev/null")
|
10
10
|
}
|
11
11
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
shared_examples "polling loop" do
|
1
|
+
shared_examples "polling loop" do
|
2
2
|
let(:app) { mock "FluidFeatures::App" }
|
3
3
|
|
4
4
|
before(:each) do
|
@@ -22,18 +22,12 @@ shared_examples "polling loop" do |payload_method|
|
|
22
22
|
described_class.new(app)
|
23
23
|
end
|
24
24
|
|
25
|
-
it "should call #run_loop" do
|
26
|
-
described_class.any_instance.
|
25
|
+
it "should not call #run_loop" do
|
26
|
+
described_class.any_instance.should_not_receive(:run_loop)
|
27
27
|
described_class.new(app)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
it "#run_loop should call payload method" do
|
32
|
-
described_class.any_instance.should_receive(payload_method)
|
33
|
-
described_class.new(app)
|
34
|
-
sleep 0.15
|
35
|
-
end
|
36
|
-
|
37
31
|
it "should initialize @app" do
|
38
32
|
described_class.new(app).app.should == app
|
39
33
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fluidfeatures
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: persistent_http
|
@@ -165,6 +165,7 @@ files:
|
|
165
165
|
- lib/fluidfeatures/client.rb
|
166
166
|
- lib/fluidfeatures/config.rb
|
167
167
|
- lib/fluidfeatures/const.rb
|
168
|
+
- lib/fluidfeatures/exceptions.rb
|
168
169
|
- lib/fluidfeatures/persistence/buckets.rb
|
169
170
|
- lib/fluidfeatures/persistence/features.rb
|
170
171
|
- lib/fluidfeatures/persistence/storage.rb
|