flagger 2.0.9 → 3.0.1
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/lib/flagger.rb +63 -77
- data/lib/flagger/entity.rb +49 -0
- data/lib/flagger/flagger-386.dll +0 -0
- data/lib/flagger/flagger-amd64.dll +0 -0
- data/lib/flagger/libflagger-386.so +0 -0
- data/lib/flagger/libflagger-amd64.so +0 -0
- data/lib/flagger/libflagger.dylib +0 -0
- data/lib/flagger/native.rb +31 -0
- data/lib/flagger/response.rb +7 -0
- data/lib/flagger/version.rb +3 -4
- metadata +70 -39
- data/lib/flagger/cloud.rb +0 -495
- data/lib/flagger/microservice.rb +0 -63
- data/lib/flagger/models.rb +0 -656
- data/lib/flagger/stat.rb +0 -56
data/lib/flagger/cloud.rb
DELETED
@@ -1,495 +0,0 @@
|
|
1
|
-
require 'eventmachine'
|
2
|
-
require 'faraday'
|
3
|
-
require 'faye/websocket'
|
4
|
-
require 'json'
|
5
|
-
require 'lru_redux'
|
6
|
-
require 'set'
|
7
|
-
require 'thread'
|
8
|
-
require 'flagger/models'
|
9
|
-
require 'flagger/stat'
|
10
|
-
require 'flagger/version'
|
11
|
-
|
12
|
-
module FlaggerEnvironments
|
13
|
-
SERVER_URL = 'https://api.airshiphq.com'
|
14
|
-
IDENTIFY_ENDPOINT = "#{SERVER_URL}/v2/identify"
|
15
|
-
GATING_INFO_ENDPOINT = "#{SERVER_URL}/v2/gating-info"
|
16
|
-
|
17
|
-
WEBSOCKET_URL = 'wss://ws.airshiphq.com'
|
18
|
-
WEBSOCKET_GATING_INFO_ENDPOINT = "#{WEBSOCKET_URL}/v2/ws-events"
|
19
|
-
|
20
|
-
CLOUDFRONT_URL = 'https://backup-api.airshiphq.com'
|
21
|
-
CLOUDFRONT_GATING_INFO_ENDPOINT = "#{CLOUDFRONT_URL}/v2/gating-info"
|
22
|
-
|
23
|
-
class CloudDelegate
|
24
|
-
attr_accessor :gating_info
|
25
|
-
attr_accessor :env_key
|
26
|
-
attr_accessor :request_timeout
|
27
|
-
|
28
|
-
def initialize(env_key, request_timeout)
|
29
|
-
@env_key = env_key
|
30
|
-
@request_timeout = request_timeout
|
31
|
-
|
32
|
-
@ingestion_max_items = 500
|
33
|
-
@ingestion_interval = 30
|
34
|
-
@ingestion_lock = Mutex.new
|
35
|
-
|
36
|
-
@entities = []
|
37
|
-
@stats = []
|
38
|
-
@exposures = []
|
39
|
-
@flags = Set[]
|
40
|
-
@ingested_flags = Set[]
|
41
|
-
|
42
|
-
@entity_lru_hashes = LruRedux::Cache.new(500)
|
43
|
-
@first_ingestion = true
|
44
|
-
|
45
|
-
@should_ingest_entities = true
|
46
|
-
@should_ingest_stats = true
|
47
|
-
@should_ingest_exposures = true
|
48
|
-
@should_ingest_flags = true
|
49
|
-
|
50
|
-
begin
|
51
|
-
get_gating_info("#{GATING_INFO_ENDPOINT}/#{@env_key}", 'duration__gating_info')
|
52
|
-
rescue Exception => e
|
53
|
-
STDERR.puts 'Failed to retrieve initial gating info from API', e.backtrace
|
54
|
-
end
|
55
|
-
|
56
|
-
begin
|
57
|
-
get_gating_info("#{CLOUDFRONT_GATING_INFO_ENDPOINT}/#{@env_key}", 'duration__cloudfront_gating_info') unless !@gating_info.nil?
|
58
|
-
rescue Exception => e
|
59
|
-
STDERR.puts 'Failed to retrieve initial gating info from CloudFront backup', e.backtrace
|
60
|
-
end
|
61
|
-
|
62
|
-
# ensure EventMachine is running
|
63
|
-
Thread.new { EventMachine.run } unless EventMachine.reactor_running?
|
64
|
-
Thread.pass until EventMachine.reactor_running?
|
65
|
-
|
66
|
-
em_subscribe_updates
|
67
|
-
em_police_updates
|
68
|
-
em_ingestion_timer
|
69
|
-
end
|
70
|
-
|
71
|
-
def get_gating_info(url, stat_name)
|
72
|
-
stat = FlaggerUtils::Stat.new(stat_name)
|
73
|
-
stat.start
|
74
|
-
conn = Faraday.new(url: url)
|
75
|
-
response = conn.get do |req|
|
76
|
-
req.options.timeout = @request_timeout
|
77
|
-
end
|
78
|
-
if response.status == 200
|
79
|
-
@gating_info = FlaggerModels::GatingInfo.new(JSON.parse(response.body))
|
80
|
-
update_sdk_vars(@gating_info)
|
81
|
-
stat.stop
|
82
|
-
save_stat(stat)
|
83
|
-
else
|
84
|
-
raise "Gating info request failed with HTTP code #{response.code}"
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def em_subscribe_updates()
|
89
|
-
@ws_unsubbed = false
|
90
|
-
@ws = Faye::WebSocket::Client.new("#{WEBSOCKET_GATING_INFO_ENDPOINT}?envkey=#{@env_key}")
|
91
|
-
|
92
|
-
@ws.on :message do |event|
|
93
|
-
begin
|
94
|
-
@last_ws_update = Time.now
|
95
|
-
@gating_info = FlaggerModels::GatingInfo.new(JSON.parse(event.data))
|
96
|
-
update_sdk_vars(@gating_info)
|
97
|
-
rescue Exception => e
|
98
|
-
STDERR.puts 'Failed to update gating information', e
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
@ws.on :close do |event|
|
103
|
-
STDERR.puts ['Connection closed to Airship update server', event.code, event.reason] unless @ws_unsubbed
|
104
|
-
EventMachine::Timer.new(5) do
|
105
|
-
em_subscribe_updates unless @ws_unsubbed
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def unsubscribe_updates()
|
111
|
-
@ws_unsubbed = true
|
112
|
-
@ws&.close
|
113
|
-
end
|
114
|
-
|
115
|
-
def em_police_updates
|
116
|
-
ping_timer = EventMachine::PeriodicTimer.new(5) do
|
117
|
-
if @cleaning then
|
118
|
-
ping_timer.cancel
|
119
|
-
else
|
120
|
-
@ws&.ping do
|
121
|
-
@last_ws_update = Time.now
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
police_update_timer = EventMachine::PeriodicTimer.new(30) do
|
127
|
-
if @cleaning then
|
128
|
-
police_update_timer.cancel
|
129
|
-
else
|
130
|
-
if Time.now - (@last_ws_update || Time.new(0)) > 30
|
131
|
-
STDERR.puts 'Did not receive a keepalive for more than 30 seconds. Reconnecting.'
|
132
|
-
@ws&.close
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
poll_gating_info_timer = EventMachine::PeriodicTimer.new(60) do
|
138
|
-
if @cleaning then
|
139
|
-
poll_gating_info_timer.cancel
|
140
|
-
else
|
141
|
-
if Time.now - (@last_ws_update || Time.new(0)) > 60
|
142
|
-
EM.defer(proc {
|
143
|
-
STDERR.puts 'Did not receive a keepalive for more than 60 seconds. Polling gating info.'
|
144
|
-
begin
|
145
|
-
get_gating_info("#{CLOUDFRONT_GATING_INFO_ENDPOINT}/#{@env_key}", 'duration__cloudfront_gating_info')
|
146
|
-
STDERR.puts 'Polled gating info from CloudFront'
|
147
|
-
rescue Exception => e
|
148
|
-
STDERR.puts 'Failed polling gating info from CloudFront', e.backtrace
|
149
|
-
end
|
150
|
-
})
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
def update_sdk_vars(gating_info)
|
157
|
-
if gating_info&.sdk_info&.ingestion_max_items then
|
158
|
-
@ingestion_max_items = gating_info.sdk_info.ingestion_max_items
|
159
|
-
end
|
160
|
-
if gating_info&.sdk_info&.ingestion_interval then
|
161
|
-
@ingestion_interval = gating_info.sdk_info.ingestion_interval
|
162
|
-
end
|
163
|
-
if gating_info&.sdk_info&.should_ingest_entities then
|
164
|
-
@should_ingest_entities = gating_info.sdk_info.should_ingest_entities
|
165
|
-
end
|
166
|
-
if gating_info&.sdk_info&.should_ingest_stats then
|
167
|
-
@should_ingest_stats = gating_info&.sdk_info&.should_ingest_stats
|
168
|
-
end
|
169
|
-
if gating_info&.sdk_info&.should_ingest_exposures then
|
170
|
-
@should_ingest_exposures = gating_info&.sdk_info&.should_ingest_exposures
|
171
|
-
end
|
172
|
-
if gating_info&.sdk_info&.should_ingest_flags then
|
173
|
-
@should_ingest_flags = gating_info&.sdk_info&.should_ingest_flags
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def maybe_ingest(should_ingest = false)
|
178
|
-
if !@should_ingest_entities then
|
179
|
-
@entities = []
|
180
|
-
end
|
181
|
-
|
182
|
-
if !@should_ingest_stats then
|
183
|
-
@stats = []
|
184
|
-
end
|
185
|
-
|
186
|
-
if !@should_ingest_exposures then
|
187
|
-
@exposures = []
|
188
|
-
end
|
189
|
-
|
190
|
-
if !@should_ingest_flags then
|
191
|
-
@flags = Set[]
|
192
|
-
end
|
193
|
-
|
194
|
-
should_ingest |= (
|
195
|
-
@entities.length > @ingestion_max_items ||
|
196
|
-
@stats.length > @ingestion_max_items ||
|
197
|
-
@exposures.length > @ingestion_max_items ||
|
198
|
-
@flags.length > 0
|
199
|
-
)
|
200
|
-
|
201
|
-
if @first_ingestion then
|
202
|
-
should_ingest |= @entities.length > 0
|
203
|
-
@first_ingestion = !should_ingest
|
204
|
-
end
|
205
|
-
|
206
|
-
should_ingest &= (
|
207
|
-
@entities.length > 0 ||
|
208
|
-
@stats.length > 0 ||
|
209
|
-
@exposures.length > 0 ||
|
210
|
-
@flags.length > 0
|
211
|
-
)
|
212
|
-
|
213
|
-
if should_ingest then
|
214
|
-
entities = @entities
|
215
|
-
stats = @stats
|
216
|
-
exposures = @exposures
|
217
|
-
flags = @flags.to_a
|
218
|
-
@entities = []
|
219
|
-
@stats = []
|
220
|
-
@exposures = []
|
221
|
-
@ingested_flags.merge(@flags)
|
222
|
-
@flags = Set[]
|
223
|
-
|
224
|
-
EM.defer(
|
225
|
-
proc {
|
226
|
-
begin
|
227
|
-
conn = Faraday.new(url: "#{IDENTIFY_ENDPOINT}/#{@env_key}")
|
228
|
-
response = conn.post do |req|
|
229
|
-
req.options.timeout = @request_timeout
|
230
|
-
req.headers['Content-Type'] = 'application/json'
|
231
|
-
req.body = JSON.generate(
|
232
|
-
{
|
233
|
-
'sdk_info' => {
|
234
|
-
'name' => 'ruby',
|
235
|
-
'version' => FlaggerUtils::VERSION
|
236
|
-
},
|
237
|
-
'objects' => entities,
|
238
|
-
'stats' => stats,
|
239
|
-
'exposures' => exposures,
|
240
|
-
'flags' => flags
|
241
|
-
})
|
242
|
-
end
|
243
|
-
if response.status != 200 then
|
244
|
-
STDERR.puts 'Failed to upload entities', response.status
|
245
|
-
nil
|
246
|
-
end
|
247
|
-
rescue Exception => e
|
248
|
-
STDERR.puts 'Failed to upload entities', e
|
249
|
-
nil
|
250
|
-
end
|
251
|
-
})
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def em_ingestion_timer
|
256
|
-
seconds = 0
|
257
|
-
timer = EventMachine::PeriodicTimer.new(1) do
|
258
|
-
timer.cancel unless !@cleaning
|
259
|
-
|
260
|
-
seconds = (seconds + 1) % @ingestion_interval
|
261
|
-
if seconds == 0 then
|
262
|
-
@ingestion_lock.synchronize {
|
263
|
-
maybe_ingest(true)
|
264
|
-
}
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
def cleanup()
|
270
|
-
@cleaning = true
|
271
|
-
unsubscribe_updates
|
272
|
-
@ingestion_lock.synchronize {
|
273
|
-
maybe_ingest(true)
|
274
|
-
}
|
275
|
-
end
|
276
|
-
|
277
|
-
def save_entity(entity)
|
278
|
-
@ingestion_lock.synchronize {
|
279
|
-
if @entity_lru_hashes[entity.id] != entity.hash then
|
280
|
-
@entity_lru_hashes[entity.id] = entity.hash
|
281
|
-
@entities.push(entity.attrs)
|
282
|
-
maybe_ingest()
|
283
|
-
end
|
284
|
-
}
|
285
|
-
end
|
286
|
-
|
287
|
-
def save_stat(stat)
|
288
|
-
@ingestion_lock.synchronize {
|
289
|
-
@stats.push(stat)
|
290
|
-
@stats = FlaggerUtils::Stat.compact(@stats)
|
291
|
-
maybe_ingest()
|
292
|
-
}
|
293
|
-
end
|
294
|
-
|
295
|
-
def save_exposure(exposure)
|
296
|
-
@ingestion_lock.synchronize {
|
297
|
-
@exposures.push(exposure)
|
298
|
-
maybe_ingest()
|
299
|
-
}
|
300
|
-
end
|
301
|
-
|
302
|
-
def save_flag(flag)
|
303
|
-
@ingestion_lock.synchronize {
|
304
|
-
@flags.add(flag)
|
305
|
-
maybe_ingest()
|
306
|
-
}
|
307
|
-
end
|
308
|
-
|
309
|
-
def stringify_keys(hash)
|
310
|
-
if hash.is_a?(Hash) then
|
311
|
-
hash.map {|k, v|
|
312
|
-
[k.to_s, stringify_keys(v)]
|
313
|
-
}.to_h
|
314
|
-
else
|
315
|
-
hash
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
def identify_entity(entity_json)
|
320
|
-
if !entity_json.is_a?(Hash) then
|
321
|
-
STDERR.puts 'Entity must be a hash'
|
322
|
-
nil
|
323
|
-
else
|
324
|
-
entity_json = stringify_keys(entity_json)
|
325
|
-
|
326
|
-
entity = if entity_json['is_group'] then
|
327
|
-
FlaggerModels::GroupEntity.new(entity_json)
|
328
|
-
else
|
329
|
-
FlaggerModels::SingleEntity.new(entity_json)
|
330
|
-
end
|
331
|
-
|
332
|
-
if !entity.valid? then
|
333
|
-
STDERR.puts "Entity validation errors: #{entity.validation_errors}"
|
334
|
-
nil
|
335
|
-
else
|
336
|
-
save_entity(entity)
|
337
|
-
|
338
|
-
entity
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
def treatment(flagger_flag, entity_json)
|
344
|
-
stat = FlaggerUtils::Stat.new('duration__get_treatment')
|
345
|
-
stat.start()
|
346
|
-
|
347
|
-
entity = identify_entity(entity_json)
|
348
|
-
flag = @gating_info&.flag(flagger_flag.flag_name)
|
349
|
-
if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
|
350
|
-
save_flag(flagger_flag.flag_name)
|
351
|
-
end
|
352
|
-
return_value = flag&.treatment(entity)
|
353
|
-
|
354
|
-
if !return_value then
|
355
|
-
return return_value
|
356
|
-
end
|
357
|
-
|
358
|
-
exposure = {
|
359
|
-
flag: flagger_flag.flag_name,
|
360
|
-
type: entity.type,
|
361
|
-
id: entity.id,
|
362
|
-
treatment: return_value[:treatment].codename,
|
363
|
-
method_called: 'get_treatment',
|
364
|
-
eligible: return_value[:eligible]
|
365
|
-
}
|
366
|
-
|
367
|
-
return_value = return_value[:treatment].codename
|
368
|
-
|
369
|
-
save_exposure(exposure)
|
370
|
-
|
371
|
-
stat.stop()
|
372
|
-
save_stat(stat)
|
373
|
-
|
374
|
-
return_value
|
375
|
-
end
|
376
|
-
|
377
|
-
|
378
|
-
def payload(flagger_flag, entity_json)
|
379
|
-
stat = FlaggerUtils::Stat.new('duration__get_payload')
|
380
|
-
stat.start()
|
381
|
-
|
382
|
-
entity = identify_entity(entity_json)
|
383
|
-
flag = @gating_info&.flag(flagger_flag.flag_name)
|
384
|
-
if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
|
385
|
-
save_flag(flagger_flag.flag_name)
|
386
|
-
end
|
387
|
-
return_value = flag&.payload(entity)
|
388
|
-
|
389
|
-
if !return_value then
|
390
|
-
return return_value
|
391
|
-
end
|
392
|
-
|
393
|
-
exposure = {
|
394
|
-
flag: flagger_flag.flag_name,
|
395
|
-
type: entity.type,
|
396
|
-
id: entity.id,
|
397
|
-
treatment: return_value[:treatment].codename,
|
398
|
-
method_called: 'get_payload',
|
399
|
-
eligible: return_value[:eligible]
|
400
|
-
}
|
401
|
-
|
402
|
-
return_value = return_value[:treatment].payload
|
403
|
-
|
404
|
-
save_exposure(exposure)
|
405
|
-
|
406
|
-
stat.stop()
|
407
|
-
save_stat(stat)
|
408
|
-
|
409
|
-
return_value
|
410
|
-
end
|
411
|
-
|
412
|
-
def eligible?(flagger_flag, entity_json)
|
413
|
-
stat = FlaggerUtils::Stat.new('duration__is_eligible')
|
414
|
-
stat.start()
|
415
|
-
|
416
|
-
entity = identify_entity(entity_json)
|
417
|
-
flag = @gating_info&.flag(flagger_flag.flag_name)
|
418
|
-
if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
|
419
|
-
save_flag(flagger_flag.flag_name)
|
420
|
-
end
|
421
|
-
return_value = flag&.eligible?(entity)
|
422
|
-
|
423
|
-
if !return_value then
|
424
|
-
return return_value
|
425
|
-
end
|
426
|
-
|
427
|
-
exposure = {
|
428
|
-
flag: flagger_flag.flag_name,
|
429
|
-
type: entity.type,
|
430
|
-
id: entity.id,
|
431
|
-
treatment: return_value[:treatment].codename,
|
432
|
-
method_called: 'is_eligible',
|
433
|
-
eligible: return_value[:eligible]
|
434
|
-
}
|
435
|
-
|
436
|
-
return_value = return_value[:eligible]
|
437
|
-
|
438
|
-
save_exposure(exposure)
|
439
|
-
|
440
|
-
stat.stop()
|
441
|
-
save_stat(stat)
|
442
|
-
|
443
|
-
return_value
|
444
|
-
end
|
445
|
-
|
446
|
-
def enabled?(flagger_flag, entity_json)
|
447
|
-
stat = FlaggerUtils::Stat.new('duration__is_enabled')
|
448
|
-
stat.start()
|
449
|
-
|
450
|
-
entity = identify_entity(entity_json)
|
451
|
-
flag = @gating_info&.flag(flagger_flag.flag_name)
|
452
|
-
if flag.nil? && !@ingested_flags.include?(flagger_flag.flag_name) then
|
453
|
-
save_flag(flagger_flag.flag_name)
|
454
|
-
end
|
455
|
-
return_value = flag&.enabled?(entity)
|
456
|
-
|
457
|
-
if !return_value then
|
458
|
-
return return_value
|
459
|
-
end
|
460
|
-
|
461
|
-
exposure = {
|
462
|
-
flag: flagger_flag.flag_name,
|
463
|
-
type: entity.type,
|
464
|
-
id: entity.id,
|
465
|
-
treatment: return_value[:treatment].codename,
|
466
|
-
method_called: 'is_enabled',
|
467
|
-
eligible: return_value[:eligible]
|
468
|
-
}
|
469
|
-
|
470
|
-
return_value = !return_value[:treatment].is_off_treatment
|
471
|
-
|
472
|
-
save_exposure(exposure)
|
473
|
-
|
474
|
-
stat.stop()
|
475
|
-
save_stat(stat)
|
476
|
-
|
477
|
-
return_value
|
478
|
-
end
|
479
|
-
|
480
|
-
def publish(entities)
|
481
|
-
if !entities.is_a?(Array) then
|
482
|
-
STDERR.puts 'The "publish" method takes an array of objects (aka entities).'
|
483
|
-
return nil
|
484
|
-
end
|
485
|
-
|
486
|
-
entities.each do |entity|
|
487
|
-
identify_entity(entity)
|
488
|
-
end
|
489
|
-
|
490
|
-
@ingestion_lock.synchronize {
|
491
|
-
maybe_ingest(true)
|
492
|
-
}
|
493
|
-
end
|
494
|
-
end
|
495
|
-
end
|