feature_switches 0.1.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.
- checksums.yaml +7 -0
- data/README.md +122 -0
- data/lib/switches/backend.rb +17 -0
- data/lib/switches/backends/memory.rb +48 -0
- data/lib/switches/backends/postgres.rb +100 -0
- data/lib/switches/backends/redis.rb +74 -0
- data/lib/switches/cohort.rb +63 -0
- data/lib/switches/cohorts.rb +14 -0
- data/lib/switches/configuration.rb +9 -0
- data/lib/switches/feature.rb +89 -0
- data/lib/switches/features.rb +14 -0
- data/lib/switches/instance.rb +86 -0
- data/lib/switches/percentage.rb +58 -0
- data/lib/switches/update.rb +23 -0
- data/lib/switches.rb +29 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d9320af56d6df754066bdf4db823d5c5664177df
|
4
|
+
data.tar.gz: 25c88dab2729ecbe5e7e3f65a93d9ecdda97ad22
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 00215b33937508434dca51ab4266de7ee4b54a689695e8ab4a3bacb5ce87dc02f8732f460e279287b92f2d45a74fb5d93a451818f016a205f25f7582af48311d
|
7
|
+
data.tar.gz: cb7f70b4d6a3778dc18db84c2f737ebbc1cf4aaa69cdf6fa2c5efefb4e81db5ff6891b9d70415bb52238254696247ad16ba405ec776776df55d56868782a9310
|
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# Feature Switches
|
2
|
+
|
3
|
+
### Status: Alpha (Don't use in production)
|
4
|
+
|
5
|
+
## Summary
|
6
|
+
|
7
|
+
A small gem for putting feature switches into your application that allow you
|
8
|
+
to dynamically enable features to a percentage of users or specific cohort
|
9
|
+
groups without a code deploy.
|
10
|
+
|
11
|
+
There are some excellent gems that provide this functionality already --
|
12
|
+
[rollout](https://github.com/jamesgolick/rollout) and [flipper](https://github.com/jnunemaker/flipper).
|
13
|
+
This project is an experiment aiming for a specific set of design goals:
|
14
|
+
|
15
|
+
1) Reduce the chatter in the protocol between a node checking to see if a
|
16
|
+
feature is enabled and the backend storage system. We're going to be reading
|
17
|
+
this data far more often than we're writing it so we want to aggressively
|
18
|
+
cache, but...
|
19
|
+
|
20
|
+
2) Ensure all nodes get the latest configured switches immediately. A cache
|
21
|
+
that expires given a certain TTL can't work as a client isn't guaranteed
|
22
|
+
to talking to the same instance of our application on each request. A
|
23
|
+
feature disappearing and reappearing depending on which application server
|
24
|
+
we instance we hit is a bug.
|
25
|
+
|
26
|
+
3) Allow for extension with new backends that support change notification;
|
27
|
+
specifically distributed systems synchronization backends like Zookeeper and
|
28
|
+
perhaps doozerd. Right now it's initially implemented against Redis which is
|
29
|
+
the simplest thing that could possibly work.
|
30
|
+
|
31
|
+
5) Ensure that any kind of identifier can be used; not just an object that
|
32
|
+
responds to #id. We want to peg our switches on things that aren't
|
33
|
+
ActiveRecord objects (e.g., incoming phone numbers, time of day, etc).
|
34
|
+
|
35
|
+
4) Expose a highly memorable CLI as that's how we're going to be interacting
|
36
|
+
with the switches via `irb`.
|
37
|
+
|
38
|
+
|
39
|
+
## Supported Backends
|
40
|
+
|
41
|
+
* Redis
|
42
|
+
* Postgres
|
43
|
+
* In-memory (for testing)
|
44
|
+
|
45
|
+
## Design
|
46
|
+
|
47
|
+
Switches uses a backend for both storage of feature configuration data and for
|
48
|
+
notifying sibling nodes that are change has occurred. We'll look at how this
|
49
|
+
works against the Redis backend.
|
50
|
+
|
51
|
+
On startup, switches will connect to Redis twice: once for querying and setting
|
52
|
+
configuration data and one for subscribing to a pub/sub channel of change
|
53
|
+
notifications. When a change is made to configuration data, an extra call is
|
54
|
+
made to Redis to publish a change notification. Once this change notification is
|
55
|
+
received by other listening nodes, they will refetch the configuration data
|
56
|
+
and update their local stores.
|
57
|
+
|
58
|
+
|
59
|
+
Node A Redis Node B
|
60
|
+
| | |
|
61
|
+
| |<-subscribe----|
|
62
|
+
| | |
|
63
|
+
|------------set->| |
|
64
|
+
| | |
|
65
|
+
|-publish(update)>|-------update->|
|
66
|
+
| | |
|
67
|
+
| |<-get----------|
|
68
|
+
| | |
|
69
|
+
| | |
|
70
|
+
|
71
|
+
## Installation
|
72
|
+
|
73
|
+
In your Gemfile:
|
74
|
+
|
75
|
+
gem "feature_switches"
|
76
|
+
|
77
|
+
## Usage
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# Initialize
|
81
|
+
$switches = Switches do |config|
|
82
|
+
config.backend = "redis://localhost:6379/0"
|
83
|
+
end
|
84
|
+
=> #<Switches redis://localhost:6379/12>
|
85
|
+
|
86
|
+
# Check to see if a feature is active for an identifier
|
87
|
+
$switches.feature(:redesign).on?(current_user.id)
|
88
|
+
# => true
|
89
|
+
|
90
|
+
$switches.feature(:redesign).on?(current_user.phone_number)
|
91
|
+
# => true
|
92
|
+
|
93
|
+
$switches.feature(:redesign).on?(Time.now.hour)
|
94
|
+
# => true
|
95
|
+
|
96
|
+
# Turn a feature on globally
|
97
|
+
$switches.feature(:redesign).on
|
98
|
+
# => #<Feature redesign; 100%>
|
99
|
+
|
100
|
+
# Turn a feature on for a given percentage of identifiers
|
101
|
+
$switches.feature(:redesign).on(25)
|
102
|
+
# => #<Feature redesign; 25%>
|
103
|
+
|
104
|
+
# Turn a feature off globally
|
105
|
+
$switches.feature(:redesign).off
|
106
|
+
# => #<Feature redesign; 0%>
|
107
|
+
|
108
|
+
# Add or remove an identifier from a cohort group
|
109
|
+
$switches.cohort(:power_users).add(424)
|
110
|
+
# => #<Cohort power_users; 1 member>
|
111
|
+
|
112
|
+
$switches.cohort(:power_users).remove(424)
|
113
|
+
# => #<Cohort power_users; 0 members>
|
114
|
+
|
115
|
+
# Add a cohort group to a feature
|
116
|
+
$switches.feature(:redesign).add(:power_users)
|
117
|
+
# => #<Feature djsd; 0%; power_users>
|
118
|
+
|
119
|
+
# Remove a cohort group from a feature
|
120
|
+
$switches.feature(:redesign).remove(:power_users)
|
121
|
+
# => #<Feature djsd; 0%>
|
122
|
+
```
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Switches
|
4
|
+
class Backend
|
5
|
+
def self.factory(url, instance)
|
6
|
+
uri = URI(url)
|
7
|
+
|
8
|
+
if uri.scheme == "redis"
|
9
|
+
Backends::Redis.new(uri, instance)
|
10
|
+
elsif uri.scheme == "postgres"
|
11
|
+
Backends::Postgres.new(uri, instance)
|
12
|
+
else
|
13
|
+
Backends::Memory.new(uri, instance)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Switches
|
2
|
+
module Backends
|
3
|
+
class Memory
|
4
|
+
def initialize(uri, instance)
|
5
|
+
@instance = instance
|
6
|
+
$switches_data ||= {}
|
7
|
+
$switches_listeners ||= Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def set(item)
|
11
|
+
$switches_data[key_for(item)] = item.to_json
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(item)
|
15
|
+
if data = $switches_data[key_for(item)]
|
16
|
+
parse(data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def listen
|
21
|
+
$switches_listeners.add(@instance)
|
22
|
+
end
|
23
|
+
|
24
|
+
def notify(update)
|
25
|
+
$switches_listeners.each do |listener|
|
26
|
+
listener.notified(update)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear
|
31
|
+
$switches_data.clear
|
32
|
+
$switches_listeners.clear
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def key_for(item)
|
38
|
+
[item.class.to_s.downcase, item.name].join(":")
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse(json)
|
42
|
+
JSON.parse(json.to_s)
|
43
|
+
rescue JSON::ParserError
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "pg"
|
2
|
+
|
3
|
+
module Switches
|
4
|
+
module Backends
|
5
|
+
class Postgres
|
6
|
+
TABLE = "switches"
|
7
|
+
|
8
|
+
def initialize(uri, instance)
|
9
|
+
@uri = uri
|
10
|
+
@instance = instance
|
11
|
+
end
|
12
|
+
|
13
|
+
def set(item)
|
14
|
+
result = connection.exec("UPDATE #{TABLE} SET value = $1 WHERE key = $2",
|
15
|
+
[item.to_json, key_for(item)]
|
16
|
+
)
|
17
|
+
|
18
|
+
if result.cmd_tuples == 0
|
19
|
+
connection.exec("INSERT INTO #{TABLE} (key, value) values($1, $2)",
|
20
|
+
[key_for(item), item.to_json]
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(item)
|
26
|
+
result = connection.exec("SELECT value FROM #{TABLE} WHERE key = $1 LIMIT 1",
|
27
|
+
[key_for(item)]
|
28
|
+
)
|
29
|
+
|
30
|
+
result.each do |row|
|
31
|
+
return parse(row["value"])
|
32
|
+
end
|
33
|
+
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def listen
|
38
|
+
Thread.new { subscribe }
|
39
|
+
end
|
40
|
+
|
41
|
+
def notify(update)
|
42
|
+
connection.exec("NOTIFY #{TABLE}, '#{update.to_json}'")
|
43
|
+
end
|
44
|
+
|
45
|
+
def clear
|
46
|
+
connection.exec("TRUNCATE TABLE #{TABLE}")
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def listener
|
52
|
+
@listener ||= connect
|
53
|
+
end
|
54
|
+
|
55
|
+
def connection
|
56
|
+
@connection ||= connect
|
57
|
+
end
|
58
|
+
|
59
|
+
def connect
|
60
|
+
PG.connect(connection_options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def connection_options
|
64
|
+
{
|
65
|
+
user: @uri.user,
|
66
|
+
password: @uri.password,
|
67
|
+
host: @uri.host,
|
68
|
+
port: @uri.port,
|
69
|
+
dbname: @uri.path[1..-1]
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def subscribe
|
74
|
+
loop do
|
75
|
+
listener.exec("LISTEN #{TABLE}")
|
76
|
+
listener.wait_for_notify do |event, pid, message|
|
77
|
+
process(message)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def key_for(item)
|
83
|
+
[item.type, item.name].join(":")
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse(json)
|
87
|
+
JSON.parse(json.to_s)
|
88
|
+
rescue JSON::ParserError
|
89
|
+
{}
|
90
|
+
end
|
91
|
+
|
92
|
+
def process(message)
|
93
|
+
attributes = parse(message)
|
94
|
+
update = Update.new(attributes)
|
95
|
+
|
96
|
+
@instance.notified(update)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "redis"
|
2
|
+
|
3
|
+
module Switches
|
4
|
+
module Backends
|
5
|
+
class Redis
|
6
|
+
PREFIX = "switches"
|
7
|
+
CHANNEL = [PREFIX, "bus"].join(":")
|
8
|
+
|
9
|
+
def initialize(uri, instance)
|
10
|
+
@uri = uri
|
11
|
+
@instance = instance
|
12
|
+
end
|
13
|
+
|
14
|
+
def set(item)
|
15
|
+
connection.set(key_for(item), item.to_json)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(item)
|
19
|
+
if data = connection.get(key_for(item))
|
20
|
+
parse(data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def listen
|
25
|
+
Thread.new { subscribe }
|
26
|
+
end
|
27
|
+
|
28
|
+
def notify(update)
|
29
|
+
connection.publish(CHANNEL, update.to_json)
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
connection.flushdb
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def listener
|
39
|
+
@listener ||= connect
|
40
|
+
end
|
41
|
+
|
42
|
+
def connection
|
43
|
+
@connection ||= connect
|
44
|
+
end
|
45
|
+
|
46
|
+
def connect
|
47
|
+
::Redis.new(url: @uri)
|
48
|
+
end
|
49
|
+
|
50
|
+
def subscribe
|
51
|
+
listener.subscribe(CHANNEL) do |on|
|
52
|
+
on.message { |_, message| process(message) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def key_for(item)
|
57
|
+
[item.type, item.name].join(":")
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse(json)
|
61
|
+
JSON.parse(json.to_s)
|
62
|
+
rescue JSON::ParserError
|
63
|
+
{}
|
64
|
+
end
|
65
|
+
|
66
|
+
def process(message)
|
67
|
+
attributes = parse(message)
|
68
|
+
update = Update.new(attributes)
|
69
|
+
|
70
|
+
@instance.notified(update)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Switches
|
2
|
+
class Cohort
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, instance)
|
6
|
+
@name = name
|
7
|
+
@instance = instance
|
8
|
+
@members = Set.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def reload
|
12
|
+
if data = @instance.get(self)
|
13
|
+
@members = data["members"].to_set
|
14
|
+
end
|
15
|
+
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def include?(identifier)
|
20
|
+
@members.include?(identifier)
|
21
|
+
end
|
22
|
+
|
23
|
+
def add(member)
|
24
|
+
@members.add(member.to_s)
|
25
|
+
updated
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove(member)
|
29
|
+
@members.delete(member.to_s)
|
30
|
+
updated
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
output = "#<Cohort #{@name}"
|
35
|
+
output += "; #{@members.count} member"
|
36
|
+
output += "s" unless @members.count == 1
|
37
|
+
output += ">"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_json
|
41
|
+
{
|
42
|
+
name: name,
|
43
|
+
members: members
|
44
|
+
}.to_json
|
45
|
+
end
|
46
|
+
|
47
|
+
def members
|
48
|
+
@members.to_a
|
49
|
+
end
|
50
|
+
|
51
|
+
def type
|
52
|
+
"cohort"
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def updated
|
58
|
+
@instance.set(self)
|
59
|
+
@instance.notify(self)
|
60
|
+
self
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Switches
|
2
|
+
class Feature
|
3
|
+
attr_reader :name, :percentage
|
4
|
+
|
5
|
+
def initialize(name, instance)
|
6
|
+
@name = name
|
7
|
+
@instance = instance
|
8
|
+
@percentage = Percentage(0)
|
9
|
+
@cohorts = Set.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def reload
|
13
|
+
if data = @instance.get(self)
|
14
|
+
@percentage = Percentage(data["percentage"])
|
15
|
+
@cohorts = data["cohorts"].to_set
|
16
|
+
end
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def on(numeric = 100)
|
22
|
+
@percentage = Percentage(numeric)
|
23
|
+
updated
|
24
|
+
end
|
25
|
+
|
26
|
+
def off
|
27
|
+
@percentage = Percentage(0)
|
28
|
+
updated
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(cohort_name)
|
32
|
+
@cohorts.add(cohort_name.to_s)
|
33
|
+
updated
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove(cohort_name)
|
37
|
+
@cohorts.delete(cohort_name.to_s)
|
38
|
+
updated
|
39
|
+
end
|
40
|
+
|
41
|
+
def inspect
|
42
|
+
output = "#<Feature #{@name}; #{@percentage}"
|
43
|
+
output += "; #{@cohorts.to_a.join(", ")}" if @cohorts.any?
|
44
|
+
output += ">"
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_json
|
48
|
+
{
|
49
|
+
name: name,
|
50
|
+
percentage: percentage.to_i,
|
51
|
+
cohorts: @cohorts.to_a
|
52
|
+
}.to_json
|
53
|
+
end
|
54
|
+
|
55
|
+
def cohorts
|
56
|
+
@cohorts.to_a
|
57
|
+
end
|
58
|
+
|
59
|
+
def on?(identifier)
|
60
|
+
return true if @percentage.max?
|
61
|
+
|
62
|
+
in_cohort?(identifier) || in_percentage?(identifier)
|
63
|
+
end
|
64
|
+
|
65
|
+
def type
|
66
|
+
"feature"
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def in_cohort?(identifier)
|
72
|
+
@cohorts.any? do |cohort|
|
73
|
+
@instance.cohort(cohort).include?(identifier)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def in_percentage?(identifier)
|
78
|
+
return false if @percentage.min?
|
79
|
+
|
80
|
+
@percentage.include?(identifier)
|
81
|
+
end
|
82
|
+
|
83
|
+
def updated
|
84
|
+
@instance.set(self)
|
85
|
+
@instance.notify(self)
|
86
|
+
self
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Switches
|
2
|
+
class Features
|
3
|
+
def initialize(instance)
|
4
|
+
@features ||= Hash.new do |features, name|
|
5
|
+
name = name.to_sym
|
6
|
+
features[name] = Feature.new(name, instance).reload
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](name)
|
11
|
+
@features[name.to_sym]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Switches
|
2
|
+
class Instance
|
3
|
+
include MonitorMixin
|
4
|
+
|
5
|
+
attr_reader :node_id
|
6
|
+
|
7
|
+
def initialize(configuration)
|
8
|
+
@url = configuration.backend
|
9
|
+
@node_id = SecureRandom.hex(3)
|
10
|
+
|
11
|
+
mon_initialize
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
backend.listen
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def feature(name)
|
20
|
+
synchronize do
|
21
|
+
features[name]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def cohort(name)
|
26
|
+
synchronize do
|
27
|
+
cohorts[name]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(item)
|
32
|
+
backend.get(item)
|
33
|
+
end
|
34
|
+
|
35
|
+
def set(item)
|
36
|
+
backend.set(item)
|
37
|
+
end
|
38
|
+
|
39
|
+
def notify(item)
|
40
|
+
Switches::Update.new.tap do |update|
|
41
|
+
update.type = item.type
|
42
|
+
update.name = item.name
|
43
|
+
update.node_id = node_id
|
44
|
+
|
45
|
+
backend.notify(update)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def notified(update)
|
50
|
+
return if update.from?(node_id)
|
51
|
+
|
52
|
+
case update.type
|
53
|
+
when "feature"
|
54
|
+
synchronize do
|
55
|
+
features[update.name].reload
|
56
|
+
end
|
57
|
+
when "cohort"
|
58
|
+
synchronize do
|
59
|
+
cohorts[update.name].reload
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def clear
|
65
|
+
backend.clear
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
"#<Switches #{@url}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def backend
|
75
|
+
@backend ||= Backend.factory(@url, self)
|
76
|
+
end
|
77
|
+
|
78
|
+
def features
|
79
|
+
@features ||= Features.new(self)
|
80
|
+
end
|
81
|
+
|
82
|
+
def cohorts
|
83
|
+
@cohorts ||= Cohorts.new(self)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Switches
|
2
|
+
class Percentage
|
3
|
+
LOWER = 0.0
|
4
|
+
UPPER = 100.0
|
5
|
+
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
attr_reader :value
|
9
|
+
protected :value
|
10
|
+
|
11
|
+
def initialize(value)
|
12
|
+
@value = clip(value)
|
13
|
+
end
|
14
|
+
|
15
|
+
def <=>(other)
|
16
|
+
@value <=> other.value
|
17
|
+
end
|
18
|
+
|
19
|
+
def include?(identifier)
|
20
|
+
Percentage(Zlib.crc32(identifier) % UPPER) < self
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_i
|
24
|
+
@value.to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_f
|
28
|
+
@value.to_f
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"#{@value}%"
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def min?
|
40
|
+
@value == LOWER
|
41
|
+
end
|
42
|
+
|
43
|
+
def max?
|
44
|
+
@value == UPPER
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def clip(value)
|
50
|
+
[LOWER, value.to_i, UPPER].sort[1]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def Percentage(value)
|
56
|
+
return value if value.kind_of?(Switches::Percentage)
|
57
|
+
Switches::Percentage.new(value)
|
58
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Switches
|
2
|
+
class Update
|
3
|
+
attr_accessor :type, :name, :node_id
|
4
|
+
|
5
|
+
def initialize(attributes = {})
|
6
|
+
@type = attributes["type"]
|
7
|
+
@name = attributes["name"]
|
8
|
+
@node_id = attributes["node_id"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def from?(node_id)
|
12
|
+
@node_id == node_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_json
|
16
|
+
{
|
17
|
+
"type" => type,
|
18
|
+
"name" => name,
|
19
|
+
"node_id" => node_id
|
20
|
+
}.to_json
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/switches.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "thread"
|
3
|
+
require "monitor"
|
4
|
+
require "json"
|
5
|
+
require "set"
|
6
|
+
require "zlib"
|
7
|
+
|
8
|
+
require "switches/configuration"
|
9
|
+
require "switches/instance"
|
10
|
+
require "switches/cohorts"
|
11
|
+
require "switches/cohort"
|
12
|
+
require "switches/features"
|
13
|
+
require "switches/feature"
|
14
|
+
require "switches/percentage"
|
15
|
+
require "switches/update"
|
16
|
+
require "switches/backends/redis"
|
17
|
+
require "switches/backends/memory"
|
18
|
+
require "switches/backends/postgres"
|
19
|
+
require "switches/backend"
|
20
|
+
|
21
|
+
Thread.abort_on_exception = true
|
22
|
+
|
23
|
+
def Switches
|
24
|
+
configuration = Switches::Configuration.new
|
25
|
+
yield configuration
|
26
|
+
|
27
|
+
instance = Switches::Instance.new(configuration)
|
28
|
+
instance.start
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: feature_switches
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John Pignata
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-04-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Feature switches for applications that run on multiple nodes that uses
|
28
|
+
a not-very-chatty protocol
|
29
|
+
email:
|
30
|
+
- john@pignata.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- lib/switches/backend.rb
|
36
|
+
- lib/switches/backends/memory.rb
|
37
|
+
- lib/switches/backends/postgres.rb
|
38
|
+
- lib/switches/backends/redis.rb
|
39
|
+
- lib/switches/cohort.rb
|
40
|
+
- lib/switches/cohorts.rb
|
41
|
+
- lib/switches/configuration.rb
|
42
|
+
- lib/switches/feature.rb
|
43
|
+
- lib/switches/features.rb
|
44
|
+
- lib/switches/instance.rb
|
45
|
+
- lib/switches/percentage.rb
|
46
|
+
- lib/switches/update.rb
|
47
|
+
- lib/switches.rb
|
48
|
+
- README.md
|
49
|
+
homepage: http://github.com/jpignata/switches
|
50
|
+
licenses: []
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.0.0
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Feature switches
|
72
|
+
test_files: []
|