tailslide 0.1.0 → 0.1.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/.vscode/launch.json +14 -0
- data/.vscode/settings.json +10 -0
- data/Gemfile +5 -0
- data/lib/tailslide/flag_manager.rb +41 -0
- data/lib/tailslide/nats_client.rb +58 -0
- data/lib/tailslide/redis_timeseries_client.rb +21 -0
- data/lib/tailslide/toggler.rb +73 -0
- data/lib/tailslide/version.rb +1 -1
- data/lib/tailslide.rb +3 -1
- data/redis_test.rb +8 -0
- data/tailslide-0.1.0.gem +0 -0
- data/test.rb +129 -0
- metadata +28 -5
- data/.rubocop.yml +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd4e9eea51d59ec4a285b3531dd36e00bb2f437e1df5e50240014c59641c7499
|
4
|
+
data.tar.gz: feaae9d7f5af47900a5031426e9015290a552e0b0eb8060f80b16c2aa148df3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81e5ec75a95f6edce34ad2136376b222e02712bc30b49ed0c0f9423a0a9b9ad78d810269a0002f607d8d9d538779c5e0237e1a264e12dc98fcd59555eb35331b
|
7
|
+
data.tar.gz: c4d4c5276a112b56c2d36596e67a438a47f0f071de792cd46b04021d5e6cfb3fca8d5a712ff26c65eaca9f9565cea7913da8fba5c91db9311b34c6222900ce01
|
data/.vscode/launch.json
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
{
|
2
|
+
// Use IntelliSense to learn about possible attributes.
|
3
|
+
// Hover to view descriptions of existing attributes.
|
4
|
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
5
|
+
"version": "0.2.0",
|
6
|
+
"configurations": [
|
7
|
+
{
|
8
|
+
"name": "Debug Local File",
|
9
|
+
"type": "Ruby",
|
10
|
+
"request": "launch",
|
11
|
+
"program": "./test.rb"
|
12
|
+
}
|
13
|
+
]
|
14
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
{
|
2
|
+
"workbench.colorCustomizations": {
|
3
|
+
"sash.hoverBorder": "#3b3b3b",
|
4
|
+
"titleBar.activeBackground": "#222222",
|
5
|
+
"titleBar.activeForeground": "#e7e7e7",
|
6
|
+
"titleBar.inactiveBackground": "#22222299",
|
7
|
+
"titleBar.inactiveForeground": "#e7e7e799"
|
8
|
+
},
|
9
|
+
"editor.acceptSuggestionOnEnter": "on"
|
10
|
+
}
|
data/Gemfile
CHANGED
@@ -5,8 +5,13 @@ source "https://rubygems.org"
|
|
5
5
|
# Specify your gem's dependencies in tailslide.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
+
gem "async", "~> 2,0.3"
|
9
|
+
gem "nats-pure", "~> 2,1.0"
|
10
|
+
gem "redis", "~>4.7.1"
|
11
|
+
gem "redistimeseries", "~>0.1.2"
|
8
12
|
gem "rake", "~> 13.0"
|
9
13
|
|
10
14
|
gem "minitest", "~> 5.0"
|
11
15
|
|
12
16
|
gem "rubocop", "~> 1.21"
|
17
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'nats_client'
|
2
|
+
require_relative 'redis_timeseries_client'
|
3
|
+
require_relative 'toggler'
|
4
|
+
|
5
|
+
class FlagManger
|
6
|
+
attr_reader :nats_client, :redis_ts_client, :user_context
|
7
|
+
|
8
|
+
def initialize(nats_server:'', stream:'', app_id:'', sdk_key:'', user_context:'', redis_host:'', redis_port:'')
|
9
|
+
@nats_client = NatsClient.new(server_url: nats_server, stream:stream, subject:app_id, callback:method(:set_flags), token:sdk_key)
|
10
|
+
@redis_ts_client = RedisTimeSeriesClient.new(redis_host, redis_port)
|
11
|
+
@user_context = user_context
|
12
|
+
@flags = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize_flags
|
16
|
+
nats_client.initialize_flags
|
17
|
+
redis_ts_client.init
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_flags(flags)
|
21
|
+
@flags = flags
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_flags
|
25
|
+
return @flags
|
26
|
+
end
|
27
|
+
|
28
|
+
def disconnect
|
29
|
+
nats_client.disconnect
|
30
|
+
redis_ts_client.disconnect
|
31
|
+
end
|
32
|
+
|
33
|
+
def new_toggler(config)
|
34
|
+
p config
|
35
|
+
|
36
|
+
return Toggler.new(**config, get_flags:method(:get_flags), user_context:user_context,
|
37
|
+
emit_redis_signal:redis_ts_client.method(:emit_signal)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'async'
|
2
|
+
require "nats/client"
|
3
|
+
TimeoutError = NATS::IO::Timeout
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
|
7
|
+
class NatsClient
|
8
|
+
attr_accessor :nats_connection, :jetstream, :subscribed_stream
|
9
|
+
attr_reader :connection_string, :stream, :subject, :callback
|
10
|
+
def initialize(server_url:'localhost:4222', stream:'', subject:'', callback:nil, token:'')
|
11
|
+
@stream = stream
|
12
|
+
@subject = subject
|
13
|
+
@connection_string = "nats://#{token}#{'@' if token}#{server_url}"
|
14
|
+
@callback = callback
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize_flags
|
18
|
+
connect()
|
19
|
+
fetch_latest_message()
|
20
|
+
fetch_ongoing_event_messages()
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def connect
|
25
|
+
self.nats_connection = NATS.connect(connection_string)
|
26
|
+
self.jetstream = nats_connection.jetstream
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch_latest_message
|
30
|
+
begin
|
31
|
+
latest_msg = jetstream.get_last_msg(stream, subject)
|
32
|
+
json_data = JSON.parse latest_msg.data
|
33
|
+
callback.call(json_data)
|
34
|
+
rescue NATS::Timeout => e
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_ongoing_event_messages
|
39
|
+
Async do |task|
|
40
|
+
self.subscribed_stream = jetstream.pull_subscribe(subject, 'me', config: { deliver_policy: 'new' })
|
41
|
+
begin
|
42
|
+
messages = subscribed_stream.fetch(1)
|
43
|
+
messages.each do |message|
|
44
|
+
message.ack
|
45
|
+
json_data = JSON.parse message.data
|
46
|
+
p json_data
|
47
|
+
callback.call(json_data)
|
48
|
+
end
|
49
|
+
rescue NATS::IO::Timeout => e
|
50
|
+
p e
|
51
|
+
end until nats_connection.closed?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def disconnect
|
56
|
+
nats_connection.close
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
require 'redistimeseries'
|
3
|
+
using Redistimeseries::RedisRefinement
|
4
|
+
|
5
|
+
class RedisTimeSeriesClient
|
6
|
+
attr_reader :host, :port
|
7
|
+
attr_accessor :redis_client
|
8
|
+
def initialize(host, port)
|
9
|
+
@host = host || 'localhost'
|
10
|
+
@port = port || 6379
|
11
|
+
end
|
12
|
+
|
13
|
+
def init
|
14
|
+
self.redis_client = Redis.new(host:host, port:port)
|
15
|
+
end
|
16
|
+
|
17
|
+
def emit_signal(flag_id, app_id, status)
|
18
|
+
redis_client.ts_add(key: "#{flag_id}:#{status}", timestamp:"*", value:1, labels:["status", status, "appId", app_id, "flagId", flag_id])
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
class Toggler
|
4
|
+
attr_reader :flag_name, :get_flags, :feature_cb, :default_cb, :error_condition, :emit_redis_signal, :user_context
|
5
|
+
attr_accessor :app_id, :flag_id
|
6
|
+
def initialize(flag_name:'', feature_cb:nil, default_cb:nil, error_condition:nil, get_flags:nil, emit_redis_signal:nil, user_context:'')
|
7
|
+
@flag_name = flag_name
|
8
|
+
@feature_cb = feature_cb
|
9
|
+
@default_cb = default_cb
|
10
|
+
@error_condition = error_condition
|
11
|
+
@get_flags = get_flags
|
12
|
+
@flag_id = nil
|
13
|
+
@app_id = nil
|
14
|
+
set_flag_id_and_app_id(flag_name)
|
15
|
+
@emit_redis_signal = emit_redis_signal
|
16
|
+
@user_context = user_context
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_flag_active
|
20
|
+
flag = get_matching_flag
|
21
|
+
flag["is_active"] && (is_user_white_listed(flag) || validate_user_rollout(flag))
|
22
|
+
end
|
23
|
+
|
24
|
+
def emit_success
|
25
|
+
return unless flag_id
|
26
|
+
p 'emiting success'
|
27
|
+
emit_redis_signal.call(flag_id, app_id, 'success')
|
28
|
+
end
|
29
|
+
|
30
|
+
def emit_failure
|
31
|
+
return unless flag_id
|
32
|
+
emit_redis_signal.call(flag_id, app_id, 'failure')
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def get_matching_flag
|
37
|
+
flag = get_flags.call.find { |flag| flag["title"] == flag_name}
|
38
|
+
raise Exception.new "Cannot find flag with flag name of: #{flag_name}" unless flag
|
39
|
+
flag
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_flag_id_and_app_id(flag_name)
|
43
|
+
matching_flag = get_matching_flag
|
44
|
+
self.flag_id = matching_flag["id"]
|
45
|
+
self.app_id = matching_flag["app_id"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def is_user_white_listed(flag)
|
49
|
+
flag["white_listed_users"].split(',').include?(user_context)
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_user_rollout(flag)
|
53
|
+
rollout = flag["rollout_percentage"] / 100.0
|
54
|
+
if is_circuit_in_recovery(flag)
|
55
|
+
rollout = rollout * (flag["circuit_recovery_percentage"] / 100.0)
|
56
|
+
end
|
57
|
+
is_user_in_rollout(rollout)
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_circuit_in_recovery(flag)
|
61
|
+
flag["is_recoverable"] && flag["circuit_status"] == "recovery"
|
62
|
+
end
|
63
|
+
|
64
|
+
def is_user_in_rollout(rollout)
|
65
|
+
puts "User context hash #{hash_user_context}"
|
66
|
+
puts "Rollout: #{rollout}"
|
67
|
+
hash_user_context <= rollout
|
68
|
+
end
|
69
|
+
|
70
|
+
def hash_user_context
|
71
|
+
(Digest::MD5.hexdigest(user_context).to_i(base=16) % 100) / 100.0
|
72
|
+
end
|
73
|
+
end
|
data/lib/tailslide/version.rb
CHANGED
data/lib/tailslide.rb
CHANGED
data/redis_test.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'redistimeseries'
|
2
|
+
using Redistimeseries::RedisRefinement
|
3
|
+
|
4
|
+
app_id = 1
|
5
|
+
flag_id = 1
|
6
|
+
status = 'success'
|
7
|
+
redis_client = Redis.new(host:'localhost', port: 6379)
|
8
|
+
redis_client.ts_add(key: "#{flag_id}:#{status}", timestamp:"*", value:1, labels:["status", status, "appId", app_id, "flagId", flag_id])
|
data/tailslide-0.1.0.gem
ADDED
Binary file
|
data/test.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require "async"
|
2
|
+
# require "./lib/tailslide/nats_client.rb"
|
3
|
+
require_relative "lib/tailslide/flag_manager.rb"
|
4
|
+
require_relative 'lib/tailslide/toggler'
|
5
|
+
|
6
|
+
# def logMessage(message)
|
7
|
+
# p message
|
8
|
+
# end
|
9
|
+
|
10
|
+
# config = {server_url: "localhost:4222", callback: :p, token: 'myToken', stream:"flags", subject:'1'}
|
11
|
+
app_id = "1"
|
12
|
+
flag_name = 'Flag in app 1 number 1'
|
13
|
+
flag_config = {"flag_name": flag_name}
|
14
|
+
|
15
|
+
config = {nats_server:'localhost:4222', stream:'flags', app_id:app_id, sdk_key:'myToken', user_context:'375d39e6-9c3f-4f58-80bd-e5960b710295',
|
16
|
+
redis_host:'localhost', redis_port:6379}
|
17
|
+
|
18
|
+
|
19
|
+
Async do |task|
|
20
|
+
manager = FlagManger.new(**config)
|
21
|
+
manager.initialize_flags
|
22
|
+
flag_toggler = manager.new_toggler(flag_config)
|
23
|
+
|
24
|
+
|
25
|
+
if flag_toggler.is_flag_active
|
26
|
+
puts "Flag in #{app_id} with name \"#{flag_name}\" is active!"
|
27
|
+
flag_toggler.emit_success()
|
28
|
+
else
|
29
|
+
puts "Flag in #{app_id} with name \"#{flag_name}\" is not active!"
|
30
|
+
flag_toggler.emit_failure()
|
31
|
+
end
|
32
|
+
sleep 5
|
33
|
+
|
34
|
+
if flag_toggler.is_flag_active
|
35
|
+
puts "Flag in #{app_id} with name \"#{flag_name}\" is active!"
|
36
|
+
flag_toggler.emit_success()
|
37
|
+
else
|
38
|
+
puts "Flag in #{app_id} with name \"#{flag_name}\" is not active!"
|
39
|
+
flag_toggler.emit_failure()
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# require "nats/client"
|
45
|
+
# require "async"
|
46
|
+
# TimeoutError = NATS::IO::Timeout
|
47
|
+
# require 'json'
|
48
|
+
|
49
|
+
# token = "myToken"
|
50
|
+
|
51
|
+
# nats_client = NATS.connect("nats://#{token}@127.0.0.1:4222")
|
52
|
+
# jet_stream = nats_client.jetstream
|
53
|
+
|
54
|
+
# # get last message
|
55
|
+
# latest_msg = jet_stream.get_last_msg("flags", "test")
|
56
|
+
# json_data = JSON.parse latest_msg.data
|
57
|
+
# p json_data
|
58
|
+
|
59
|
+
|
60
|
+
# # pull subscribe for new onging messages (workaround until Nats.rb make new update)
|
61
|
+
# subscribed_stream = jet_stream.pull_subscribe("test", 'mydurable', config: { deliver_policy: 'new' })
|
62
|
+
# Async do |task|
|
63
|
+
# task.async do
|
64
|
+
# begin
|
65
|
+
# messages = subscribed_stream.fetch(1)
|
66
|
+
# messages.each do |message|
|
67
|
+
# message.ack
|
68
|
+
# json_data = JSON.parse message.data
|
69
|
+
# p json_data
|
70
|
+
# end
|
71
|
+
# rescue NATS::Timeout => e
|
72
|
+
# p e
|
73
|
+
# end until nats_client.closed?
|
74
|
+
# end
|
75
|
+
# p "hello past async"
|
76
|
+
# end
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
# push subscribe for new ongoing messages
|
81
|
+
# setting deliver_policy still results in delivering all messages in "test" subject
|
82
|
+
# push_sub = jetStream.subscribe("test", {manual_ack: true, deliver_policy:"new"} ) do |msg|
|
83
|
+
# msg.ack
|
84
|
+
# puts msg.data
|
85
|
+
# end
|
86
|
+
|
87
|
+
# Get ongoing messages
|
88
|
+
# Push subscribe
|
89
|
+
# consumer_req = {
|
90
|
+
# stream_name: "test",
|
91
|
+
# config: {
|
92
|
+
# durable_name: "sample",
|
93
|
+
# deliver_policy: "new",
|
94
|
+
# ack_policy: "explicit",
|
95
|
+
# max_deliver: -1,
|
96
|
+
# replay_policy: "instant"
|
97
|
+
# }
|
98
|
+
# }
|
99
|
+
|
100
|
+
# config = NATS::JetStream::API::ConsumerConfig.new({ deliver_policy: "new" })
|
101
|
+
# Create inbox for push consumer.
|
102
|
+
# deliver = natsClient.new_inbox
|
103
|
+
# config.deliver_subject = deliver
|
104
|
+
|
105
|
+
# push_sub = jetStream.subscribe("test", {manual_ack: true, deliver_policy:"new"} ) do |msg|
|
106
|
+
# puts msg.data
|
107
|
+
# end
|
108
|
+
|
109
|
+
# cinfo = push_sub.consumer_info["config"]
|
110
|
+
# puts cinfo
|
111
|
+
# msg = push_sub.next_msg(timeout: 10000000000)
|
112
|
+
# msg.ack
|
113
|
+
# puts msg.data
|
114
|
+
# push_sub.consumer_info["config"]["deliver_policy"] = "new"
|
115
|
+
# puts cinfo
|
116
|
+
|
117
|
+
# loop do
|
118
|
+
# msgs = push_sub.next_msg()
|
119
|
+
# puts msgs.data
|
120
|
+
# rescue TimeoutError => e
|
121
|
+
# puts e
|
122
|
+
# sleep 1
|
123
|
+
# end
|
124
|
+
|
125
|
+
# js.publish("9", "Hello JetStream! 1")
|
126
|
+
# js.publish("9", "Hello JetStream! 2")
|
127
|
+
# js.publish("9", "Hello JetStream! 3")
|
128
|
+
# js.publish("9", "Hello JetStream! 4")
|
129
|
+
# js.publish("9", "Hello JetStream! Latest")
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tailslide
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steven Liou
|
@@ -11,8 +11,22 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2022-07-
|
15
|
-
dependencies:
|
14
|
+
date: 2022-07-24 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: "[]"
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
16
30
|
description: " Write a longer description or delete this line."
|
17
31
|
email:
|
18
32
|
- stevenliou@gmail.com
|
@@ -23,7 +37,8 @@ executables: []
|
|
23
37
|
extensions: []
|
24
38
|
extra_rdoc_files: []
|
25
39
|
files:
|
26
|
-
- ".
|
40
|
+
- ".vscode/launch.json"
|
41
|
+
- ".vscode/settings.json"
|
27
42
|
- CHANGELOG.md
|
28
43
|
- CODE_OF_CONDUCT.md
|
29
44
|
- Gemfile
|
@@ -31,8 +46,15 @@ files:
|
|
31
46
|
- README.md
|
32
47
|
- Rakefile
|
33
48
|
- lib/tailslide.rb
|
49
|
+
- lib/tailslide/flag_manager.rb
|
50
|
+
- lib/tailslide/nats_client.rb
|
51
|
+
- lib/tailslide/redis_timeseries_client.rb
|
52
|
+
- lib/tailslide/toggler.rb
|
34
53
|
- lib/tailslide/version.rb
|
54
|
+
- redis_test.rb
|
35
55
|
- sig/tailslide.rbs
|
56
|
+
- tailslide-0.1.0.gem
|
57
|
+
- test.rb
|
36
58
|
homepage: https://github.com/tailslide-io/tailslide.rb
|
37
59
|
licenses:
|
38
60
|
- MIT
|
@@ -58,5 +80,6 @@ requirements: []
|
|
58
80
|
rubygems_version: 3.3.7
|
59
81
|
signing_key:
|
60
82
|
specification_version: 4
|
61
|
-
summary:
|
83
|
+
summary: This is the Ruby SDK for Tailslide, which is a feature flag framework with
|
84
|
+
automatic fail safe and circuit recovery.
|
62
85
|
test_files: []
|
data/.rubocop.yml
DELETED