Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Tatuya Kamada
gitlab-ce
Commits
53783027
Commit
53783027
authored
Dec 17, 2016
by
Nick Thomas
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add a ReactiveCaching concern for use in the KubernetesService
parent
7c2e16d0
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
324 additions
and
0 deletions
+324
-0
app/models/concerns/reactive_caching.rb
app/models/concerns/reactive_caching.rb
+110
-0
app/workers/reactive_caching_worker.rb
app/workers/reactive_caching_worker.rb
+15
-0
config/sidekiq_queues.yml
config/sidekiq_queues.yml
+1
-0
spec/models/concerns/reactive_caching_spec.rb
spec/models/concerns/reactive_caching_spec.rb
+145
-0
spec/support/reactive_caching_helpers.rb
spec/support/reactive_caching_helpers.rb
+38
-0
spec/workers/reactive_caching_worker_spec.rb
spec/workers/reactive_caching_worker_spec.rb
+15
-0
No files found.
app/models/concerns/reactive_caching.rb
0 → 100644
View file @
53783027
# The ReactiveCaching concern is used to fetch some data in the background and
# store it in the Rails cache, keeping it up-to-date for as long as it is being
# requested. If the data hasn't been requested for +reactive_cache_lifetime+,
# it stop being refreshed, and then be removed.
#
# Example of use:
#
# class Foo < ActiveRecord::Base
# include ReactiveCaching
#
# self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
#
# after_save :clear_reactive_cache!
#
# def calculate_reactive_cache
# # Expensive operation here. The return value of this method is cached
# end
#
# def result
# with_reactive_cache do |data|
# # ...
# end
# end
# end
#
# In this example, the first time `#result` is called, it will return `nil`.
# However, it will enqueue a background worker to call `#calculate_reactive_cache`
# and set an initial cache lifetime of ten minutes.
#
# Each time the background job completes, it stores the return value of
# `#calculate_reactive_cache`. It is also re-enqueued to run again after
# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
# Calculations are never run concurrently.
#
# Calling `#result` while a value is in the cache will call the block given to
# `#with_reactive_cache`, yielding the cached value. It will also extend the
# lifetime by `reactive_cache_lifetime`.
#
# Once the lifetime has expired, no more background jobs will be enqueued and
# calling `#result` will again return `nil` - starting the process all over
# again
module
ReactiveCaching
extend
ActiveSupport
::
Concern
included
do
class_attribute
:reactive_cache_lease_timeout
class_attribute
:reactive_cache_key
class_attribute
:reactive_cache_lifetime
class_attribute
:reactive_cache_refresh_interval
# defaults
self
.
reactive_cache_lease_timeout
=
2
.
minutes
self
.
reactive_cache_refresh_interval
=
1
.
minute
self
.
reactive_cache_lifetime
=
10
.
minutes
def
with_reactive_cache
(
&
blk
)
within_reactive_cache_lifetime
do
data
=
Rails
.
cache
.
read
(
full_reactive_cache_key
)
yield
data
if
data
.
present?
end
ensure
Rails
.
cache
.
write
(
full_reactive_cache_key
(
'alive'
),
true
,
expires_in:
self
.
class
.
reactive_cache_lifetime
)
ReactiveCachingWorker
.
perform_async
(
self
.
class
,
id
)
end
def
clear_reactive_cache!
Rails
.
cache
.
delete
(
full_reactive_cache_key
)
end
def
exclusively_update_reactive_cache!
locking_reactive_cache
do
within_reactive_cache_lifetime
do
enqueuing_update
do
value
=
calculate_reactive_cache
Rails
.
cache
.
write
(
full_reactive_cache_key
,
value
)
end
end
end
end
private
def
full_reactive_cache_key
(
*
qualifiers
)
prefix
=
self
.
class
.
reactive_cache_key
prefix
=
prefix
.
call
(
self
)
if
prefix
.
respond_to?
(
:call
)
([
prefix
].
flatten
+
qualifiers
).
join
(
':'
)
end
def
locking_reactive_cache
lease
=
Gitlab
::
ExclusiveLease
.
new
(
full_reactive_cache_key
,
timeout:
reactive_cache_lease_timeout
)
uuid
=
lease
.
try_obtain
yield
if
uuid
ensure
Gitlab
::
ExclusiveLease
.
cancel
(
full_reactive_cache_key
,
uuid
)
end
def
within_reactive_cache_lifetime
yield
if
Rails
.
cache
.
read
(
full_reactive_cache_key
(
'alive'
))
end
def
enqueuing_update
yield
ensure
ReactiveCachingWorker
.
perform_in
(
self
.
class
.
reactive_cache_refresh_interval
,
self
.
class
,
id
)
end
end
end
app/workers/reactive_caching_worker.rb
0 → 100644
View file @
53783027
class
ReactiveCachingWorker
include
Sidekiq
::
Worker
include
DedicatedSidekiqQueue
def
perform
(
class_name
,
id
)
klass
=
begin
Kernel
.
const_get
(
class_name
)
rescue
NameError
nil
end
return
unless
klass
klass
.
find_by
(
id:
id
).
try
(
:exclusively_update_reactive_cache!
)
end
end
config/sidekiq_queues.yml
View file @
53783027
...
@@ -46,5 +46,6 @@
...
@@ -46,5 +46,6 @@
- [repository_check, 1]
- [repository_check, 1]
- [system_hook, 1]
- [system_hook, 1]
- [git_garbage_collect, 1]
- [git_garbage_collect, 1]
- [reactive_caching, 1]
- [cronjob, 1]
- [cronjob, 1]
- [default, 1]
- [default, 1]
spec/models/concerns/reactive_caching_spec.rb
0 → 100644
View file @
53783027
require
'spec_helper'
describe
ReactiveCaching
,
caching:
true
do
include
ReactiveCachingHelpers
class
CacheTest
include
ReactiveCaching
self
.
reactive_cache_key
=
->
(
thing
)
{
[
"foo"
,
thing
.
id
]
}
self
.
reactive_cache_lifetime
=
5
.
minutes
self
.
reactive_cache_refresh_interval
=
15
.
seconds
attr_reader
:id
def
initialize
(
id
,
&
blk
)
@id
=
id
@calculator
=
blk
end
def
calculate_reactive_cache
@calculator
.
call
end
def
result
with_reactive_cache
do
|
data
|
data
/
2
end
end
end
let
(
:now
)
{
Time
.
now
.
utc
}
around
(
:each
)
do
|
example
|
Timecop
.
freeze
(
now
)
{
example
.
run
}
end
let
(
:calculation
)
{
->
{
2
+
2
}
}
let
(
:cache_key
)
{
"foo:666"
}
let
(
:instance
)
{
CacheTest
.
new
(
666
,
&
calculation
)
}
describe
'#with_reactive_cache'
do
before
{
stub_reactive_cache
}
subject
(
:go!
)
{
instance
.
result
}
context
'when cache is empty'
do
it
{
is_expected
.
to
be_nil
}
it
'queues a background worker'
do
expect
(
ReactiveCachingWorker
).
to
receive
(
:perform_async
).
with
(
CacheTest
,
666
)
go!
end
it
'updates the cache lifespan'
do
go!
expect
(
reactive_cache_alive?
(
instance
)).
to
be_truthy
end
end
context
'when the cache is full'
do
before
{
stub_reactive_cache
(
instance
,
4
)
}
it
{
is_expected
.
to
eq
(
2
)
}
context
'and expired'
do
before
{
invalidate_reactive_cache
(
instance
)
}
it
{
is_expected
.
to
be_nil
}
end
end
end
describe
'#clear_reactive_cache!'
do
before
do
stub_reactive_cache
(
instance
,
4
)
instance
.
clear_reactive_cache!
end
it
{
expect
(
instance
.
result
).
to
be_nil
}
end
describe
'#exclusively_update_reactive_cache!'
do
subject
(
:go!
)
{
instance
.
exclusively_update_reactive_cache!
}
context
'when the lease is free and lifetime is not exceeded'
do
before
{
stub_reactive_cache
(
instance
,
"preexisting"
)
}
it
'takes and releases the lease'
do
expect_any_instance_of
(
Gitlab
::
ExclusiveLease
).
to
receive
(
:try_obtain
).
and_return
(
"000000"
)
expect
(
Gitlab
::
ExclusiveLease
).
to
receive
(
:cancel
).
with
(
cache_key
,
"000000"
)
go!
end
it
'caches the result of #calculate_reactive_cache'
do
go!
expect
(
read_reactive_cache
(
instance
)).
to
eq
(
calculation
.
call
)
end
it
"enqueues a repeat worker"
do
expect_reactive_cache_update_queued
(
instance
)
go!
end
context
'and #calculate_reactive_cache raises an exception'
do
before
{
stub_reactive_cache
(
instance
,
"preexisting"
)
}
let
(
:calculation
)
{
->
{
raise
"foo"
}
}
it
'leaves the cache untouched'
do
expect
{
go!
}.
to
raise_error
(
"foo"
)
expect
(
read_reactive_cache
(
instance
)).
to
eq
(
"preexisting"
)
end
it
'enqueues a repeat worker'
do
expect_reactive_cache_update_queued
(
instance
)
expect
{
go!
}.
to
raise_error
(
"foo"
)
end
end
end
context
'when lifetime is exceeded'
do
it
'skips the calculation'
do
expect
(
instance
).
to
receive
(
:calculate_reactive_cache
).
never
go!
end
end
context
'when the lease is already taken'
do
before
do
expect_any_instance_of
(
Gitlab
::
ExclusiveLease
).
to
receive
(
:try_obtain
).
and_return
(
nil
)
end
it
'skips the calculation'
do
expect
(
instance
).
to
receive
(
:calculate_reactive_cache
).
never
go!
end
end
end
end
spec/support/reactive_caching_helpers.rb
0 → 100644
View file @
53783027
module
ReactiveCachingHelpers
def
reactive_cache_key
(
subject
,
*
qualifiers
)
([
subject
.
class
.
reactive_cache_key
.
call
(
subject
)].
flatten
+
qualifiers
).
join
(
':'
)
end
def
stub_reactive_cache
(
subject
=
nil
,
data
=
nil
)
allow
(
ReactiveCachingWorker
).
to
receive
(
:perform_async
)
allow
(
ReactiveCachingWorker
).
to
receive
(
:perform_in
)
write_reactive_cache
(
subject
,
data
)
if
data
end
def
read_reactive_cache
(
subject
)
Rails
.
cache
.
read
(
reactive_cache_key
(
subject
))
end
def
write_reactive_cache
(
subject
,
data
)
start_reactive_cache_lifetime
(
subject
)
Rails
.
cache
.
write
(
reactive_cache_key
(
subject
),
data
)
end
def
reactive_cache_alive?
(
subject
)
Rails
.
cache
.
read
(
reactive_cache_key
(
subject
,
'alive'
))
end
def
invalidate_reactive_cache
(
subject
)
Rails
.
cache
.
delete
(
reactive_cache_key
(
subject
,
'alive'
))
end
def
start_reactive_cache_lifetime
(
subject
)
Rails
.
cache
.
write
(
reactive_cache_key
(
subject
,
'alive'
),
true
)
end
def
expect_reactive_cache_update_queued
(
subject
)
expect
(
ReactiveCachingWorker
).
to
receive
(
:perform_in
).
with
(
subject
.
class
.
reactive_cache_refresh_interval
,
subject
.
class
,
subject
.
id
)
end
end
spec/workers/reactive_caching_worker_spec.rb
0 → 100644
View file @
53783027
require
'spec_helper'
describe
ReactiveCachingWorker
do
let
(
:project
)
{
create
(
:kubernetes_project
)
}
let
(
:service
)
{
project
.
deployment_service
}
subject
{
described_class
.
new
.
perform
(
"KubernetesService"
,
service
.
id
)
}
describe
'#perform'
do
it
'calls #exclusively_update_reactive_cache!'
do
expect_any_instance_of
(
KubernetesService
).
to
receive
(
:exclusively_update_reactive_cache!
)
subject
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment