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
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Default
5
+ ALLOWED_ERRORS = [
6
+ NoMemoryError,
7
+ ScriptError,
8
+ SecurityError,
9
+ SignalException,
10
+ SystemExit,
11
+ SystemStackError
12
+ ].freeze
13
+
14
+ DATA_STORE = DataStore::Memory.new
15
+
16
+ ERROR_NOTIFIER = -> error { warn error }
17
+
18
+ FALLBACK = nil
19
+
20
+ FORMATTER = lambda do |light, from_color, to_color, error|
21
+ words = ['Switching', light.name, 'from', from_color, 'to', to_color]
22
+ words += ['because', error.class, error.message] if error
23
+ words.join(' ')
24
+ end
25
+
26
+ NOTIFIERS = [
27
+ Notifier::IO.new($stderr)
28
+ ].freeze
29
+
30
+ THRESHOLD = 3
31
+
32
+ TIMEOUT = 60.0
33
+ end
34
+ end
@@ -2,49 +2,7 @@
2
2
 
3
3
  module Stoplight
4
4
  module Error
5
- # @return [Class]
6
5
  Base = Class.new(StandardError)
7
-
8
- # @return [Class]
9
6
  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)
25
-
26
- # @return [Class]
27
- class BadDataStore < Base
28
- # @return [Exception]
29
- attr_reader :cause
30
-
31
- # @param cause [Exception]
32
- def initialize(cause)
33
- super(cause.message)
34
- @cause = cause
35
- end
36
- end
37
-
38
- # @return [Class]
39
- class BadNotifier < Base
40
- # @return [Exception]
41
- attr_reader :cause
42
-
43
- # @param cause [Exception]
44
- def initialize(cause)
45
- super(cause.message)
46
- @cause = cause
47
- end
48
- end
49
7
  end
50
8
  end
@@ -5,48 +5,44 @@ require 'time'
5
5
 
6
6
  module Stoplight
7
7
  class Failure
8
- # @return [String]
9
8
  attr_reader :error_class
10
-
11
- # @return [String]
12
9
  attr_reader :error_message
13
-
14
- # @return [Time]
15
10
  attr_reader :time
16
11
 
17
- # @param error [Exception]
18
- def self.create(error)
19
- new(error.class.name, error.message)
12
+ def self.from_error(error)
13
+ new(error.class.name, error.message, Time.new)
20
14
  end
21
15
 
22
- # @param json [String]
23
16
  def self.from_json(json)
24
- h = JSON.parse(json)
25
- new(
26
- h['error']['class'],
27
- h['error']['message'],
28
- Time.parse(h['time']))
29
- rescue => error
30
- new(Error::InvalidFailure.name, error.message)
17
+ object = JSON.parse(json)
18
+
19
+ error_class = object['error']['class']
20
+ error_message = object['error']['message']
21
+ time = Time.parse(object['time'])
22
+
23
+ new(error_class, error_message, time)
31
24
  end
32
25
 
33
- # @param error_class [String]
34
- # @param error_message [String]
35
- # @param time [Time, nil]
36
- def initialize(error_class, error_message, time = nil)
26
+ def initialize(error_class, error_message, time)
37
27
  @error_class = error_class
38
28
  @error_message = error_message
39
- @time = time || Time.now
29
+ @time = time
30
+ end
31
+
32
+ def ==(other)
33
+ error_class == other.error_class &&
34
+ error_message == other.error_message &&
35
+ time == other.time
40
36
  end
41
37
 
42
- def to_json(*args)
43
- {
38
+ def to_json
39
+ JSON.generate(
44
40
  error: {
45
41
  class: error_class,
46
42
  message: error_message
47
43
  },
48
- time: time.iso8601
49
- }.to_json(*args)
44
+ time: time.strftime('%Y-%m-%dT%H:%M:%S.%N%:z')
45
+ )
50
46
  end
51
47
  end
52
48
  end
@@ -2,159 +2,74 @@
2
2
 
3
3
  module Stoplight
4
4
  class Light
5
- # @return [Array<Exception>]
6
- attr_reader :allowed_errors
5
+ include Runnable
7
6
 
8
- # @return [Proc]
7
+ attr_reader :allowed_errors
9
8
  attr_reader :code
10
-
11
- # @return [String]
9
+ attr_reader :data_store
10
+ attr_reader :error_notifier
11
+ attr_reader :fallback
12
12
  attr_reader :name
13
+ attr_reader :notifiers
14
+ attr_reader :threshold
15
+ attr_reader :timeout
13
16
 
14
- # @param name [String]
15
- # @yield []
16
- def initialize(name, &code)
17
- @allowed_errors = []
18
- @code = code.to_proc
19
- @name = name.to_s
17
+ class << self
18
+ attr_accessor :default_data_store
19
+ attr_accessor :default_error_notifier
20
+ attr_accessor :default_notifiers
20
21
  end
21
22
 
22
- # @return [Object]
23
- # @raise [Error::RedLight]
24
- # @see #fallback
25
- # @see #green?
26
- def run
27
- sync
23
+ @default_data_store = Default::DATA_STORE
24
+ @default_error_notifier = Default::ERROR_NOTIFIER
25
+ @default_notifiers = Default::NOTIFIERS
28
26
 
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
36
- end
37
- end
27
+ def initialize(name, &code)
28
+ @name = name
29
+ @code = code
38
30
 
39
- # Fluent builders
31
+ @allowed_errors = Default::ALLOWED_ERRORS
32
+ @data_store = self.class.default_data_store
33
+ @error_notifier = self.class.default_error_notifier
34
+ @fallback = Default::FALLBACK
35
+ @notifiers = self.class.default_notifiers
36
+ @threshold = Default::THRESHOLD
37
+ @timeout = Default::TIMEOUT
38
+ end
40
39
 
41
- # @param allowed_errors [Array<Exception>]
42
- # @return [self]
43
40
  def with_allowed_errors(allowed_errors)
44
- @allowed_errors = allowed_errors.to_a
41
+ @allowed_errors = Default::ALLOWED_ERRORS + allowed_errors
45
42
  self
46
43
  end
47
44
 
48
- # @yield []
49
- # @return [self]
50
- def with_fallback(&fallback)
51
- @fallback = fallback.to_proc
45
+ def with_data_store(data_store)
46
+ @data_store = data_store
52
47
  self
53
48
  end
54
49
 
55
- # @param threshold [Integer]
56
- # @return [self]
57
- def with_threshold(threshold)
58
- Stoplight.data_store.set_threshold(name, threshold)
50
+ def with_error_notifier(&error_notifier)
51
+ @error_notifier = error_notifier
59
52
  self
60
53
  end
61
54
 
62
- # @param timeout [Integer]
63
- # @return [self]
64
- def with_timeout(timeout)
65
- Stoplight.data_store.set_timeout(name, timeout)
55
+ def with_fallback(&fallback)
56
+ @fallback = fallback
66
57
  self
67
58
  end
68
59
 
69
- # Attribute readers
70
-
71
- # @return [Object]
72
- # @raise [Error::RedLight]
73
- def fallback
74
- return @fallback if defined?(@fallback)
75
- fail Error::RedLight, name
76
- end
77
-
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?)
96
- def green?
97
- Stoplight.data_store.green?(name)
98
- end
99
-
100
- # @return (see Stoplight::DataStore::Base#yellow?)
101
- def yellow?
102
- Stoplight.data_store.yellow?(name)
103
- end
104
-
105
- # @return (see Stoplight::DataStore::Base#red?)
106
- def red?
107
- Stoplight.data_store.red?(name)
108
- end
109
-
110
- private
111
-
112
- def run_green
113
- code.call.tap { Stoplight.data_store.greenify(name) }
114
- rescue => error
115
- handle_error(error)
116
- raise
117
- end
118
-
119
- def run_yellow
120
- run_green.tap { notify(DataStore::COLOR_RED, DataStore::COLOR_GREEN) }
121
- end
122
-
123
- def run_red
124
- if Stoplight.data_store.record_attempt(name) == 1
125
- notify(DataStore::COLOR_GREEN, DataStore::COLOR_RED)
126
- end
127
- fallback.call
128
- end
129
-
130
- def handle_error(error)
131
- if error_allowed?(error)
132
- Stoplight.data_store.greenify(name)
133
- else
134
- Stoplight.data_store.record_failure(name, Failure.create(error))
135
- end
136
- end
137
-
138
- def error_allowed?(error)
139
- allowed_errors.any? { |klass| error.is_a?(klass) }
60
+ def with_notifiers(notifiers)
61
+ @notifiers = notifiers
62
+ self
140
63
  end
