stoplight 0.2.1 → 0.3.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.
@@ -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