caddy 1.5.5 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/bin/console +7 -0
- data/bin/setup +5 -0
- data/lib/caddy.rb +41 -13
- data/lib/caddy/cache.rb +44 -11
- data/lib/caddy/task_observer.rb +16 -10
- data/lib/caddy/version.rb +2 -1
- data/test/caddy_test.rb +48 -8
- data/test/test_helper.rb +10 -0
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed9c2bf138fdfa093e81a888f423ade1a55890af
|
4
|
+
data.tar.gz: 0cb897a71cb7bb5345059a80c56c9f93cb25e29d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ebff58f7afbfcaa321a3f69158c92636cd545946df87d11eb660911f54ae938256fcb3c7b77b94793296dc40db66aad3a654dc0ee3563e82ad335970f9bf824
|
7
|
+
data.tar.gz: b6f5786869e114c14d3374f086c7b439c6312b637ee45079d7fd7f8d99dcb012804a64fb000df9409b29220db793ffa1fb2f336fe247fa57e3aa7db6f1baf700
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/caddy.rb
CHANGED
@@ -5,23 +5,31 @@ require "caddy/version"
|
|
5
5
|
require "caddy/task_observer"
|
6
6
|
require "caddy/cache"
|
7
7
|
|
8
|
+
# Caddy gives you a auto-updating global cache to speed up requests
|
8
9
|
module Caddy
|
9
10
|
class << self
|
11
|
+
# @!attribute error_handler
|
12
|
+
# @return [Proc] called when any cache refresher throws an exception (or times out)
|
10
13
|
attr_accessor :error_handler
|
14
|
+
|
15
|
+
# see {#logger}
|
16
|
+
attr_writer :logger
|
11
17
|
end
|
12
18
|
|
13
19
|
@started_pid = nil
|
14
20
|
@caches = Hash.new { |h, k| h[k] = Caddy::Cache.new(k) }
|
21
|
+
@needs_fork_check = !Concurrent::TimerSet.private_method_defined?(:ns_reset_if_forked)
|
15
22
|
|
16
|
-
|
17
|
-
# Returns the cache object for key +k+.
|
23
|
+
# Returns the cache object at a key.
|
18
24
|
#
|
19
25
|
# If the cache at +k+ does not exist yet, Caddy will initialize an empty one.
|
26
|
+
#
|
27
|
+
# @param k [Symbol] the cache key.
|
28
|
+
# @return [Caddy::Cache] the cache object at key +k+.
|
20
29
|
def self.[](k)
|
21
30
|
@caches[k]
|
22
31
|
end
|
23
32
|
|
24
|
-
##
|
25
33
|
# Starts the Caddy refresh processes for all caches.
|
26
34
|
#
|
27
35
|
# If the refresh process was started pre-fork, Caddy will error out, as this means
|
@@ -30,27 +38,47 @@ module Caddy
|
|
30
38
|
# Caddy freezes the hash of caches at this point, so no more further caches can be
|
31
39
|
# added after start.
|
32
40
|
def self.start
|
33
|
-
if
|
34
|
-
|
35
|
-
elsif @started_pid && $$ != @started_pid
|
36
|
-
raise "Please run `Caddy.start` *after* forking, as the refresh thread will get killed after fork"
|
37
|
-
end
|
38
|
-
|
39
|
-
@caches.freeze
|
41
|
+
raise_if_forked if @needs_fork_check
|
42
|
+
logger.info "Starting Caddy with refreshers: #{@caches.keys.join(', ')}"
|
40
43
|
|
41
44
|
@caches.values.each(&:start).all?
|
42
45
|
end
|
43
46
|
|
44
|
-
##
|
45
47
|
# Cleanly shut down all currently running refreshers.
|
46
48
|
def self.stop
|
49
|
+
logger.info "Stopping Caddy refreshers"
|
50
|
+
|
47
51
|
@caches.values.each(&:stop).all?
|
48
52
|
end
|
49
53
|
|
50
|
-
|
51
|
-
# Start and then stop again all refreshers. Useful for triggering an immediate refresh of all caches.
|
54
|
+
# Start and then stop all refreshers. Useful for triggering an immediate refresh of all caches.
|
52
55
|
def self.restart
|
53
56
|
stop
|
54
57
|
start
|
55
58
|
end
|
59
|
+
|
60
|
+
# @!attribute logger
|
61
|
+
# @return [Logger] logger used for all non-fatals; defaults to the Rails logger if it exists
|
62
|
+
def self.logger
|
63
|
+
@logger ||= begin
|
64
|
+
if defined?(Rails.logger)
|
65
|
+
Rails.logger
|
66
|
+
else
|
67
|
+
@logger ||= Logger.new(STDOUT).tap do |logger|
|
68
|
+
logger.formatter = -> (_, datetime, _, msg) { "#{datetime}: #{msg}\n" }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# TODO: when https://github.com/ruby-concurrency/concurrent-ruby/pull/573 is merged, remove this whole block
|
75
|
+
# or add a warning
|
76
|
+
# @private
|
77
|
+
def self.raise_if_forked
|
78
|
+
if !@started_pid
|
79
|
+
@started_pid = $$
|
80
|
+
elsif @started_pid && $$ != @started_pid
|
81
|
+
raise "Please run `Caddy.start` *after* forking, as the refresh thread will get killed after fork"
|
82
|
+
end
|
83
|
+
end
|
56
84
|
end
|
data/lib/caddy/cache.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Caddy
|
3
3
|
class Cache
|
4
|
+
# Default refresh interval, in seconds
|
4
5
|
DEFAULT_REFRESH_INTERVAL = 60
|
6
|
+
|
7
|
+
# Percentage to randomly smooth the refresh interval to avoid stampeding herd on expiration
|
5
8
|
REFRESH_INTERVAL_JITTER_PCT = 0.15
|
6
9
|
|
7
|
-
|
10
|
+
# @!attribute refresher
|
11
|
+
# @return [Proc] called on interval {#refresh_interval} with the returned object used as the cache
|
12
|
+
attr_accessor :refresher
|
13
|
+
|
14
|
+
# @!attribute refresh_interval
|
15
|
+
# @return [Numeric] number of seconds between calls to {#refresher}; timeout is set to <tt>{#refresher} - 0.1</tt>
|
16
|
+
attr_accessor :refresh_interval
|
8
17
|
|
9
|
-
|
10
|
-
#
|
18
|
+
# @!attribute error_handler
|
19
|
+
# @return [Proc] if unset, defaults to the global error handler (see #{Caddy.error_handler});
|
20
|
+
# called when exceptions or timeouts happen within the refresher
|
21
|
+
attr_accessor :error_handler
|
22
|
+
|
23
|
+
# Create a new periodically updated cache.
|
24
|
+
# @param key [Symbol] the name of this cache
|
11
25
|
def initialize(key)
|
12
26
|
@task = nil
|
13
27
|
@refresh_interval = DEFAULT_REFRESH_INTERVAL
|
@@ -15,25 +29,28 @@ module Caddy
|
|
15
29
|
@key = key
|
16
30
|
end
|
17
31
|
|
18
|
-
##
|
19
32
|
# Convenience method for getting the value of the refresher-returned object at path +k+,
|
20
33
|
# assuming the refresher-returned value responds to <tt>[]</tt>.
|
21
34
|
#
|
22
|
-
# If not, #cache can be used instead to access the refresher-returned object.
|
35
|
+
# If not, {#cache} can be used instead to access the refresher-returned object.
|
36
|
+
# @param k key to access from the refresher-returned cache.
|
23
37
|
def [](k)
|
24
38
|
cache[k]
|
25
39
|
end
|
26
40
|
|
27
|
-
##
|
28
41
|
# Returns the refresher-produced value that is used as the cache.
|
29
42
|
def cache
|
30
43
|
raise "Please run `Caddy.start` before attempting to access the cache" unless @task && @task.running?
|
31
|
-
|
44
|
+
|
45
|
+
unless @cache
|
46
|
+
logger.warn "Caddy cache access of :#{@key} before initial load; doing synchronous load."\
|
47
|
+
"Please allow some more time for your app to start up."
|
48
|
+
refresh
|
49
|
+
end
|
32
50
|
|
33
51
|
@cache
|
34
52
|
end
|
35
53
|
|
36
|
-
##
|
37
54
|
# Starts the period refresh cycle.
|
38
55
|
#
|
39
56
|
# Every +refresh_interval+ seconds -- smoothed by a jitter amount (a random amount +/- +REFRESH_INTERVAL_JITTER_PCT+) --
|
@@ -58,22 +75,38 @@ module Caddy
|
|
58
75
|
execution_interval: interval,
|
59
76
|
timeout_interval: timeout_interval
|
60
77
|
) do
|
61
|
-
|
62
|
-
nil # no need to
|
78
|
+
refresh
|
79
|
+
nil # no need for the {#Concurrent::TimerTask} to keep a reference to the value
|
63
80
|
end
|
64
81
|
|
65
82
|
@task.add_observer(Caddy::TaskObserver.new(error_handler, @key))
|
83
|
+
|
84
|
+
logger.debug "Starting Caddy refresher for :#{@key}, updating every #{interval.round(1)}s."
|
85
|
+
|
66
86
|
@task.execute
|
67
87
|
|
68
88
|
@task.running?
|
69
89
|
end
|
70
90
|
|
71
|
-
##
|
72
91
|
# Stops the current executing refresher.
|
73
92
|
#
|
74
93
|
# The current cache value is persisted even if the task is stopped.
|
75
94
|
def stop
|
76
95
|
@task.shutdown if @task && @task.running?
|
77
96
|
end
|
97
|
+
|
98
|
+
# Updates the internal cache object.
|
99
|
+
#
|
100
|
+
# Freezes the result to avoid mutation errors.
|
101
|
+
def refresh
|
102
|
+
@cache = refresher.call.freeze
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Delegates logging to the module logger
|
108
|
+
def logger
|
109
|
+
Caddy.logger
|
110
|
+
end
|
78
111
|
end
|
79
112
|
end
|
data/lib/caddy/task_observer.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Caddy
|
3
|
-
|
3
|
+
# {TaskObserver} is used internally to monitor the status of the running refreshers
|
4
|
+
# @private
|
5
|
+
class TaskObserver
|
4
6
|
def initialize(error_handler, cache_name)
|
5
7
|
@error_handler = error_handler || Caddy.error_handler
|
6
8
|
@cache_name = cache_name
|
@@ -14,28 +16,32 @@ module Caddy
|
|
14
16
|
begin
|
15
17
|
@error_handler.call(boom)
|
16
18
|
rescue => incepted_boom
|
17
|
-
|
19
|
+
log_exception("Caddy error handler itself errored handling refresh for :#{@cache_name}", incepted_boom)
|
18
20
|
end
|
19
21
|
else
|
20
22
|
# rubocop:disable Style/StringLiterals
|
21
|
-
|
22
|
-
|
23
|
+
logger.error 'Caddy error handler not callable. Please set the error handler like:'\
|
24
|
+
' `Caddy.error_handler = -> (e) { puts "#{e}" }`'
|
23
25
|
# rubocop:enable Style/StringLiterals
|
24
26
|
|
25
|
-
|
27
|
+
log_exception("Caddy refresher for :#{@cache_name} failed with error", boom)
|
26
28
|
end
|
27
29
|
elsif boom.is_a?(Concurrent::TimeoutError)
|
28
|
-
|
30
|
+
logger.error "Caddy refresher for :#{@cache_name} timed out"
|
29
31
|
else
|
30
|
-
|
32
|
+
log_exception("Caddy refresher for :#{@cache_name} failed with error", boom)
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
34
36
|
private
|
35
37
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
38
|
+
def log_exception(msg, boom)
|
39
|
+
logger.error "\n#{msg}: #{boom}"
|
40
|
+
logger.error "\t#{boom.backtrace.join("\n\t")}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger
|
44
|
+
Caddy.logger
|
39
45
|
end
|
40
46
|
end
|
41
47
|
end
|
data/lib/caddy/version.rb
CHANGED
data/test/caddy_test.rb
CHANGED
@@ -14,6 +14,14 @@ class CaddyTest < Minitest::Test
|
|
14
14
|
sleep(0.05)
|
15
15
|
end
|
16
16
|
|
17
|
+
def with_test_logger
|
18
|
+
sio = StringIO.new
|
19
|
+
Caddy.logger = Logger.new(sio)
|
20
|
+
yield
|
21
|
+
Caddy.logger = $test_logger
|
22
|
+
sio.string
|
23
|
+
end
|
24
|
+
|
17
25
|
def test_basic_lookup
|
18
26
|
Caddy[:test].refresher = -> { {foo: "bar"} }
|
19
27
|
Caddy.start
|
@@ -122,15 +130,23 @@ class CaddyTest < Minitest::Test
|
|
122
130
|
def test_incepted_error_handling
|
123
131
|
Caddy[:test].refresher = -> { raise "boom" }
|
124
132
|
Caddy.error_handler = -> (_) { raise "boomboom" }
|
125
|
-
|
126
|
-
|
133
|
+
log_output = with_test_logger do
|
134
|
+
Caddy.start
|
135
|
+
sleep(0.1)
|
136
|
+
end
|
137
|
+
|
138
|
+
assert_match(/Caddy error handler itself errored/, log_output)
|
127
139
|
end
|
128
140
|
|
129
141
|
def test_bad_error_handler
|
130
142
|
Caddy[:test].refresher = -> { raise "boom" }
|
131
143
|
Caddy.error_handler = "no"
|
132
|
-
|
133
|
-
|
144
|
+
log_output = with_test_logger do
|
145
|
+
Caddy.start
|
146
|
+
sleep(0.1)
|
147
|
+
end
|
148
|
+
|
149
|
+
assert_match(/Caddy error handler not callable/, log_output)
|
134
150
|
end
|
135
151
|
|
136
152
|
def test_timeout
|
@@ -149,16 +165,27 @@ class CaddyTest < Minitest::Test
|
|
149
165
|
def test_no_handler_timeout
|
150
166
|
Caddy[:test].refresher = -> { sleep 1 }
|
151
167
|
Caddy[:test].refresh_interval = 0.5
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
168
|
+
|
169
|
+
log_output = with_test_logger do
|
170
|
+
Caddy.start
|
171
|
+
sleep(2)
|
172
|
+
Caddy.stop
|
173
|
+
sleep(2)
|
174
|
+
end
|
175
|
+
|
176
|
+
assert_match(/timed out/, log_output)
|
156
177
|
end
|
157
178
|
|
158
179
|
def test_no_handler
|
159
180
|
Caddy[:test].refresher = -> { raise "boom" }
|
160
181
|
Caddy.start
|
161
182
|
sleep(0.1)
|
183
|
+
log_output = with_test_logger do
|
184
|
+
Caddy.start
|
185
|
+
sleep(0.1)
|
186
|
+
end
|
187
|
+
|
188
|
+
assert_match(/failed with error/, log_output)
|
162
189
|
end
|
163
190
|
|
164
191
|
def test_requires_refesher
|
@@ -173,6 +200,19 @@ class CaddyTest < Minitest::Test
|
|
173
200
|
assert_raises { Caddy[:nope][:not_there] }
|
174
201
|
end
|
175
202
|
|
203
|
+
def test_does_synchronous_load
|
204
|
+
Caddy[:sync].refresher = -> { sleep 1; "sync" }
|
205
|
+
|
206
|
+
assert_raises { Caddy[:sync].cache }
|
207
|
+
|
208
|
+
log_output = with_test_logger do
|
209
|
+
Caddy.start
|
210
|
+
assert_equal "sync", Caddy[:sync].cache
|
211
|
+
end
|
212
|
+
|
213
|
+
assert_match(/doing synchronous load./, log_output)
|
214
|
+
end
|
215
|
+
|
176
216
|
def test_requires_positive_interval
|
177
217
|
Caddy[:test].refresh_interval = -2
|
178
218
|
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: caddy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Elser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -72,7 +72,9 @@ description: |2
|
|
72
72
|
Caddy is great for storing information like feature flags -- accessed extremely frequently during many requests, updated relatively rarely and usually safe to be stale by some small duration.
|
73
73
|
email:
|
74
74
|
- nick.elser@gmail.com
|
75
|
-
executables:
|
75
|
+
executables:
|
76
|
+
- console
|
77
|
+
- setup
|
76
78
|
extensions: []
|
77
79
|
extra_rdoc_files: []
|
78
80
|
files:
|
@@ -80,11 +82,14 @@ files:
|
|
80
82
|
- ".gitignore"
|
81
83
|
- ".rubocop.yml"
|
82
84
|
- ".travis.yml"
|
85
|
+
- ".yardopts"
|
83
86
|
- CHANGELOG.md
|
84
87
|
- Gemfile
|
85
88
|
- LICENSE.txt
|
86
89
|
- README.md
|
87
90
|
- Rakefile
|
91
|
+
- bin/console
|
92
|
+
- bin/setup
|
88
93
|
- caddy.gemspec
|
89
94
|
- docs/architecture.dot
|
90
95
|
- docs/architecture.svg
|
@@ -121,4 +126,3 @@ summary: Caddy gives you a auto-updating global cache to speed up requests.
|
|
121
126
|
test_files:
|
122
127
|
- test/caddy_test.rb
|
123
128
|
- test/test_helper.rb
|
124
|
-
has_rdoc:
|