feature_switches 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|