stoplight 0.4.1 → 0.5.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +66 -63
  5. data/lib/stoplight.rb +10 -15
  6. data/lib/stoplight/color.rb +9 -0
  7. data/lib/stoplight/data_store.rb +0 -146
  8. data/lib/stoplight/data_store/base.rb +7 -130
  9. data/lib/stoplight/data_store/memory.rb +25 -100
  10. data/lib/stoplight/data_store/redis.rb +61 -119
  11. data/lib/stoplight/default.rb +34 -0
  12. data/lib/stoplight/error.rb +0 -42
  13. data/lib/stoplight/failure.rb +21 -25
  14. data/lib/stoplight/light.rb +42 -127
  15. data/lib/stoplight/light/runnable.rb +97 -0
  16. data/lib/stoplight/notifier/base.rb +1 -4
  17. data/lib/stoplight/notifier/hip_chat.rb +17 -32
  18. data/lib/stoplight/notifier/io.rb +9 -9
  19. data/lib/stoplight/state.rb +9 -0
  20. data/spec/spec_helper.rb +2 -3
  21. data/spec/stoplight/color_spec.rb +39 -0
  22. data/spec/stoplight/data_store/base_spec.rb +56 -36
  23. data/spec/stoplight/data_store/memory_spec.rb +120 -2
  24. data/spec/stoplight/data_store/redis_spec.rb +123 -24
  25. data/spec/stoplight/data_store_spec.rb +2 -69
  26. data/spec/stoplight/default_spec.rb +86 -0
  27. data/spec/stoplight/error_spec.rb +29 -0
  28. data/spec/stoplight/failure_spec.rb +61 -51
  29. data/spec/stoplight/light/runnable_spec.rb +234 -0
  30. data/spec/stoplight/light_spec.rb +143 -191
  31. data/spec/stoplight/notifier/base_spec.rb +8 -11
  32. data/spec/stoplight/notifier/hip_chat_spec.rb +66 -55
  33. data/spec/stoplight/notifier/io_spec.rb +49 -21
  34. data/spec/stoplight/notifier_spec.rb +3 -0
  35. data/spec/stoplight/state_spec.rb +39 -0
  36. data/spec/stoplight_spec.rb +2 -65
  37. metadata +55 -19
  38. data/spec/support/data_store.rb +0 -36
  39. data/spec/support/fakeredis.rb +0 -3
  40. data/spec/support/hipchat.rb +0 -3
@@ -3,158 +3,35 @@
3
3
  module Stoplight
4
4
  module DataStore
5
5
  class Base
6
- # @return [Array<String>]
7
6
  def names
8
7
  fail NotImplementedError
9
8
  end
10
9
 
11
- def clear_stale
10
+ def get_all(_light)
12
11
  fail NotImplementedError
13
12
  end
14
13
 
15
- # @param _name [String]
16
- def clear(_name)
14
+ def get_failures(_light)
17
15
  fail NotImplementedError
18
16
  end
19
17
 
20
- # @param _name [String]
21
- def sync(_name)
18
+ def record_failure(_light, _failure)
22
19
  fail NotImplementedError
23
20
  end
24
21
 
25
- # @param _name [String]
26
- def greenify(_name)
22
+ def clear_failures(_light)
27
23
  fail NotImplementedError
28
24
  end
29
25
 
30
- # @group Colors
31
-
32
- # @param name [String]
33
- # @return [Boolean]
34
- def green?(name)
35
- color = get_color(name)
36
- DataStore.validate_color!(color)
37
- color == COLOR_GREEN
38
- end
39
-
40
- # @param name [String]
41
- # @return [Boolean]
42
- def yellow?(name)
43
- color = get_color(name)
44
- DataStore.validate_color!(color)
45
- color == COLOR_YELLOW
46
- end
47
-
48
- # @param name [String]
49
- # @return [Boolean]
50
- def red?(name)
51
- color = get_color(name)
52
- DataStore.validate_color!(color)
53
- color == COLOR_RED
54
- end
55
-
56
- # @param _name [String]
57
- # @return [String]
58
- def get_color(_name)
59
- fail NotImplementedError
60
- end
61
-
62
- # @group Attempts
63
-
64
- # @param _name [String]
65
- # @return [Integer]
66
- def get_attempts(_name)
67
- fail NotImplementedError
68
- end
69
-
70
- # @param _name [String]
71
- # @return [Integer]
72
- def record_attempt(_name)
73
- fail NotImplementedError
74
- end
75
-
76
- # @param _name [String]
77
- def clear_attempts(_name)
78
- fail NotImplementedError
79
- end
80
-
81
- # @group Failures
82
-
83
- # @param _name [String]
84
- # @return [Array<Failure>]
85
- def get_failures(_name)
86
- fail NotImplementedError
87
- end
88
-
89
- # @param _name [String]
90
- # @param _failure [Failure]
91
- # @return [Failure]
92
- def record_failure(_name, _failure)
93
- fail NotImplementedError
94
- end
95
-
96
- # @param _name [String]
97
- def clear_failures(_name)
98
- fail NotImplementedError
99
- end
100
-
101
- # @group States
102
-
103
- # @param _name [String]
104
- # @return [String]
105
- def get_state(_name)
106
- fail NotImplementedError
107
- end
108
-
109
- # @param _name [String]
110
- # @param _state [String]
111
- # @return [String]
112
- def set_state(_name, _state)
113
- fail NotImplementedError
114
- end
115
-
116
- # @param _name [String]
117
- def clear_state(_name)
118
- fail NotImplementedError
119
- end
120
-
121
- # @group Thresholds
122
-
123
- # @param _name [String]
124
- # @return [Integer]
125
- def get_threshold(_name)
126
- fail NotImplementedError
127
- end
128
-
129
- # @param _name [String]
130
- # @param _threshold [Integer]
131
- # @return [Integer]
132
- def set_threshold(_name, _threshold)
133
- fail NotImplementedError
134
- end
135
-
136
- # @param _name [String]
137
- def clear_threshold(_name)
138
- fail NotImplementedError
139
- end
140
-
141
- # @group Timeouts
142
-
143
- # @param _name [String]
144
- # @return [Integer]
145
- def get_timeout(_name)
26
+ def get_state(_light)
146
27
  fail NotImplementedError
147
28
  end
148
29
 
149
- # @param _name [String]
150
- # @param _timeout [Integer]
151
- # @return [Integer]
152
- def set_timeout(_name, _timeout)
30
+ def set_state(_light, _state)
153
31
  fail NotImplementedError
154
32
  end
155
33
 
156
- # @param _name [String]
157
- def clear_timeout(_name)
34
+ def clear_state(_light)
158
35
  fail NotImplementedError
159
36
  end
160
37
  end
@@ -8,126 +8,51 @@ module Stoplight
8
8
  end
9
9
 
10
10
  def names
11
- thresholds.keys
11
+ (all_failures.keys + all_states.keys).uniq
12
12
  end
13
13
 
14
- def clear_stale
15
- names
16
- .select { |name| get_failures(name).empty? }
17
- .each { |name| clear(name) }
14
+ def get_all(light)
15
+ [get_failures(light), get_state(light)]
18
16
  end
19
17
 
20
- def clear(name)
21
- clear_attempts(name)
22
- clear_failures(name)
23
- clear_state(name)
24
- clear_threshold(name)
25
- clear_timeout(name)
18
+ def get_failures(light)
19
+ all_failures[light.name] || []
26
20
  end
27
21
 
28
- def sync(name)
29
- set_threshold(name, get_threshold(name))
22
+ def record_failure(light, failure)
23
+ failures = get_failures(light).unshift(failure).first(light.threshold)
24
+ all_failures[light.name] = failures
25
+ failures.size
30
26
  end
31
27
 
32
- def greenify(name)
33
- clear_attempts(name)
34
- clear_failures(name)
28
+ def clear_failures(light)
29
+ failures = get_failures(light)
30
+ all_failures.delete(light.name)
31
+ failures
35
32
  end
36
33
 
37
- def get_color(name)
38
- state = get_state(name)
39
- threshold = get_threshold(name)
40
- failures = get_failures(name)
41
- timeout = get_timeout(name)
42
- DataStore.colorize(state, threshold, failures, timeout)
34
+ def get_state(light)
35
+ all_states[light.name] || State::UNLOCKED
43
36
  end
44
37
 
45
- def get_attempts(name)
46
- attempts[name]
38
+ def set_state(light, state)
39
+ all_states[light.name] = state
47
40
  end
48
41
 
49
- def record_attempt(name)
50
- attempts[name] += 1
51
- end
52
-
53
- def clear_attempts(name)
54
- attempts.delete(name)
55
- end
56
-
57
- def get_failures(name)
58
- @data.fetch(DataStore.failures_key(name), DEFAULT_FAILURES)
59
- end
60
-
61
- def record_failure(name, failure)
62
- DataStore.validate_failure!(failure)
63
- @data[DataStore.failures_key(name)] ||= DEFAULT_FAILURES.dup
64
- @data[DataStore.failures_key(name)].push(failure)
65
- failure
66
- end
67
-
68
- def clear_failures(name)
69
- @data.delete(DataStore.failures_key(name))
70
- end
71
-
72
- def get_state(name)
73
- states[name]
74
- end
75
-
76
- def set_state(name, state)
77
- DataStore.validate_state!(state)
78
- states[name] = state
79
- end
80
-
81
- def clear_state(name)
82
- states.delete(name)
83
- end
84
-
85
- def get_threshold(name)
86
- thresholds[name]
87
- end
88
-
89
- def set_threshold(name, threshold)
90
- DataStore.validate_threshold!(threshold)
91
- thresholds[name] = threshold
92
- end
93
-
94
- def clear_threshold(name)
95
- thresholds.delete(name)
96
- end
97
-
98
- def get_timeout(name)
99
- timeouts[name]
100
- end
101
-
102
- def set_timeout(name, timeout)
103
- DataStore.validate_timeout!(timeout)
104
- timeouts[name] = timeout
105
- end
106
-
107
- def clear_timeout(name)
108
- timeouts.delete(name)
42
+ def clear_state(light)
43
+ state = get_state(light)
44
+ all_states.delete(light.name)
45
+ state
109
46
  end
110
47
 
111
48
  private
112
49
 
113
- # @return [Hash{String => Integer}]
114
- def attempts
115
- @data[DataStore.attempts_key] ||= Hash.new(DEFAULT_ATTEMPTS)
116
- end
117
-
118
- # @return [Hash{String => String}]
119
- def states
120
- @data[DataStore.states_key] ||= Hash.new(DEFAULT_STATE)
121
- end
122
-
123
- # @return [Hash{String => Integer}]
124
- def thresholds
125
- @data[DataStore.thresholds_key] ||= Hash.new(DEFAULT_THRESHOLD)
50
+ def all_failures
51
+ @data['failures'] ||= {}
126
52
  end
127
53
 
128
- # @return [Hash{String => Integer}]
129
- def timeouts
130
- @data[DataStore.timeouts_key] ||= Hash.new(DEFAULT_TIMEOUT)
54
+ def all_states
55
+ @data['states'] ||= {}
131
56
  end
132
57
  end
133
58
  end
@@ -1,171 +1,113 @@
1
1
  # coding: utf-8
2
- # rubocop:disable Metrics/ClassLength
3
-
4
- require 'json'
5
2
 
6
3
  module Stoplight
7
4
  module DataStore
8
5
  class Redis < Base
6
+ KEY_PREFIX = 'stoplight'.freeze
7
+ KEY_SEPARATOR = ':'.freeze
8
+
9
9
  def initialize(redis)
10
10
  @redis = redis
11
11
  end
12
12
 
13
13
  def names
14
- @redis.hkeys(DataStore.thresholds_key)
15
- end
16
-
17
- def clear_stale
18
- names
19
- .select { |name| get_failures(name).empty? }
20
- .each { |name| clear(name) }
21
- end
14
+ state_names = @redis.hkeys(states_key)
22
15
 
23
- def clear(name)
24
- @redis.pipelined do
25
- clear_attempts(name)
26
- clear_failures(name)
27
- clear_state(name)
28
- clear_threshold(name)
29
- clear_timeout(name)
16
+ pattern = key('failures', '*')
17
+ failure_names = @redis.scan_each(match: pattern).to_a.map do |key|
18
+ key.split(KEY_SEPARATOR).last
30
19
  end
31
- end
32
20
 
33
- def sync(name)
34
- threshold = @redis.hget(DataStore.thresholds_key, name)
35
- threshold = normalize_threshold(threshold)
36
- @redis.hset(DataStore.thresholds_key, name, threshold)
37
- rescue ::Redis::BaseError => error
38
- raise Error::BadDataStore, error
21
+ (state_names + failure_names).uniq
39
22
  end
40
23
 
41
- def greenify(name)
42
- @redis.pipelined do
43
- clear_attempts(name)
44
- clear_failures(name)
24
+ def get_all(light)
25
+ failures, state = @redis.multi do
26
+ query_failures(light)
27
+ @redis.hget('stoplight:states', light.name)
45
28
  end
46
- end
47
-
48
- def get_color(name)
49
- DataStore.colorize(*colorize_args(name))
50
- end
51
29
 
52
- def get_attempts(name)
53
- normalize_attempts(@redis.hget(DataStore.attempts_key, name))
30
+ [
31
+ normalize_failures(failures, light.error_notifier),
32
+ normalize_state(state)
33
+ ]
54
34
  end
55
35
 
56
- def record_attempt(name)
57
- @redis.hincrby(DataStore.attempts_key, name, 1)
36
+ def get_failures(light)
37
+ normalize_failures(query_failures(light), light.error_notifier)
58
38
  end
59
39
 
