stoplight 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,84 +8,128 @@ module Stoplight
8
8
  end
9
9
 
10
10
  def names
11
- all_thresholds.keys
11
+ thresholds.keys
12
12
  end
13
13
 
14
- def purge
14
+ def clear_stale
15
15
  names
16
- .select { |l| failures(l).empty? }
17
- .each { |l| delete(l) }
16
+ .select { |name| get_failures(name).empty? }
17
+ .each { |name| clear(name) }
18
+ nil
18
19
  end
19
20
 
20
- def delete(name)
21
+ def clear(name)
21
22
  clear_attempts(name)
22
23
  clear_failures(name)
23
- all_states.delete(name)
24
- all_thresholds.delete(name)
24
+ clear_state(name)
25
+ clear_threshold(name)
26
+ clear_timeout(name)
25
27
  end
26
28
 
27
- # @group Attempts
29
+ def sync(name)
30
+ set_threshold(name, get_threshold(name))
31
+ nil
32
+ end
33
+
34
+ def get_color(name)
35
+ state = get_state(name)
36
+ threshold = get_threshold(name)
37
+ failures = get_failures(name)
38
+ timeout = get_timeout(name)
39
+ DataStore.colorize(state, threshold, failures, timeout)
40
+ end
28
41
 
29
- def attempts(name)
30
- all_attempts[name] || 0
42
+ def get_attempts(name)
43
+ attempts[name]
31
44
  end
32
45
 
33
46
  def record_attempt(name)
34
- all_attempts[name] ||= 0
35
- all_attempts[name] += 1
47
+ attempts[name] += 1
36
48
  end
37
49
 
38
50
  def clear_attempts(name)
39
- all_attempts.delete(name)
51
+ attempts.delete(name)
52
+ nil
40
53
  end
41
54
 
42
- # @group Failures
43
-
44
- def failures(name)
45
- @data[failures_key(name)] || []
55
+ def get_failures(name)
56
+ @data.fetch(DataStore.failures_key(name), DEFAULT_FAILURES)
46
57
  end
47
58
 
48
- def record_failure(name, error)
49
- (@data[failures_key(name)] ||= []).push(Failure.new(error))
59
+ def record_failure(name, failure)
60
+ DataStore.validate_failure!(failure)
61
+ @data[DataStore.failures_key(name)] ||= DEFAULT_FAILURES.dup
62
+ @data[DataStore.failures_key(name)].push(failure)
63
+ failure
50
64
  end
51
65
 
52
66
  def clear_failures(name)
53
- @data.delete(failures_key(name))
67
+ @data.delete(DataStore.failures_key(name))
68
+ nil
54
69
  end
55
70
 
56
- # @group State
57
-
58
- def state(name)
59
- all_states[name] || STATE_UNLOCKED
71
+ def get_state(name)
72
+ states[name]
60
73
  end
61
74
 
62
75
  def set_state(name, state)
63
- validate_state!(state)
64
- all_states[name] = state
76
+ DataStore.validate_state!(state)
77
+ states[name] = state
65
78
  end
66
79
 
67
- # @group Threshold
80
+ def clear_state(name)
81
+ states.delete(name)
82
+ nil
83
+ end
68
84
 
69
- def threshold(name)
70
- all_thresholds[name]
85
+ def get_threshold(name)
86
+ thresholds[name]
71
87
  end
72
88
 
73
89
  def set_threshold(name, threshold)
74
- all_thresholds[name] = threshold
90
+ DataStore.validate_threshold!(threshold)
91
+ thresholds[name] = threshold
92
+ end
93
+
94
+ def clear_threshold(name)
95
+ thresholds.delete(name)
96
+ nil
97
+ end
98
+
99
+ def get_timeout(name)
100
+ timeouts[name]
101
+ end
102
+
103
+ def set_timeout(name, timeout)
104
+ DataStore.validate_timeout!(timeout)
105
+ timeouts[name] = timeout
106
+ end
107
+
108
+ def clear_timeout(name)
109
+ timeouts.delete(name)
110
+ nil
75
111
  end
76
112
 
77
113
  private
78
114
 
79
- def all_attempts
80
- @data[attempts_key] ||= {}
115
+ # @return [Hash{String => Integer}]
116
+ def attempts
117
+ @data[DataStore.attempts_key] ||= Hash.new(DEFAULT_ATTEMPTS)
118
+ end
119
+
120
+ # @return [Hash{String => String}]
121
+ def states
122
+ @data[DataStore.states_key] ||= Hash.new(DEFAULT_STATE)
81
123
  end
82
124
 
