stoplight 0.1.0 → 0.2.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.
@@ -1,72 +1,82 @@
1
1
  # coding: utf-8
2
2
 
3
- begin
4
- require 'redis'
5
- REDIS_LOADED = true
6
- rescue LoadError
7
- REDIS_LOADED = false
8
- end
9
-
10
3
  module Stoplight
11
4
  module DataStore
12
5
  class Redis < Base
13
- def initialize(*args)
14
- fail Error::NoRedis unless REDIS_LOADED
15
-
16
- @redis = ::Redis.new(*args)
6
+ def initialize(redis)
7
+ @redis = redis
17
8
  end
18
9
 
19
10
  def names
20
- @redis.scan_each(match: "#{DataStore::KEY_PREFIX}:*:*").map do |key|
21
- key[/^#{DataStore::KEY_PREFIX}:(.+):[^:]+$/o, 1]
22
- end.uniq
11
+ @redis.hkeys(thresholds_key)
23
12
  end
24
13
 
25
- def record_failure(name, error)
26
- failure = Failure.new(error)
27
- @redis.rpush(failure_key(name), failure.to_json)
28
- # TODO: Trim failures (think ring buffer). Probably in a multi block.
14
+ def purge
15
+ names
16
+ .select { |l| failures(l).empty? }
17
+ .each { |l| delete(l) }
29
18
  end
30
19
 
31
- def clear_failures(name)
32
- @redis.del(failure_key(name))
20
+ def delete(name)
21
+ @redis.pipelined do
22
+ clear_attempts(name)
23
+ clear_failures(name)
24
+ @redis.hdel(states_key, name)
25
+ @redis.hdel(thresholds_key, name)
26
+ end
33
27
  end
34
28
 
35
- def failures(name)
36
- @redis.lrange(failure_key(name), 0, -1)
29
+ # @group Attempts
30
+
31
+ def attempts(name)
32
+ @redis.hget(attempts_key, name).to_i
37
33
  end
38
34
 
39
- def threshold(name)
40
- value = @redis.hget(settings_key(name), 'threshold')
41
- Integer(value) if value
35
+ def record_attempt(name)
36
+ @redis.hincrby(attempts_key, name, 1)
42
37
  end
43
38
 
44
- def set_threshold(name, threshold)
45
- @redis.hset(settings_key(name), 'threshold', threshold)
46
- threshold
39
+ def clear_attempts(name)
40
+ @redis.hdel(attempts_key, name)
47
41
  end
48
42
 
49
- def record_attempt(name)
50
- @redis.incr(attempt_key(name))
43
+ # @group Failures
44
+
45
+ def failures(name)
46
+ @redis.lrange(failures_key(name), 0, -1)
51
47
  end
52
48
 
53
- def clear_attempts(name)
54
- @redis.del(attempt_key(name))
49
+ def record_failure(name, error)
50
+ @redis.rpush(failures_key(name), Failure.new(error).to_json)
55
51
  end
56
52
 
57
- def attempts(name)
58
- @redis.get(attempt_key(name)).to_i
53
+ def clear_failures(name)
54
+ @redis.del(failures_key(name))
59
55
  end
60
56
 
57
+ # @group State
58
+
61
59
  def state(name)
62
- @redis.hget(settings_key(name), 'state') || DataStore::STATE_UNLOCKED
60
+ @redis.hget(states_key, name) || STATE_UNLOCKED
63
61
  end
64
62
 
65
63
  def set_state(name, state)
66
64
  validate_state!(state)
67
- @redis.hset(settings_key(name), 'state', state)
65
+ @redis.hset(states_key, name, state)
68
66
  state
69
67
  end
68
+
69
+ # @group Threshold
70
+
71
+ def threshold(name)
72
+ value = @redis.hget(thresholds_key, name)
73
+ Integer(value) if value
74
+ end
75
+
76
+ def set_threshold(name, threshold)
77
+ @redis.hset(thresholds_key, name, threshold)
78
+ threshold
79
+ end
70
80
  end
71
81
  end
72
82
  end
@@ -5,8 +5,6 @@ module Stoplight
5
5
  # @return [Class]
6
6
  Base = Class.new(StandardError)
7
7
  # @return [Class]
8
- NoFallback = Class.new(Base)
9
- # @return [Class]
10
- NoRedis = Class.new(Base)
8
+ RedLight = Class.new(Base)
11
9
  end
12
10
  end
@@ -2,9 +2,6 @@
2
2
 
3
3
  module Stoplight
4
4
  class Light
5
- # @return [Integer]
6
- DEFAULT_THRESHOLD = 3
7
-
8
5
  # @return [Array<Exception>]
9
6
  attr_reader :allowed_errors
10
7
 
@@ -23,7 +20,7 @@ module Stoplight
23
20
  end
24
21
 
25
22
  # @return [Object]
26
- # @raise [Error::NoFallback]
23
+ # @raise [Error::RedLight]
27
24
  # @see #fallback
28
25
  # @see #green?
29
26
  def run
@@ -32,6 +29,11 @@ module Stoplight
32
29
  if green?
33
30
  run_code
34
31
  else
32
+ if Stoplight.attempts(name).zero?
33
+ message = "Switching #{name} stoplight from green to red."
34
+ Stoplight.notifiers.each { |notifier| notifier.notify(message) }
35
+ end
36
+
35
37
  run_fallback
36
38
  end
37
39
  end
@@ -55,17 +57,17 @@ module Stoplight
55
57
  # @param threshold [Integer]
56
58
  # @return [self]
57
59
  def with_threshold(threshold)
58
- Stoplight.data_store.set_threshold(name, threshold.to_i)
60
+ Stoplight.set_threshold(name, threshold.to_i)
59
61
  self
60
62
  end
61
63
 
62
64
  # Attribute readers
63
65
 
64
66
  # @return [Object]
65
- # @raise [Error::NoFallback]
67
+ # @raise [Error::RedLight]
66
68
  def fallback
67
69
  return @fallback if defined?(@fallback)
68
- fail Error::NoFallback
70
+ fail Error::RedLight
69
71
  end
70
72
 
71
73
  # @return (see Stoplight.green?)
@@ -91,25 +93,25 @@ module Stoplight
91
93
 
92
94
  def run_code
93
95
  result = code.call
94
- Stoplight.data_store.clear_failures(name)
96
+ Stoplight.clear_failures(name)
95
97
  result
96
98
  rescue => error
97
99
  if error_allowed?(error)
98
- Stoplight.data_store.clear_failures(name)
100
+ Stoplight.clear_failures(name)
99
101
  else
100
- Stoplight.data_store.record_failure(name, error)
102
+ Stoplight.record_failure(name, error)
101
103
  end
102
104
 
103
105
  raise
104
106
  end
105
107
 
106
108
  def run_fallback
107
- Stoplight.data_store.record_attempt(name)
109
+ Stoplight.record_attempt(name)
108
110
  fallback.call
109
111
  end
110
112
 
111
113
  def sync_settings
112
- Stoplight.data_store.set_threshold(name, threshold)
114
+ Stoplight.set_threshold(name, threshold)
113
115
  end
114
116
  end
115
117
  end
@@ -0,0 +1,9 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Mixin
5
+ def stoplight(name, &block)
6
+ Stoplight::Light.new(name, &block).run
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Notifier
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Notifier
5
+ class Base
6
+ def notify(_message)
7
+ fail NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Notifier
5
+ # @note hipchat ~> 1.3.0
6
+ class HipChat < Base
7
+ # @param client [HipChat::Client]
8
+ # @param room [String]
9
+ # @param options [Hash]
10
+ def initialize(client, room, options = {})
11
+ @client = client
12
+ @room = room
13
+ @options = default_options.merge(options)
14
+ end
15
+
16
+ def notify(message)
17
+ @client[@room].send('Stoplight', "@all #{message}", @options)
18
+ end
19
+
20
+ private
21
+
22
+ def default_options
23
+ { color: 'red', notify: true }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Notifier
5
+ class StandardError < Base
6
+ def notify(message)
7
+ warn(message)
8
+ end
9
+ end
10
+ end
11
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,3 +4,7 @@ require 'coveralls'
4
4
  Coveralls.wear!
5
5
 
6
6
  require 'stoplight'
7
+
8
+ Dir.glob(File.join('.', 'spec', 'support', '**', '*.rb')).each do |filename|
9
+ require filename
10
+ end
@@ -9,8 +9,10 @@ describe Stoplight::DataStore::Base do
9
9
  attempts
10
10
  clear_attempts
11
11
  clear_failures
12
+ delete
12
13
  failures
13
14
  names
15
+ purge
14
16
  record_attempt
15
17
  record_failure
16
18
  set_state
@@ -3,187 +3,7 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe Stoplight::DataStore::Memory do
6
- let(:error) { error_klass.new }
7
- let(:error_klass) { Class.new(StandardError) }
8
- let(:name) { SecureRandom.hex }
9
- let(:state) { Stoplight::DataStore::STATE_LOCKED_GREEN }
10
- let(:threshold) { rand(10) }
11
-
12
6
  subject(:data_store) { described_class.new }
13
7
 
14
- describe '#attempts' do
15
- subject(:result) { data_store.attempts(name) }
16
-
17
- it 'is zero' do
18
- expect(result).to eql(0)
19
- end
20
-
21
- context 'with an attempt' do
22
- before { data_store.record_attempt(name) }
23
-
24
- it 'is one' do
25
- expect(result).to eql(1)
26
- end
27
- end
28
- end
29
-
30
- describe '#clear_attempts' do
31
- subject(:result) { data_store.clear_attempts(name) }
32
-
33
- it 'returns nil' do
34
- expect(result).to be_nil
35
- end
36
-
37
- context 'with an attempt' do
38
- before { data_store.record_attempt(name) }
39
-
40
- it 'returns one' do
41
- expect(result).to eql(1)
42
- end
43
-
44
- it 'clears attempts' do
45
- result
46
- expect(data_store.attempts(name)).to eql(0)
47
- end
48
- end
49
- end
50
-
51
- describe '#clear_failures' do
52
- subject(:result) { data_store.clear_failures(name) }
53
-
54
- it 'returns nil' do
55
- expect(result).to be nil
56
- end
57
-
58
- context 'with a failure' do
59
- before { data_store.record_failure(name, error) }
60
-
61
- it 'returns the failures' do
62
- failures = data_store.failures(name)
63
- expect(result).to eql(failures)
64
- end
65
-
66
- it 'clears the failures' do
67
- result
68
- expect(data_store.failures(name)).to be_empty
69
- end
70
- end
71
- end
72
-
73
- describe '#threshold' do
74
- subject(:result) { data_store.threshold(name) }
75
-
76
- it 'is nil' do
77
- expect(result).to be nil
78
- end
79
-
80
- context 'with a threshold' do
81
- before { data_store.set_threshold(name, threshold) }
82
-
83
- it 'returns the threshold' do
84
- expect(result).to eql(threshold)
85
- end
86
- end
87
- end
88
-
89
- describe '#failures' do
90
- subject(:result) { data_store.failures(name) }
91
-
92
- it 'is an array' do
93
- expect(result).to be_an(Array)
94
- end
95
-
96
- it 'is empty' do
97
- expect(result).to be_empty
98
- end
99
-
100
- context 'with a failure' do
101
- before { data_store.record_failure(name, error) }
102
-
103
- it 'returns the failures' do
104
- expect(result.size).to eql(1)
105
- end
106
- end
107
- end
108
-
109
- describe '#names' do
110
- subject(:result) { data_store.names }
111
-
112
- it 'is an array' do
113
- expect(result).to be_an(Array)
114
- end
115
-
116
- it 'is empty' do
117
- expect(result).to be_empty
118
- end
119
-
120
- context 'with a name' do
121
- before { data_store.settings(name) }
122
-
123
- it 'includes the name' do
124
- expect(result).to include(name)
125
- end
126
- end
127
- end
128
-
129
- describe '#record_attempt' do
130
- subject(:result) { data_store.record_attempt(name) }
131
-
132
- it 'records the attempt' do
133
- result
134
- expect(data_store.attempts(name)).to eql(1)
135
- end
136
- end
137
-
138
- describe '#record_failure' do
139
- subject(:result) { data_store.record_failure(name, error) }
140
-
141
- it 'returns the failures' do
142
- expect(result).to eql(data_store.failures(name))
143
- end
144
-
145
- it 'logs the failure' do
146
- expect(result.size).to eql(1)
147
- end
148
- end
149
-
150
- describe '#set_threshold' do
151
- subject(:result) { data_store.set_threshold(name, threshold) }
152
-
153
- it 'returns the threshold' do
154
- expect(result).to eql(threshold)
155
- end
156
- end
157
-
158
- describe '#set_state' do
159
- subject(:result) { data_store.set_state(name, state) }
160
-
161
- it 'returns the state' do
162
- expect(result).to eql(state)
163
- end
164
-
165
- context 'with an invalid state' do
166
- let(:state) { SecureRandom.hex }
167
-
168
- it 'raises an error' do
169
- expect { result }.to raise_error(ArgumentError)
170
- end
171
- end
172
- end
173
-
174
- describe '#state' do
175
- subject(:result) { data_store.state(name) }
176
-
177
- it 'returns the default state' do
178
- expect(result).to eql(Stoplight::DataStore::STATE_UNLOCKED)
179
- end
180
-
181
- context 'with a custom state' do
182
- before { data_store.set_state(name, state) }
183
-
184
- it 'returns the state' do
185
- expect(result).to eql(state)
186
- end
187
- end
188
- end
8
+ it_behaves_like 'a data store'
189
9
  end