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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 314c63808a80e427881c2d09152f74e68f4ffd37
4
- data.tar.gz: 67ab9ca03f20512f5b119b95eec17494d7fdf726
3
+ metadata.gz: ad268d747b85be7b3a0d773d9aeec70b1b22c25a
4
+ data.tar.gz: 56c5c8f88361a3c9d705212d9725251017097ddb
5
5
  SHA512:
6
- metadata.gz: 5a758d0cd6beeb2b0c034f424ec4b4b2fc1fe72f11a7eb598fe195e408437ec6ba3dd95aa9b29554d5a9e8a0727fabbfdc46aade4a7c1674cf457467459032c5
7
- data.tar.gz: 50eb0565526a389406422fdca3b93a9ce4923bd7ab7e50aa558b976ca09ed56ee976c9bcc3fc4b0f72c6a28290964e4a676d1c6dbb741ae45fed26b9f30619ab
6
+ metadata.gz: 043418c2e767031b2a229ad2e22d378c52101d342cdec3bb600bec88c5c46238c345eadb558d779ccedb8f60122b81b5cddf969dfb7dbf475e7bfe3e0cb7b3c2
7
+ data.tar.gz: 8a9952d663eb04f03e05736f3541ac09b69e69776c80265116d43a7c4c4e2e8d699691598ef8ae35d1c0018b5bc7880f73614894d79e194987961e857c2f7016
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ This project uses [Semantic Versioning][1].
4
+
5
+ - Data stores and notifiers can be configured on a per-stoplight basis. This
6
+ allows stoplights to use stoplights internally.
7
+ - Stoplights use stoplights internally to wrap calls to data stores and
8
+ notifiers. This means they gracefully handle either going down.
9
+ - Data stores only store failures and states. Also failures are stored in a ring
10
+ buffer. This drastically reduces the amount of data stored.
11
+ - Stoplights will use the fallback (if it's given) when they fail while they're
12
+ green. This means they won't re-raise exceptions if you provide a fallback.
13
+ - Stoplights pass the error to their notifiers when transitioning from green to
14
+ red.
15
+
3
16
  ## v0.4.1 (2014-10-03)
4
17
 
5
18
  - Fixed a bug that caused green to red notifications to sometimes not be sent.
@@ -62,3 +75,5 @@
62
75
  ## v0.1.0 (2014-08-12)
63
76
 
64
77
  - Initial release.
78
+
79
+ [1]: http://semver.org/spec/v2.0.0.html
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Cameron Desautels & Taylor Fausak
1
+ Copyright (c) 2014 Cameron Desautels, Taylor Fausak & Justin Steffy
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of
4
4
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -33,7 +33,7 @@ Check out [stoplight-admin][12] for controlling your stoplights.
33
33
  Add it to your Gemfile:
34
34
 
35
35
  ``` rb
36
- gem 'stoplight', '~> 0.4.1'
36
+ gem 'stoplight', '~> 0.5.0'
37
37
  ```
38
38
 
39
39
  Or install it manually:
@@ -42,8 +42,6 @@ Or install it manually:
42
42
  $ gem install stoplight
43
43
  ```
44
44
 
45
- This project uses [Semantic Versioning][13].
46
-
47
45
  ## Setup
48
46
 
49
47
  ### Data store
@@ -53,22 +51,22 @@ Stoplight uses an in-memory data store out of the box.
53
51
  ``` irb
54
52
  >> require 'stoplight'
55
53
  => true
56
- >> Stoplight.data_store
54
+ >> Stoplight::Light.default_data_store
57
55
  => #<Stoplight::DataStore::Memory:...>
58
56
  ```
59
57
 
60
58
  If you want to use a persistent data store, you'll have to set it up. Currently
61
59
  the only supported persistent data store is Redis. Make sure you have [the Redis
62
- gem][14] installed before configuring Stoplight.
60
+ gem][13] installed before configuring Stoplight.
63
61
 
64
62
  ``` irb
65
63
  >> require 'redis'
66
64
  => true
67
- >> redis = Redis.new(url: 'redis://127.0.0.1:6379/0')
68
- => #<Redis:...>
65
+ >> redis = Redis.new
66
+ => #<Redis client ...>
69
67
  >> data_store = Stoplight::DataStore::Redis.new(redis)
70
68
  => #<Stoplight::DataStore::Redis:...>
71
- >> Stoplight.data_store = data_store
69
+ >> Stoplight::Light.default_data_store = data_store
72
70
  => #<Stoplight::DataStore::Redis:...>
73
71
  ```
74
72
 
@@ -77,22 +75,22 @@ gem][14] installed before configuring Stoplight.
77
75
  Stoplight sends notifications to standard error by default.
78
76
 
79
77
  ``` irb
80
- >> Stoplight.notifiers
78
+ >> Stoplight::Light.default_notifiers
81
79
  => [#<Stoplight::Notifier::IO:...>]
82
80
  ```
83
81
 
84
82
  If you want to send notifications elsewhere, you'll have to set them up.
85
83
  Currently the only other supported notifier is HipChat. Make sure you have [the
86
- HipChat gem][15] installed before configuring Stoplight.
84
+ HipChat gem][14] installed before configuring Stoplight.
87
85
 
88
86
  ``` irb
89
87
  >> require 'hipchat'
90
88
  => true
91
- >> hipchat = HipChat::Client.new('token')
89
+ >> hip_chat = HipChat::Client.new('token')
92
90
  => #<HipChat::Client:...>
93
- >> notifier = Stoplight::Notifier::HipChat.new(hipchat, 'room')
91
+ >> notifier = Stoplight::Notifier::HipChat.new(hip_chat, 'room')
94
92
  => #<Stoplight::Notifier::HipChat:...>
95
- >> Stoplight.notifiers << notifier
93
+ >> Stoplight::Light.default_notifiers += [notifier]
96
94
  => [#<Stoplight::Notifier::IO:...>, #<Stoplight::Notifier::HipChat:...>]
97
95
  ```
98
96
 
@@ -106,8 +104,8 @@ Stoplight:
106
104
  ``` rb
107
105
  # config/initializers/stoplight.rb
108
106
  require 'stoplight'
109
- Stoplight.data_store = Stoplight::DataStore::Redis.new(...)
110
- Stoplight.notifiers << Stoplight::Notifier::HipChat.new(...)
107
+ Stoplight::Light.default_data_store = Stoplight::DataStore::Redis.new(...)
108
+ Stoplight::Light.default_notifiers += [Stoplight::Notifier::HipChat.new(...)]
111
109
  ```
112
110
 
113
111
  ## Basic usage
@@ -125,8 +123,8 @@ the green state.
125
123
  ``` irb
126
124
  >> light.run
127
125
  => 3.142857142857143
128
- >> light.green?
129
- => true
126
+ >> light.color
127
+ => "green"
130
128
  ```
131
129
 
132
130
  If everything goes well, you shouldn't even be able to tell that you're using a
@@ -147,12 +145,12 @@ ZeroDivisionError: divided by 0
147
145
  >> light.run
148
146
  ZeroDivisionError: divided by 0
149
147
  >> light.run
148
+ Switching example-2 from green to red because ZeroDivisionError divided by 0
150
149
  ZeroDivisionError: divided by 0
151
150
  >> light.run
152
- Switching example-2 from green to red.
153
151
  Stoplight::Error::RedLight: example-2
154
- >> light.red?
155
- => true
152
+ >> light.color
153
+ => "red"
156
154
  ```
157
155
 
158
156
  When the stoplight changes from green to red, it will notify every configured
@@ -165,7 +163,7 @@ these are handled elsewhere in your stack and don't represent real failures. A
165
163
  good example is `ActiveRecord::RecordNotFound`.
166
164
 
167
165
  ``` irb
168
- >> light = Stoplight::Light.new('example-4') { User.find(123) }.
166
+ >> light = Stoplight::Light.new('example-3') { User.find(123) }.
169
167
  .. with_allowed_errors([ActiveRecord::RecordNotFound])
170
168
  => #<Stoplight::Light:...>
171
169
  >> light.run
@@ -174,28 +172,34 @@ ActiveRecord::RecordNotFound: Couldn't find User with ID=123
174
172
  ActiveRecord::RecordNotFound: Couldn't find User with ID=123
175
173
  >> light.run
176
174
  ActiveRecord::RecordNotFound: Couldn't find User with ID=123
177
- >> light.green?
178
- => true
175
+ >> light.color
176
+ => "green"
179
177
  ```
180
178
 
181
179
  ### Custom fallback
182
180
 
183
- Instead of raising a `Stoplight::Error::RedLight` error when in the red state,
184
- you can provide a block to be run. This is useful when there's a good default
185
- value for the block.
181
+ By default, stoplights will re-raise errors when they're green. When they're
182
+ red, they'll raise a `Stoplight::Error::RedLight` error. You can provide a
183
+ fallback that will be called in both of these cases. It will be passed the error
184
+ if the light was green.
186
185
 
187
186
  ``` irb
188
- >> light = Stoplight::Light.new('example-5') { fail }.
189
- .. with_fallback { [] }
190
- => #<Stoplight::Light:...>
187
+ >> light = Stoplight::Light.new('example-4') { 1 / 0 }.
188
+ .. with_fallback { |e| p e; 'default' }
189
+ => #<Stoplight::Light:..>
191
190
  >> light.run
192
- RuntimeError:
191
+ #<ZeroDivisionError: divided by 0>
192
+ => "default"
193
193
  >> light.run
194
- RuntimeError:
194
+ #<ZeroDivisionError: divided by 0>
195
+ => "default"
195
196
  >> light.run
196
- RuntimeError:
197
+ Switching example-4 from green to red because ZeroDivisionError divided by 0
198
+ #<ZeroDivisionError: divided by 0>
199
+ => "default"
197
200
  >> light.run
198
- => []
201
+ nil
202
+ => "default"
199
203
  ```
200
204
 
201
205
  ### Custom threshold
@@ -204,13 +208,14 @@ Some bits of code might be allowed to fail more or less frequently than others.
204
208
  You can configure this by setting a custom threshold in seconds.
205
209
 
206
210
  ``` irb
207
- >> light = Stoplight::Light.new('example-6') { fail }.
211
+ >> light = Stoplight::Light.new('example-5') { fail }.
208
212
  .. with_threshold(1)
209
213
  => #<Stoplight::Light:...>
210
214
  >> light.run
215
+ Switching example-5 from green to red because RuntimeError
211
216
  RuntimeError:
212
217
  >> light.run
213
- Stoplight::Error::RedLight: example-6
218
+ Stoplight::Error::RedLight: example-5
214
219
  ```
215
220
 
216
221
  ### Custom timeout
@@ -220,7 +225,7 @@ A light in the red state for longer than the timeout will transition to the
220
225
  yellow state. This timeout is customizable.
221
226
 
222
227
  ``` irb
223
- >> light = Stoplight::Light.new('example-7') { fail }.
228
+ >> light = Stoplight::Light.new('example-6') { fail }.
224
229
  .. with_timeout(1)
225
230
  => #<Stoplight::Light:...>
226
231
  >> light.run
@@ -228,14 +233,12 @@ RuntimeError:
228
233
  >> light.run
229
234
  RuntimeError:
230
235
  >> light.run
236
+ Switching example-6 from green to red because RuntimeError
231
237
  RuntimeError:
232
- >> light.run
233
- Switching example-7 from green to red.
234
- Stoplight::Error::RedLight: example-7
235
238
  >> sleep(1)
236
239
  => 1
237
- >> light.yellow?
238
- => true
240
+ >> light.color
241
+ => "yellow"
239
242
  >> light.run
240
243
  RuntimeError:
241
244
  ```
@@ -254,7 +257,10 @@ class ApplicationController < ActionController::Base
254
257
  def stoplight(&block)
255
258
  Stoplight::Light.new("#{params[:controller]}##{params[:action]}", &block)
256
259
  .with_allowed_errors([ActiveRecord::RecordNotFound])
257
- .with_fallback { render(nothing: true, status: :service_unavailable) }
260
+ .with_fallback do |error|
261
+ Rails.logger.error(error)
262
+ render(nothing: true, status: :service_unavailable)
263
+ end
258
264
  .run
259
265
  end
260
266
  end
@@ -269,16 +275,14 @@ override the default behavior. You can lock a light in either the green or red
269
275
  state using `set_state`.
270
276
 
271
277
  ``` irb
272
- >> light = Stoplight::Light.new('example-8') { true }
273
- => #<Stoplight::Light:...>
278
+ >> light = Stoplight::Light.new('example-7') { true }
279
+ => #<Stoplight::Light:..>
274
280
  >> light.run
275
281
  => true
276
- >> Stoplight.data_store.set_state(
277
- .. light.name, Stoplight::DataStore::STATE_LOCKED_RED)
282
+ >> light.data_store.set_state(light, Stoplight::State::LOCKED_RED)
278
283
  => "locked_red"
279
284
  >> light.run
280
- Switching example-8 from green to red
281
- Stoplight::Error::RedLight: example-8
285
+ Stoplight::Error::RedLight: example-7
282
286
  ```
283
287
 
284
288
  **Code in locked red lights may still run under certain conditions!** If you
@@ -288,13 +292,13 @@ locked state of any stoplights.
288
292
 
289
293
  ## Credits
290
294
 
291
- Stoplight is brought to you by [@camdez][16] and [@tfausak][17] from
292
- [@OrgSync][18]. We were inspired by Martin Fowler's [CircuitBreaker][19]
295
+ Stoplight is brought to you by [@camdez][15] and [@tfausak][16] from
296
+ [@OrgSync][17]. We were inspired by Martin Fowler's [CircuitBreaker][18]
293
297
  article.
294
298
 
295
299
  If this gem isn't cutting it for you, there are a few alternatives, including:
296
- [circuit_b][20], [circuit_breaker][21], [simple_circuit_breaker][22], and
297
- [ya_circuit_breaker][23].
300
+ [circuit_b][19], [circuit_breaker][20], [simple_circuit_breaker][21], and
301
+ [ya_circuit_breaker][22].
298
302
 
299
303
  [1]: https://github.com/orgsync/stoplight
300
304
  [2]: https://badge.fury.io/rb/stoplight.svg
@@ -308,14 +312,13 @@ If this gem isn't cutting it for you, there are a few alternatives, including:
308
312
  [10]: https://gemnasium.com/orgsync/stoplight.svg
309
313
  [11]: https://gemnasium.com/orgsync/stoplight
310
314
  [12]: https://github.com/orgsync/stoplight-admin
311
- [13]: http://semver.org/spec/v2.0.0.html
312
- [14]: https://rubygems.org/gems/redis
313
- [15]: https://rubygems.org/gems/hipchat
314
- [16]: https://github.com/camdez
315
- [17]: https://github.com/tfausak
316
- [18]: https://github.com/OrgSync
317
- [19]: http://martinfowler.com/bliki/CircuitBreaker.html
318
- [20]: https://github.com/alg/circuit_b
319
- [21]: https://github.com/wsargent/circuit_breaker
320
- [22]: https://github.com/soundcloud/simple_circuit_breaker
321
- [23]: https://github.com/wooga/circuit_breaker
315
+ [13]: https://rubygems.org/gems/redis
316
+ [14]: https://rubygems.org/gems/hipchat
317
+ [15]: https://github.com/camdez
318
+ [16]: https://github.com/tfausak
319
+ [17]: https://github.com/OrgSync
320
+ [18]: http://martinfowler.com/bliki/CircuitBreaker.html
321
+ [19]: https://github.com/alg/circuit_b
322
+ [20]: https://github.com/wsargent/circuit_breaker
323
+ [21]: https://github.com/soundcloud/simple_circuit_breaker
324
+ [22]: https://github.com/wooga/circuit_breaker
data/lib/stoplight.rb CHANGED
@@ -1,29 +1,24 @@
1
1
  # coding: utf-8
2
2
 
3
+ require 'stoplight/color'
4
+ require 'stoplight/error'
5
+ require 'stoplight/failure'
6
+ require 'stoplight/state'
7
+
3
8
  require 'stoplight/data_store'
4
9
  require 'stoplight/data_store/base'
5
10
  require 'stoplight/data_store/memory'
6
11
  require 'stoplight/data_store/redis'
7
- require 'stoplight/error'
8
- require 'stoplight/failure'
9
- require 'stoplight/light'
12
+
10
13
  require 'stoplight/notifier'
11
14
  require 'stoplight/notifier/base'
12
15
  require 'stoplight/notifier/hip_chat'
13
16
  require 'stoplight/notifier/io'
14
17
 
15
- module Stoplight
16
- # @return [Gem::Version]
17
- VERSION = Gem::Version.new('0.4.1')
18
-
19
- @data_store = DataStore::Memory.new
20
- @notifiers = [Notifier::IO.new($stderr)]
18
+ require 'stoplight/default'
21
19
 
22
- class << self
23
- # @return [DataStore::Base]
24
- attr_accessor :data_store
20
+ require 'stoplight/light/runnable'
21
+ require 'stoplight/light'
25
22
 
26
- # @return [Array<Notifier::Base>]
27
- attr_accessor :notifiers
28
- end
23
+ module Stoplight
29
24
  end
@@ -0,0 +1,9 @@
1
+ # coding: utf-8
2
+
3
+ module Stoplight
4
+ module Color
5
+ GREEN = 'green'.freeze
6
+ YELLOW = 'yellow'.freeze
7
+ RED = 'red'.freeze
8
+ end
9
+ end
@@ -2,151 +2,5 @@
2
2
 
3
3
  module Stoplight
4
4
  module DataStore
5
- KEY_PREFIX = 'stoplight'.freeze
6
-
7
- COLOR_GREEN = 'green'.freeze
8
- COLOR_YELLOW = 'yellow'.freeze
9
- COLOR_RED = 'red'.freeze
10
- COLORS = Set.new([
11
- COLOR_GREEN,
12
- COLOR_YELLOW,
13
- COLOR_RED
14
- ]).freeze
15
-
16
- STATE_UNLOCKED = 'unlocked'.freeze
17
- STATE_LOCKED_GREEN = 'locked_green'.freeze
18
- STATE_LOCKED_RED = 'locked_red'.freeze
19
- STATES = Set.new([
20
- STATE_UNLOCKED,
21
- STATE_LOCKED_GREEN,
22
- STATE_LOCKED_RED
23
- ]).freeze
24
-
25
- DEFAULT_ATTEMPTS = 0
26
- DEFAULT_FAILURES = []
27
- DEFAULT_STATE = STATE_UNLOCKED
28
- DEFAULT_THRESHOLD = 3
29
- DEFAULT_TIMEOUT = 60
30
-
31
- module_function
32
-
33
- # @group Colors
34
-
35
- # @param state [String]
36
- # @param threshold [Integer]
37
- # @param failures [Array<Failure>]
38
- # @param timeout [Integer]
39
- # @return [String]
40
- def colorize(state, threshold, failures, timeout)
41
- case
42
- when state == STATE_LOCKED_GREEN then COLOR_GREEN
43
- when state == STATE_LOCKED_RED then COLOR_RED
44
- when failures.size < threshold then COLOR_GREEN
45
- when Time.now - failures.last.time > timeout then COLOR_YELLOW
46
- else COLOR_RED
47
- end
48
- end
49
-
50
- # @group Validation
51
-
52
- # @param color [String]
53
- # @raise [ArgumentError]
54
- def validate_color!(color)
55
- return if valid_color?(color)
56
- fail Error::InvalidColor, color.inspect
57
- end
58
-
59
- # @param color [String]
60
- # @return [Boolean]
61
- def valid_color?(color)
62
- COLORS.include?(color)
63
- end
64
-
65
- # @param failure [Failure]
66
- # @raise [ArgumentError]
67
- def validate_failure!(failure)
68
- return if valid_failure?(failure)
69
- fail Error::InvalidFailure, failure.inspect
70
- end
71
-
72
- # @param failure [Failure]
73
- # @return [Boolean]
74
- def valid_failure?(failure)
75
- failure.is_a?(Failure)
76
- end
77
-
78
- # @param state [String]
79
- # @raise [ArgumentError]
80
- def validate_state!(state)
81
- return if valid_state?(state)
82
- fail Error::InvalidState, state.inspect
83
- end
84
-
85
- # @param state [String]
86
- # @return [Boolean]
87
- def valid_state?(state)
88
- STATES.include?(state)
89
- end
90
-
91
- # @param threshold [Integer]
92
- # @raise [ArgumentError]
93
- def validate_threshold!(threshold)
94
- return if valid_threshold?(threshold)
95
- fail Error::InvalidThreshold, threshold.inspect
96
- end
97
-
98
- # @param threshold [Integer]
99
- # @return [Boolean]
100
- def valid_threshold?(threshold)
101
- threshold.is_a?(Integer) && threshold > 0
102
- end
103
-
104
- # @param timeout [Integer]
105
- # @raise [ArgumentError]
106
- def validate_timeout!(timeout)
107
- return if valid_timeout?(timeout)
108
- fail Error::InvalidTimeout, timeout.inspect
109
- end
110
-
111
- # @param timeout [Integer]
112
- # @return [Boolean]
113
- def valid_timeout?(timeout)
114
- timeout.is_a?(Integer)
115
- end
116
-
117
- # @group Keys
118
-
119
- # @return (see #key)
120
- def attempts_key
121
- key('attempts')
122
- end
123
-
124
- # @param name [String]
125
- # @return (see #key)
126
- def failures_key(name)
127
- key('failures', name)
128
- end
129
-
130
- # @return (see #key)
131
- def states_key
132
- key('states')
133
- end
134
-
135
- # @return (see #key)
136
- def thresholds_key
137
- key('thresholds')
138
- end
139
-
140
- # @return (see #key)
141
- def timeouts_key
142
- key('timeouts')
143
- end
144
-
145
- # @param slug [String]
146
- # @param suffix [String, nil]
147
- # @return [String]
148
- def key(slug, suffix = nil)
149
- [KEY_PREFIX, slug, suffix].compact.join(':')
150
- end
151
5
  end
152
6
  end