Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Toggleable for palanca #14

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
85d6297
init for palanca
budipang Jul 18, 2018
9f3378c
update version
budipang Jul 18, 2018
a110df2
bug
budipang Jul 18, 2018
a68de33
rubocop
budipang Jul 19, 2018
6e9ed62
remove unused callback
budipang Jul 19, 2018
970c153
add spec
budipang Jul 20, 2018
cd3856f
optimize things
budipang Jul 20, 2018
2de4205
typo
budipang Jul 20, 2018
3af3372
optimize things
budipang Jul 22, 2018
0f82be2
add timeout handler for get key
budipang Jul 26, 2018
35df283
raise error when cannot connect redis and sort avail features
budipang Jul 31, 2018
8a49bf4
add notify to notify endpoint
budipang Aug 28, 2018
a33f51b
version
budipang Aug 28, 2018
09b9f64
bump rest-client version
budipang Aug 28, 2018
9e3a95c
bump rest-client version
budipang Aug 28, 2018
572937d
add actor to endpoint
budipang Aug 29, 2018
ce53020
add blacklist notif key for spammed toggle keys
budipang Aug 29, 2018
9c54284
missed line
budipang Aug 29, 2018
74da1ec
missed line
budipang Aug 29, 2018
6632a4c
missed line
budipang Aug 29, 2018
9bc1366
adjust request to post
budipang Aug 29, 2018
d58146c
improve notify
budipang Sep 13, 2018
5205ff2
rubocop
budipang Sep 13, 2018
9891b7b
stupid mistake
budipang Sep 17, 2018
8ea9d7a
fix bug
budipang Sep 19, 2018
2f53e9d
prevent fe bug
budipang Oct 12, 2018
53d94b6
bump version
budipang Oct 12, 2018
450d8e5
fix bug
budipang Oct 12, 2018
e0289e6
fix bug
budipang Oct 12, 2018
d2fb19c
cleanup each test
budipang Oct 12, 2018
391bd22
add instrumentor
budipang Dec 17, 2018
18f842b
require instumrenotr
budipang Dec 17, 2018
e416fb4
fix instrument
budipang Dec 17, 2018
7345631
missed
budipang Dec 17, 2018
b569cbf
transform values
budipang Mar 26, 2019
3b9c6e5
add user param
budipang Apr 25, 2019
eabb9c2
rubocop
budipang Apr 25, 2019
01f9013
add write key
budipang Jun 9, 2019
5fccc13
bump version
budipang Jun 9, 2019
694d2fe
mis pry;
budipang Jun 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: ruby:2.3.0
- image: ruby:2.5.3
- image: redis:3.2-alpine
working_directory: ~/toggleable

Expand Down
32 changes: 27 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
PATH
remote: .
specs:
toggleable (0.1.5)
toggleable (0.2.3)
activesupport (>= 4.0.0)
rest-client

GEM
remote: https://rubygems.org/
specs:
activesupport (5.2.0)
activesupport (5.2.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
Expand All @@ -17,22 +18,39 @@ GEM
json
simplecov
url
concurrent-ruby (1.0.5)
coderay (1.1.2)
concurrent-ruby (1.1.5)
diff-lcs (1.3)
docile (1.3.1)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.4.0)
i18n (1.0.1)
http-cookie (1.0.3)
domain_name (~> 0.5)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.1)
json (2.1.0)
method_source (0.9.0)
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0331)
minitest (5.11.3)
netrc (0.11.0)
parallel (1.12.1)
parser (2.5.1.0)
ast (~> 2.4.0)
powerpack (0.1.2)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
rainbow (3.0.0)
rake (10.5.0)
redis (3.3.5)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
Expand Down Expand Up @@ -65,6 +83,9 @@ GEM
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.4.0)
url (0.3.2)

Expand All @@ -75,6 +96,7 @@ DEPENDENCIES
bundler (~> 1.14)
codecov
dotenv (>= 2.4.0)
pry
rake (~> 10.0)
redis (~> 3.0)
rspec (~> 3.0)
Expand All @@ -84,4 +106,4 @@ DEPENDENCIES
toggleable!

BUNDLED WITH
1.16.1
1.17.2
3 changes: 2 additions & 1 deletion lib/toggleable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'toggleable/version'
require 'toggleable/configuration'
require 'toggleable/storage'
require 'toggleable/instrumentor_abstract'
require 'toggleable/logger_abstract'
require 'toggleable/feature_toggler'
require 'toggleable/base'
Expand All @@ -24,7 +25,7 @@ def configure

# set default configuration for storage and namespace if none was provided
configuration.storage ||= Toggleable::MemoryStore.new
configuration.namespace ||= 'toggleable'
configuration.namespace ||= 'toggleable_rspec'
configuration.expiration_time ||= 5.minutes
end
end
34 changes: 26 additions & 8 deletions lib/toggleable/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'active_support/concern'
require 'active_support/inflector'
require 'active_support/core_ext/numeric/time'
require 'rest-client'

module Toggleable
# Toggleable::Base provides basic functionality for toggling into a class.
Expand All @@ -17,8 +18,9 @@ module Base

# it will generate these methods into included class.
module ClassMethods
def active?
return to_bool(toggle_active.to_s) unless toggle_active.nil?
def active?(_user = nil)
toggle_status = toggle_active
return toggle_status.to_s == 'true' unless toggle_status.nil?

# Lazily register the key
Toggleable.configuration.storage.set_if_not_exist(key, DEFAULT_VALUE, namespace: Toggleable.configuration.namespace)
Expand Down Expand Up @@ -50,24 +52,40 @@ def process

def toggle_key(value, actor)
Toggleable.configuration.logger&.log(key: key, value: value, actor: actor)

start_time = Time.now
Toggleable.configuration.storage.set(key, value, namespace: Toggleable.configuration.namespace)
duration = (Time.now - start_time)
Toggleable.configuration.instrumentor&.latency(duration, 'redis_set', 'ok')

notify_changes({ key => value.to_s }, actor) if Toggleable.configuration.notify_host && !Toggleable.configuration.blacklisted_notif_key&.include?(key)
end

def notify_changes(mapping, actor)
url = "#{Toggleable.configuration.notify_host}/_internal/toggle-features/bulk-notify"
payload = { mappings: mapping, user_id: actor.to_s }.to_json
RestClient::Resource.new(url).post payload, timeout: 2, open_timeout: 1
rescue StandardError
nil
end

def toggle_active
return @_toggle_active if defined?(@_toggle_active) && !read_expired? && Toggleable.configuration.use_memoization
@_last_read_at = Time.now.localtime

start_time = Time.now
@_toggle_active = Toggleable.configuration.storage.get(key, namespace: Toggleable.configuration.namespace)
duration = (Time.now - start_time)
Toggleable.configuration.instrumentor&.latency(duration, 'redis_get', 'ok')

@_toggle_active
rescue StandardError => e
raise e
end

def read_expired?
@_last_read_at < Time.now.localtime - Toggleable.configuration.expiration_time
end

def to_bool(value)
return true if value =~ /^(true|t|yes|y|1)$/i
return false if value.empty? || value =~ /^(false|f|no|n|0)$/i
raise ArgumentError, "invalid value for Boolean: \"#{value}\""
end
end
end
end
3 changes: 3 additions & 0 deletions lib/toggleable/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class Configuration
attr_accessor :storage ## storage used. default: memory store
attr_accessor :namespace ## required for prefixing the keys. default: `toggleable``
attr_accessor :logger ## optional, it will not log if not configured.
attr_accessor :instrumentor ## optional, to instrument metrics. It will not instrument if not configured
attr_accessor :use_memoization ## set true to use memoization. default: false
attr_accessor :notify_host ## optional for notify changes on telegram
attr_accessor :blacklisted_notif_key ## optional for blacklisting keys that won't be broadcasted
end
end
98 changes: 90 additions & 8 deletions lib/toggleable/feature_toggler.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# frozen_string_literal: true
# frozen_string_literal: false

require 'singleton'
require 'rest-client'

module Toggleable
# Toggleable::FeatureToggler provides an instance to manage all toggleable keys.
class FeatureToggler
include Singleton

DEFAULT_VALUE = false

attr_reader :features

def initialize
Expand All @@ -17,38 +20,117 @@ def register(key)
features << key
end

def get_key(key)
@_toggle_active ||= {}
@_last_key_read_at ||= {}
toggle_status = toggle_active(key)
return toggle_status unless toggle_status.nil?

# Lazily register the key
Toggleable.configuration.storage.set_if_not_exist(key, DEFAULT_VALUE, namespace: Toggleable.configuration.namespace)
DEFAULT_VALUE
end

def create_key(key, value, actor)
success = Toggleable.configuration.storage.set_if_not_exist(key, value, namespace: Toggleable.configuration.namespace)
return unless success

Toggleable.configuration.logger&.log(key: key, value: value, actor: actor)
notify_changes({ key => value.to_s }, actor) if Toggleable.configuration.notify_host

success
end

def toggle_key(key, value, actor)
prev = Toggleable.configuration.storage.get(key, namespace: Toggleable.configuration.namespace)