141
64
 
142
- def notify(from_color, to_color)
143
- Stoplight.notifiers.each do |notifier|
144
- begin
145
- notifier.notify(self, from_color, to_color)
146
- rescue Error::BadNotifier => error
147
- warn(error.cause)
148
- end
149
- end
65
+ def with_threshold(threshold)
66
+ @threshold = threshold
67
+ self
150
68
  end
151
69
 
152
- def sync
153
- Stoplight.data_store.sync(name)
154
- rescue Error::BadDataStore => error
155
- warn(error.cause)
156
- Stoplight.data_store = Stoplight::DataStore::Memory.new
157
- retry
70
+ def with_timeout(timeout)
71
+ @timeout = timeout
72
+ self
158
73
  end
159
74
  end
160
75
  end
@@ -0,0 +1,97 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ class Light
5
+ module Runnable
6
+ def color
7
+ failures, state = failures_and_state
8
+ now = Time.new
9
+
10
+ case
11
+ when state == State::LOCKED_GREEN then Color::GREEN
12
+ when state == State::LOCKED_RED then Color::RED
13
+ when failures.size < threshold then Color::GREEN
14
+ when failures.any? { |f| now - f.time >= timeout } then Color::YELLOW
15
+ else Color::RED
16
+ end
17
+ end
18
+
19
+ def run
20
+ case color
21
+ when Color::GREEN then run_green
22
+ when Color::YELLOW then run_yellow
23
+ else run_red
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def run_green
30
+ on_failure = lambda do |size, error|
31
+ notify(Color::GREEN, Color::RED, error) if size == threshold
32
+ end
33
+ run_code(nil, on_failure)
34
+ end
35
+
36
+ def run_yellow
37
+ on_success = lambda do |failures|
38
+ notify(Color::RED, Color::GREEN) unless failures.empty?
39
+ end
40
+ run_code(on_success, nil)
41
+ end
42
+
43
+ def run_red
44
+ fail Error::RedLight, name unless fallback
45
+ fallback.call(nil)
46
+ end
47
+
48
+ def run_code(on_success, on_failure)
49
+ result = code.call
50
+ failures = clear_failures
51
+ on_success.call(failures) if on_success
52
+ result
53
+ rescue => error
54
+ handle_error(error, on_failure)
55
+ end
56
+
57
+ def handle_error(error, on_failure)
58
+ fail error if allowed_errors.any? { |klass| error.is_a?(klass) }
59
+ size = record_failure(error)
60
+ on_failure.call(size, error) if on_failure
61
+ fail error unless fallback
62
+ fallback.call(error)
63
+ end
64
+
65
+ def clear_failures
66
+ safely([]) { data_store.clear_failures(self) }
67
+ end
68
+
69
+ def failures_and_state
70
+ safely([[], State::UNLOCKED]) { data_store.get_all(self) }
71
+ end
72
+
73
+ def notify(from_color, to_color, error = nil)
74
+ notifiers.each do |notifier|
75
+ safely { notifier.notify(self, from_color, to_color, error) }
76
+ end
77
+ end
78
+
79
+ def record_failure(error)
80
+ failure = Failure.from_error(error)
81
+ safely(0) { data_store.record_failure(self, failure) }
82
+ end
83
+
84
+ def safely(default = nil, &code)
85
+ return code.call if data_store == Default::DATA_STORE
86
+
87
+ self.class.new("#{name}-safely", &code)
88
+ .with_data_store(Default::DATA_STORE)
89
+ .with_fallback do |error|
90
+ error_notifier.call(error) if error
91
+ default
92
+ end
93
+ .run
94
+ end
95
+ end
96
+ end
97
+ end