caddy 1.5.5 → 1.6.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.
- 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:
|