60
- def clear_attempts(name)
61
- @redis.hdel(DataStore.attempts_key, name)
62
- end
40
+ def record_failure(light, failure)
41
+ size, _ = @redis.multi do
42
+ @redis.lpush(failures_key(light), failure.to_json)
43
+ @redis.ltrim(failures_key(light), 0, light.threshold - 1)
44
+ end
63
45
 
64
- def get_failures(name)
65
- normalize_failures(@redis.lrange(DataStore.failures_key(name), 0, -1))
46
+ size
66
47
  end
67
48
 
68
- def record_failure(name, failure)
69
- DataStore.validate_failure!(failure)
70
- @redis.rpush(DataStore.failures_key(name), failure.to_json)
71
- failure
72
- end
49
+ def clear_failures(light)
50
+ failures, _ = @redis.multi do
51
+ query_failures(light)
52
+ @redis.del(failures_key(light))
53
+ end
73
54
 
74
- def clear_failures(name)
75
- @redis.del(DataStore.failures_key(name))
55
+ normalize_failures(failures, light.error_notifier)
76
56
  end
77
57
 
78
- def get_state(name)
79
- normalize_state(@redis.hget(DataStore.states_key, name))
58
+ def get_state(light)
59
+ query_state(light) || State::UNLOCKED
80
60
  end
81
61
 
82
- def set_state(name, state)
83
- DataStore.validate_state!(state)
84
- @redis.hset(DataStore.states_key, name, state)
62
+ def set_state(light, state)
63
+ @redis.hset(states_key, light.name, state)
85
64
  state
86
65
  end
87
66
 
88
- def clear_state(name)
89
- @redis.hdel(DataStore.states_key, name)
90
- end
91
-
92
- def get_threshold(name)
93
- normalize_threshold(@redis.hget(DataStore.thresholds_key, name))
94
- end
95
-
96
- def set_threshold(name, threshold)
97
- DataStore.validate_threshold!(threshold)
98
- @redis.hset(DataStore.thresholds_key, name, threshold)
99
- threshold
100
- end
101
-
102
- def clear_threshold(name)
103
- @redis.hdel(DataStore.thresholds_key, name)
104
- end
105
-
106
- def get_timeout(name)
107
- normalize_timeout(@redis.hget(DataStore.timeouts_key, name))
108
- end
109
-
110
- def set_timeout(name, timeout)
111
- DataStore.validate_timeout!(timeout)
112
- @redis.hset(DataStore.timeouts_key, name, timeout)
113
- timeout
114
- end
67
+ def clear_state(light)
68
+ state, _ = @redis.multi do
69
+ query_state(light)
70
+ @redis.hdel(states_key, light.name)
71
+ end
115
72
 
116
- def clear_timeout(name)
117
- @redis.hdel(DataStore.timeouts_key, name)
73
+ normalize_state(state)
118
74
  end
119
75
 
120
76
  private
121
77
 
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)
78
+ def query_failures(light)
79
+ @redis.lrange(failures_key(light), 0, -1)
130
80
  end
131
81
 
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
- ]
82
+ def normalize_failures(failures, error_notifier)
83
+ failures.map do |json|
84
+ begin
85
+ Failure.from_json(json)
86
+ rescue => error
87
+ error_notifier.call(error)
88
+ Failure.from_error(error)
89
+ end
90
+ end
139
91
  end
140
92
 
141
- # @param attempts [String, nil]
142
- # @return [Integer]
143
- def normalize_attempts(attempts)
144
- attempts ? attempts.to_i : DEFAULT_ATTEMPTS
93
+ def query_state(light)
94
+ @redis.hget(states_key, light.name)
145
95
  end
146
96
 
147
- # @param failures [Array<String>]
148
- # @return [Array<Failure>]
149
- def normalize_failures(failures)
150
- failures.map { |json| Failure.from_json(json) }
97
+ def normalize_state(state)
98
+ state || State::UNLOCKED
151
99
  end
152
100
 
153
- # @param state [String, nil]
154
- # @return [String]
155
- def normalize_state(state)
156
- state || DEFAULT_STATE
101
+ def failures_key(light)
102
+ key('failures', light.name)
157
103
  end
158
104
 
159
- # @param threshold [String, nil]
160
- # @return [Integer]
161
- def normalize_threshold(threshold)
162
- threshold ? threshold.to_i : DEFAULT_THRESHOLD
105
+ def states_key
106
+ key('states')
163
107
  end
164
108
 
165
- # @param timeout [String, nil]
166
- # @return [Integer]
167
- def normalize_timeout(timeout)
168
- timeout ? timeout.to_i : DEFAULT_TIMEOUT
109
+ def key(*pieces)
110
+ ([KEY_PREFIX] + pieces).join(KEY_SEPARATOR)
169
111
  end
170
112
  end
171
113
  end