stoplight 0.4.1 → 0.5.0

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