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
@@ -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