Toggleable.configuration.logger&.log(key: key, value: value, actor: actor)
Toggleable.configuration.storage.set(key, value, namespace: Toggleable.configuration.namespace)
notify_changes({ key => value.to_s }, actor) if should_notify?(key, prev, value)
end

def available_features(memoize: Toggleable.configuration.use_memoization)
available_features = memoize ? memoized_keys : keys
available_features.slice(*features)
available_features.sort_by{ |k, _v| k }.to_h
end

def mass_toggle!(mapping, actor: nil)
log_changes(mapping, actor) if Toggleable.configuration.logger

return if mapping.empty?

start_time = Time.now
Toggleable.configuration.storage.mass_set(mapping, namespace: Toggleable.configuration.namespace)
duration = (Time.now - start_time)
Toggleable.configuration.instrumentor&.latency(duration, 'redis_mass_set', 'ok')

mapping.transform_values!(&:to_s)
notify_changes(mapping, actor) if Toggleable.configuration.notify_host
end

private

def keys
Toggleable.configuration.storage.get_all(namespace: Toggleable.configuration.namespace)
start_time = Time.now
keys = Toggleable.configuration.storage.get_all(namespace: Toggleable.configuration.namespace)
duration = (Time.now - start_time)
Toggleable.configuration.instrumentor&.latency(duration, 'redis_mass_get', 'ok')

keys
end

def toggle_active(key)
return @_toggle_active[key] if Toggleable.configuration.use_memoization && @_toggle_active.key?(key) && !read_key_expired?(key)

@_last_key_read_at[key] = Time.now.localtime

start_time = Time.now
@_toggle_active[key] = Toggleable.configuration.storage.get(key, namespace: Toggleable.configuration.namespace)
duration = (Time.now - start_time)
Toggleable.configuration.instrumentor&.latency(duration, 'redis_get', 'ok')

@_toggle_active[key]
rescue StandardError
false
end

def memoized_keys
return @_memoized_keys if defined?(@_memoized_keys) && !read_expired?
return @_memoized_keys if defined?(@_memoized_keys) && !read_all_keys_expired?
@_last_read_at = Time.now.localtime

start_time = Time.now
@_memoized_keys = Toggleable.configuration.storage.get_all(namespace: Toggleable.configuration.namespace)
duration = (Time.now - start_time)
Toggleable.configuration.instrumentor&.latency(duration, 'redis_mass_get', 'ok')

@_memoized_keys
end

def read_expired?
def read_all_keys_expired?
@_last_read_at < Time.now.localtime - Toggleable.configuration.expiration_time
end

def read_key_expired?(key)
@_last_key_read_at[key] < Time.now.localtime - Toggleable.configuration.expiration_time
end

def log_changes(mapping, actor)
previous_values = available_features
previous_mapping = available_features(memoize: false)
mapping.each do |key, val|
next if previous_values[key].to_s == val.to_s
Toggleable.configuration.logger.log(key: key, value: val, actor: actor)
previous_mapping[key] != val.to_s ? Toggleable.configuration.logger.log(key: key, value: val, actor: actor) : mapping.delete(key)
end
end

def should_notify?(key, prev, value)
Toggleable.configuration.notify_host && !Toggleable.configuration.blacklisted_notif_key&.include?(key) && (prev != value.to_s)
end

def notify_changes(mapping, actor)
url = "#{Toggleable.configuration.notify_host}/_internal/toggle-features/bulk-notify"
payload = { mappings: mapping, user_id: actor.to_s }.to_json
RestClient::Resource.new(url).post payload, timeout: 2, open_timeout: 1
rescue StandardError
nil
end
end
end
18 changes: 18 additions & 0 deletions lib/toggleable/instrumentor_abstract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Toggleable
# Toggleable::InstrumentorAbstract describes the interface class for instrumentor.
class InstrumentorAbstract
## the instrumentor you provide must implement these methods

def latency(_duration, _action, _status)
raise NotImplementedError, "You must implement #{method_name}"
end

private

def method_name
caller_locations(1, 1)[0].label
end
end
end
5 changes: 2 additions & 3 deletions lib/toggleable/storage/memory_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ def set(key, value, namespace:)
end

def set_if_not_exist(key, value, namespace:)
fetch(key, namespace: namespace) do
value
end
exist = read(key, namespace: namespace)
write(key, value, namespace: namespace) if exist.nil?
end

def mass_set(mappings, namespace:)
Expand Down
2 changes: 1 addition & 1 deletion lib/toggleable/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Toggleable
VERSION = '0.1.6'
VERSION = '0.2.4'
end
1 change: 1 addition & 0 deletions spec/class_initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ def log(key:, value:, actor:)

Toggleable.configure do |t|
t.logger = logger
t.notify_host = 'http://localhost:5858'
end
Loading