83
- def all_states
84
- @data[states_key] ||= {}
125
+ # @return [Hash{String => Integer}]
126
+ def thresholds
127
+ @data[DataStore.thresholds_key] ||= Hash.new(DEFAULT_THRESHOLD)
85
128
  end
86
129
 
87
- def all_thresholds
88
- @data[thresholds_key] ||= {}
130
+ # @return [Hash{String => Integer}]
131
+ def timeouts
132
+ @data[DataStore.timeouts_key] ||= Hash.new(DEFAULT_TIMEOUT)
89
133
  end
90
134
  end
91
135
  end
@@ -1,4 +1,7 @@
1
1
  # coding: utf-8
2
+ # rubocop:disable Metrics/ClassLength
3
+
4
+ require 'json'
2
5
 
3
6
  module Stoplight
4
7
  module DataStore
@@ -8,75 +11,162 @@ module Stoplight
8
11
  end
9
12
 
10
13
  def names
11
- @redis.hkeys(thresholds_key)
14
+ @redis.hkeys(DataStore.thresholds_key)
12
15
  end
13
16
 
14
- def purge
17
+ def clear_stale
15
18
  names
16
- .select { |l| failures(l).empty? }
17
- .each { |l| delete(l) }
19
+ .select { |name| get_failures(name).empty? }
20
+ .each { |name| clear(name) }
21
+ nil
18
22
  end
19
23
 
20
- def delete(name)
24
+ def clear(name)
21
25
  @redis.pipelined do
22
26
  clear_attempts(name)
23
27
  clear_failures(name)
24
- @redis.hdel(states_key, name)
25
- @redis.hdel(thresholds_key, name)
28
+ clear_state(name)
29
+ clear_threshold(name)
30
+ clear_timeout(name)
26
31
  end
32
+
33
+ nil
34
+ end
35
+
36
+ def sync(name)
37
+ threshold = @redis.hget(DataStore.thresholds_key, name)
38
+ threshold = normalize_threshold(threshold)
39
+ @redis.hset(DataStore.thresholds_key, name, threshold)
40
+ nil
27
41
  end
28
42
 
29
- # @group Attempts
43
+ def get_color(name)
44
+ DataStore.colorize(*colorize_args(name))
45
+ end
30
46
 
31
- def attempts(name)
32
- @redis.hget(attempts_key, name).to_i
47
+ def get_attempts(name)
48
+ normalize_attempts(@redis.hget(DataStore.attempts_key, name))
33
49
  end
34
50
 
35
51
  def record_attempt(name)
36
- @redis.hincrby(attempts_key, name, 1)
52
+ @redis.hincrby(DataStore.attempts_key, name, 1)
37
53
  end
38
54
 
39
55
  def clear_attempts(name)
40
- @redis.hdel(attempts_key, name)
56
+ @redis.hdel(DataStore.attempts_key, name)
57
+ nil
41
58
  end
42
59
 
43
- # @group Failures
44
-
45
- def failures(name)
46
- @redis.lrange(failures_key(name), 0, -1)
60
+ def get_failures(name)
61
+ normalize_failures(@redis.lrange(DataStore.failures_key(name), 0, -1))
47
62
  end
48
63
 
49
- def record_failure(name, error)
50
- @redis.rpush(failures_key(name), Failure.new(error).to_json)
64
+ def record_failure(name, failure)
65
+ DataStore.validate_failure!(failure)
66
+ @redis.rpush(DataStore.failures_key(name), failure.to_json)
67
+ failure
51
68
  end
52
69
 
53
70
  def clear_failures(name)
54
- @redis.del(failures_key(name))
71
+ @redis.del(DataStore.failures_key(name))
72
+ nil
55
73
  end
56
74
 
57
- # @group State
58
-
59
- def state(name)
60
- @redis.hget(states_key, name) || STATE_UNLOCKED
75
+ def get_state(name)
76
+ normalize_state(@redis.hget(DataStore.states_key, name))
61
77
  end
62
78
 
63
79
  def set_state(name, state)
64
- validate_state!(state)
65
- @redis.hset(states_key, name, state)
80
+ DataStore.validate_state!(state)
81
+ @redis.hset(DataStore.states_key, name, state)
66
82
  state
67
83
  end
68
84
 
69
- # @group Threshold
85
+ def clear_state(name)
86
+ @redis.hdel(DataStore.states_key, name)
87
+ nil
88
+ end
70
89
 
71
- def threshold(name)
72
- value = @redis.hget(thresholds_key, name)
73
- Integer(value) if value
90
+ def get_threshold(name)
91
+ normalize_threshold(@redis.hget(DataStore.thresholds_key, name))
74
92
  end
75
93
 
76
94
  def set_threshold(name, threshold)
77
- @redis.hset(thresholds_key, name, threshold)
95
+ DataStore.validate_threshold!(threshold)
96
+ @redis.hset(DataStore.thresholds_key, name, threshold)
78
97
  threshold
79
98
  end
99
+
100
+ def clear_threshold(name)
101
+ @redis.hdel(DataStore.thresholds_key, name)
102
+ nil
103
+ end
104
+
105
+ def get_timeout(name)
106
+ normalize_timeout(@redis.hget(DataStore.timeouts_key, name))
107
+ end
108
+
109
+ def set_timeout(name, timeout)
110
+ DataStore.validate_timeout!(timeout)
111
+ @redis.hset(DataStore.timeouts_key, name, timeout)
112
+ timeout
113
+ end
114
+
115
+ def clear_timeout(name)
116
+ @redis.hdel(DataStore.timeouts_key, name)
117
+ nil
118
+ end
119
+
120
+ private
121
+
122
+ def colorize_args(name)
123
+ state, threshold, failures, timeout = @redis.pipelined do
124
+ @redis.hget(DataStore.states_key, name)
125
+ @redis.hget(DataStore.thresholds_key, name)
126
+ @redis.lrange(DataStore.failures_key(name), 0, -1)
127
+ @redis.hget(DataStore.timeouts_key, name)
128
+ end
129
+ normalize_colorize_args(state, threshold, failures, timeout)
130
+ end
131
+
132
+ def normalize_colorize_args(state, threshold, failures, timeout)
133
+ [
134
+ normalize_state(state),
135
+ normalize_threshold(threshold),
136
+ normalize_failures(failures),
137
+ normalize_timeout(timeout)
138
+ ]
139
+ end
140
+
141
+ # @param attempts [String, nil]
142
+ # @return [Integer]
143
+ def normalize_attempts(attempts)
144
+ attempts ? attempts.to_i : DEFAULT_ATTEMPTS
145
+ end
146
+
147
+ # @param failures [Array<String>]
148
+ # @return [Array<Failure>]
149
+ def normalize_failures(failures)
150
+ failures.map { |json| Failure.from_json(json) }
151
+ end
152
+
153
+ # @param state [String, nil]
154
+ # @return [String]
155
+ def normalize_state(state)
156
+ state || DEFAULT_STATE
157
+ end
158
+
159
+ # @param threshold [String, nil]
160
+ # @return [Integer]
161
+ def normalize_threshold(threshold)
162
+ threshold ? threshold.to_i : DEFAULT_THRESHOLD
163
+ end
164
+
165
+ # @param timeout [String, nil]
166
+ # @return [Integer]
167
+ def normalize_timeout(timeout)
168
+ timeout ? timeout.to_i : DEFAULT_TIMEOUT
169
+ end
80
170
  end
81
171
  end
82
172
  end
@@ -4,7 +4,23 @@ module Stoplight
4
4
  module Error
5
5
  # @return [Class]
6
6
  Base = Class.new(StandardError)
7
+
7
8
  # @return [Class]
8
9
  RedLight = Class.new(Base)
10
+
11
+ # @return [Class]
12
+ InvalidColor = Class.new(Base)
13
+
14
+ # @return [Class]
15
+ InvalidFailure = Class.new(Base)
16
+
17
+ # @return [Class]
18
+ InvalidState = Class.new(Base)
19
+
20
+ # @return [Class]
21
+ InvalidThreshold = Class.new(Base)
22
+
23
+ # @return [Class]
24
+ InvalidTimeout = Class.new(Base)
9
25
  end
10
26
  end
@@ -4,21 +4,34 @@ require 'json'
4
4
 
5
5
  module Stoplight
6
6
  class Failure
7
- # @param error [Exception]
8
- def initialize(error)
9
- @error = error
10
- @time = Time.now
11
- end
7
+ # @return [Exception]
8
+ attr_reader :error
9
+
10
+ # @return [Time]
11
+ attr_reader :time
12
+
13
+ def self.from_json(json)
14
+ h = JSON.parse(json)
12
15
 
13
- # @return [String]
14
- def to_json
15
- JSON.dump(to_h)
16
+ match = /#<(.+): (.+)>/.match(h['error'])
17
+ error = Object.const_get(match[1]).new(match[2]) if match
18
+
19
+ time = Time.parse(h['time'])
20
+
21
+ new(error, time)
16
22
  end
17
23
 
18
- private
24
+ # @param error [Exception]
25
+ def initialize(error, time = nil)
26
+ @error = error
27
+ @time = time || Time.now
28
+ end
19
29
 
20
- def to_h
21
- { error: @error.inspect, time: @time.inspect }
30
+ def to_json(*args)
31
+ {
32
+ error: @error.inspect,
33
+ time: time.inspect
34
+ }.to_json(*args)
22
35
  end
23
36
  end
24
37
  end
@@ -24,17 +24,15 @@ module Stoplight
24
24
  # @see #fallback
25
25
  # @see #green?
26
26
  def run
27
- sync_settings
28
-
29
- if green?
30
- run_code
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
-
37
- run_fallback
27
+ Stoplight.data_store.sync(name)
28
+
29
+ case color
30
+ when DataStore::COLOR_GREEN
31
+ run_green
32
+ when DataStore::COLOR_YELLOW
33
+ run_yellow
34
+ when DataStore::COLOR_RED
35
+ run_red
38
36
  end
39
37
  end
40
38
 
@@ -57,7 +55,14 @@ module Stoplight
57
55
  # @param threshold [Integer]
58
56
  # @return [self]
59
57
  def with_threshold(threshold)
60
- Stoplight.set_threshold(name, threshold.to_i)
58
+ Stoplight.data_store.set_threshold(name, threshold)
59
+ self
60
+ end
61
+
62
+ # @param timeout [Integer]
63
+ # @return [self]
64
+ def with_timeout(timeout)
65
+ Stoplight.data_store.set_timeout(name, timeout)
61
66
  self
62
67
  end
63
68
 
@@ -67,51 +72,75 @@ module Stoplight
67
72
  # @raise [Error::RedLight]
68
73
  def fallback
69
74
  return @fallback if defined?(@fallback)
70
- fail Error::RedLight
75
+ fail Error::RedLight, name
71
76
  end
72
77
 
73
- # @return (see Stoplight.green?)
78
+ # @return (see Stoplight::DataStore::Base#threshold)
79
+ def threshold
80
+ Stoplight.data_store.get_threshold(name)
81
+ end
82
+
83
+ # @return (see Stoplight::DataStore::Base#timeout)
84
+ def timeout
85
+ Stoplight.data_store.get_timeout(name)
86
+ end
87
+
88
+ # Colors
89
+
90
+ # @return (see Stoplight::DataStore::Base#get_color)
91
+ def color
92
+ Stoplight.data_store.get_color(name)
93
+ end
94
+
95
+ # @return (see Stoplight::DataStore::Base#green?)
74
96
  def green?
75
- Stoplight.green?(name)
97
+ Stoplight.data_store.green?(name)
76
98
  end
77
99
 
78
- # @return (see Stoplight.red?)
79
- def red?
80
- !green?
100
+ # @return (see Stoplight::DataStore::Base#yellow?)
101
+ def yellow?
102
+ Stoplight.data_store.yellow?(name)
81
103
  end
82
104
 
83
- # @return (see Stoplight.threshold)
84
- def threshold
85
- Stoplight.threshold(name)
105
+ # @return (see Stoplight::DataStore::Base#red?)
106
+ def red?
107
+ Stoplight.data_store.red?(name)
86
108
  end
87
109
 
88
110
  private
89
111
 
90
- def error_allowed?(error)
91
- allowed_errors.any? { |klass| error.is_a?(klass) }
112
+ def run_green
113
+ code.call.tap { Stoplight.data_store.clear_failures(name) }
114
+ rescue => error
115
+ handle_error(error)
116
+ raise
92
117
  end
93
118
 
94
- def run_code
95
- result = code.call
96
- Stoplight.clear_failures(name)
97
- result
98
- rescue => error
119
+ def run_yellow
120
+ run_green.tap { notify("Switching #{name} from red to green.") }
121
+ end
122
+
123
+ def run_red
124
+ if Stoplight.data_store.record_attempt(name) == 1
125
+ notify("Switching #{name} from green to red.")
126
+ end
127
+ fallback.call
128
+ end
129
+
130
+ def handle_error(error)
99
131
  if error_allowed?(error)
100
- Stoplight.clear_failures(name)
132
+ Stoplight.data_store.clear_failures(name)
101
133
  else
102
- Stoplight.record_failure(name, error)
134
+ Stoplight.data_store.record_failure(name, Failure.new(error))
103
135
  end
104
-
105
- raise
106
136
  end
107
137
 
108
- def run_fallback
109
- Stoplight.record_attempt(name)
110
- fallback.call
138
+ def error_allowed?(error)
139
+ allowed_errors.any? { |klass| error.is_a?(klass) }
111
140
  end
112
141
 
113
- def sync_settings
114
- Stoplight.set_threshold(name, threshold)
142
+ def notify(message)
143
+ Stoplight.notifiers.each { |notifier| notifier.notify(message) }
115
144
  end
116
145
  end
117
146
  end