copycopter_client 1.0.2 → 1.0.3
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.
- data/lib/copycopter_client/cache.rb +106 -0
- data/lib/copycopter_client/configuration.rb +12 -8
- data/lib/copycopter_client/errors.rb +1 -1
- data/lib/copycopter_client/i18n_backend.rb +12 -12
- data/lib/copycopter_client/poller.rb +44 -0
- data/lib/copycopter_client/process_guard.rb +92 -0
- data/lib/copycopter_client/request_sync.rb +5 -5
- data/lib/copycopter_client/version.rb +1 -1
- data/lib/copycopter_client.rb +9 -7
- data/spec/copycopter_client/cache_spec.rb +207 -0
- data/spec/copycopter_client/configuration_spec.rb +23 -10
- data/spec/copycopter_client/i18n_backend_spec.rb +22 -22
- data/spec/copycopter_client/poller_spec.rb +110 -0
- data/spec/copycopter_client/process_guard_spec.rb +118 -0
- data/spec/copycopter_client/request_sync_spec.rb +5 -5
- data/spec/support/fake_client.rb +7 -1
- data/spec/support/fake_resque_job.rb +12 -5
- data/spec/support/writing_cache.rb +17 -0
- metadata +18 -13
- data/lib/copycopter_client/sync.rb +0 -182
- data/spec/copycopter_client/sync_spec.rb +0 -415
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'copycopter_client/client'
|
3
|
+
|
4
|
+
module CopycopterClient
|
5
|
+
# Manages synchronization of copy between {I18nBackend} and {Client}. Acts
|
6
|
+
# like a Hash. Applications using the client will not need to interact with
|
7
|
+
# this class directly.
|
8
|
+
#
|
9
|
+
# Responsible for locking down access to data used by both threads.
|
10
|
+
class Cache
|
11
|
+
# Usually instantiated when {Configuration#apply} is invoked.
|
12
|
+
# @param client [Client] the client used to fetch and upload data
|
13
|
+
# @param options [Hash]
|
14
|
+
# @option options [Logger] :logger where errors should be logged
|
15
|
+
def initialize(client, options)
|
16
|
+
@client = client
|
17
|
+
@blurbs = {}
|
18
|
+
@queued = {}
|
19
|
+
@mutex = Mutex.new
|
20
|
+
@logger = options[:logger]
|
21
|
+
@started = false
|
22
|
+
@downloaded = false
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns content for the given blurb.
|
26
|
+
# @param key [String] the key of the desired blurb
|
27
|
+
# @return [String] the contents of the blurb
|
28
|
+
def [](key)
|
29
|
+
lock { @blurbs[key] }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Sets content for the given blurb. The content will be pushed to the
|
33
|
+
# server on the next flush.
|
34
|
+
# @param key [String] the key of the blurb to update
|
35
|
+
# @param value [String] the new contents of the blurb
|
36
|
+
def []=(key, value)
|
37
|
+
lock { @queued[key] = value }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Keys for all blurbs stored on the server.
|
41
|
+
# @return [Array<String>] keys
|
42
|
+
def keys
|
43
|
+
lock { @blurbs.keys }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Waits until the first download has finished.
|
47
|
+
def wait_for_download
|
48
|
+
if pending?
|
49
|
+
logger.info("Waiting for first download")
|
50
|
+
logger.flush if logger.respond_to?(:flush)
|
51
|
+
while pending?
|
52
|
+
sleep(0.1)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def flush
|
58
|
+
with_queued_changes do |queued|
|
59
|
+
client.upload(queued)
|
60
|
+
end
|
61
|
+
rescue ConnectionError => error
|
62
|
+
logger.error(error.message)
|
63
|
+
end
|
64
|
+
|
65
|
+
def download
|
66
|
+
@started = true
|
67
|
+
client.download do |downloaded_blurbs|
|
68
|
+
downloaded_blurbs.reject! { |key, value| value == "" }
|
69
|
+
lock { @blurbs = downloaded_blurbs }
|
70
|
+
end
|
71
|
+
rescue ConnectionError => error
|
72
|
+
logger.error(error.message)
|
73
|
+
ensure
|
74
|
+
@downloaded = true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Downloads and then flushes
|
78
|
+
def sync
|
79
|
+
download
|
80
|
+
flush
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
attr_reader :client, :logger
|
86
|
+
|
87
|
+
def with_queued_changes
|
88
|
+
changes_to_push = nil
|
89
|
+
lock do
|
90
|
+
unless @queued.empty?
|
91
|
+
changes_to_push = @queued
|
92
|
+
@queued = {}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
yield(changes_to_push) if changes_to_push
|
96
|
+
end
|
97
|
+
|
98
|
+
def lock(&block)
|
99
|
+
@mutex.synchronize(&block)
|
100
|
+
end
|
101
|
+
|
102
|
+
def pending?
|
103
|
+
@started && !@downloaded
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'logger'
|
2
2
|
require 'copycopter_client/i18n_backend'
|
3
3
|
require 'copycopter_client/client'
|
4
|
-
require 'copycopter_client/
|
4
|
+
require 'copycopter_client/cache'
|
5
|
+
require 'copycopter_client/process_guard'
|
6
|
+
require 'copycopter_client/poller'
|
5
7
|
require 'copycopter_client/prefixed_logger'
|
6
8
|
require 'copycopter_client/request_sync'
|
7
9
|
|
@@ -156,20 +158,22 @@ module CopycopterClient
|
|
156
158
|
#
|
157
159
|
# Called automatically when {CopycopterClient.configure} is called in the application.
|
158
160
|
#
|
159
|
-
# This creates the {Client}, {
|
161
|
+
# This creates the {Client}, {Cache}, and {I18nBackend} and puts them together.
|
160
162
|
#
|
161
|
-
# When {#test?} returns +false+, the
|
163
|
+
# When {#test?} returns +false+, the poller will be started.
|
162
164
|
def apply
|
163
165
|
client = Client.new(to_hash)
|
164
|
-
|
165
|
-
|
166
|
+
cache = Cache.new(client, to_hash)
|
167
|
+
poller = Poller.new(cache, to_hash)
|
168
|
+
process_guard = ProcessGuard.new(cache, poller, to_hash)
|
169
|
+
I18n.backend = I18nBackend.new(cache)
|
166
170
|
CopycopterClient.client = client
|
167
|
-
CopycopterClient.
|
168
|
-
middleware.use(RequestSync, :
|
171
|
+
CopycopterClient.cache = cache
|
172
|
+
middleware.use(RequestSync, :cache => cache) if middleware && development?
|
169
173
|
@applied = true
|
170
174
|
logger.info("Client #{VERSION} ready")
|
171
175
|
logger.info("Environment Info: #{environment_info}")
|
172
|
-
|
176
|
+
process_guard.start unless test?
|
173
177
|
end
|
174
178
|
|
175
179
|
def port
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module CopycopterClient
|
2
2
|
# Raised when an error occurs while contacting the Copycopter server. This is
|
3
|
-
# raised by {Client} and generally rescued by {
|
3
|
+
# raised by {Client} and generally rescued by {Cache}. The application will
|
4
4
|
# not encounter this error. Polling will continue even if this error is raised.
|
5
5
|
class ConnectionError < StandardError
|
6
6
|
end
|
@@ -14,9 +14,9 @@ module CopycopterClient
|
|
14
14
|
include I18n::Backend::Simple::Implementation
|
15
15
|
|
16
16
|
# Usually instantiated when {Configuration#apply} is invoked.
|
17
|
-
# @param
|
18
|
-
def initialize(
|
19
|
-
@
|
17
|
+
# @param cache [Cache] must act like a hash, returning and accept blurbs by key.
|
18
|
+
def initialize(cache)
|
19
|
+
@cache = cache
|
20
20
|
end
|
21
21
|
|
22
22
|
# Translates the given local and key. See the I18n API documentation for details.
|
@@ -34,14 +34,14 @@ module CopycopterClient
|
|
34
34
|
# Returns locales availabile for this Copycopter project.
|
35
35
|
# @return [Array<String>] available locales
|
36
36
|
def available_locales
|
37
|
-
|
38
|
-
(
|
37
|
+
cached_locales = cache.keys.map { |key| key.split('.').first }
|
38
|
+
(cached_locales + super).uniq.map { |locale| locale.to_sym }
|
39
39
|
end
|
40
40
|
|
41
41
|
# Stores the given translations.
|
42
42
|
#
|
43
43
|
# Updates will be visible in the current process immediately, and will
|
44
|
-
# propagate to Copycopter during the next
|
44
|
+
# propagate to Copycopter during the next flush.
|
45
45
|
#
|
46
46
|
# @param [String] locale the locale (ie "en") to store translations for
|
47
47
|
# @param [Hash] data nested key-value pairs to be added as blurbs
|
@@ -56,8 +56,8 @@ module CopycopterClient
|
|
56
56
|
def lookup(locale, key, scope = [], options = {})
|
57
57
|
parts = I18n.normalize_keys(locale, key, scope, options[:separator])
|
58
58
|
key_with_locale = parts.join('.')
|
59
|
-
content =
|
60
|
-
|
59
|
+
content = cache[key_with_locale] || super
|
60
|
+
cache[key_with_locale] = "" if content.nil?
|
61
61
|
content
|
62
62
|
end
|
63
63
|
|
@@ -68,13 +68,13 @@ module CopycopterClient
|
|
68
68
|
end
|
69
69
|
elsif data.respond_to?(:to_str)
|
70
70
|
key = ([locale] + scope).join('.')
|
71
|
-
|
71
|
+
cache[key] = data.to_str
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
75
|
def load_translations(*filenames)
|
76
76
|
super
|
77
|
-
|
77
|
+
cache.wait_for_download
|
78
78
|
end
|
79
79
|
|
80
80
|
def default(locale, object, subject, options = {})
|
@@ -82,11 +82,11 @@ module CopycopterClient
|
|
82
82
|
if content.respond_to?(:to_str)
|
83
83
|
parts = I18n.normalize_keys(locale, object, options[:scope], options[:separator])
|
84
84
|
key = parts.join('.')
|
85
|
-
|
85
|
+
cache[key] = content.to_str
|
86
86
|
end
|
87
87
|
content
|
88
88
|
end
|
89
89
|
|
90
|
-
attr_reader :
|
90
|
+
attr_reader :cache
|
91
91
|
end
|
92
92
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'copycopter_client/cache'
|
3
|
+
|
4
|
+
module CopycopterClient
|
5
|
+
# Starts a background thread that continually resynchronizes with the remote
|
6
|
+
# server using the given {Cache} after a set delay.
|
7
|
+
class Poller
|
8
|
+
# @param options [Hash]
|
9
|
+
# @option options [Logger] :logger where errors should be logged
|
10
|
+
# @option options [Fixnum] :polling_delay how long to wait in between requests
|
11
|
+
def initialize(cache, options)
|
12
|
+
@cache = cache
|
13
|
+
@polling_delay = options[:polling_delay]
|
14
|
+
@logger = options[:logger]
|
15
|
+
@stop = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
Thread.new { poll } or logger.error("Couldn't start poller thread")
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
@stop = true
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :cache, :logger, :polling_delay
|
29
|
+
|
30
|
+
def poll
|
31
|
+
until @stop
|
32
|
+
cache.sync
|
33
|
+
logger.flush if logger.respond_to?(:flush)
|
34
|
+
delay
|
35
|
+
end
|
36
|
+
rescue InvalidApiKey => error
|
37
|
+
logger.error(error.message)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delay
|
41
|
+
sleep(polling_delay)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module CopycopterClient
|
2
|
+
# Starts the poller from a worker process, or register hooks for a spawner
|
3
|
+
# process (such as in Unicorn or Passenger). Also registers hooks for exiting
|
4
|
+
# processes and completing background jobs. Applications using the client
|
5
|
+
# will not need to interact with this class directly.
|
6
|
+
class ProcessGuard
|
7
|
+
# @param options [Hash]
|
8
|
+
# @option options [Logger] :logger where errors should be logged
|
9
|
+
def initialize(cache, poller, options)
|
10
|
+
@cache = cache
|
11
|
+
@poller = poller
|
12
|
+
@logger = options[:logger]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Starts the poller or registers hooks
|
16
|
+
def start
|
17
|
+
if spawner?
|
18
|
+
register_spawn_hooks
|
19
|
+
else
|
20
|
+
register_exit_hooks
|
21
|
+
register_job_hooks
|
22
|
+
start_polling
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def start_polling
|
29
|
+
@poller.start
|
30
|
+
end
|
31
|
+
|
32
|
+
def spawner?
|
33
|
+
passenger_spawner? || unicorn_spawner?
|
34
|
+
end
|
35
|
+
|
36
|
+
def passenger_spawner?
|
37
|
+
$0.include?("ApplicationSpawner")
|
38
|
+
end
|
39
|
+
|
40
|
+
def unicorn_spawner?
|
41
|
+
$0.include?("unicorn") && !caller.any? { |line| line.include?("worker_loop") }
|
42
|
+
end
|
43
|
+
|
44
|
+
def register_spawn_hooks
|
45
|
+
if defined?(PhusionPassenger)
|
46
|
+
register_passenger_hook
|
47
|
+
elsif defined?(Unicorn::HttpServer)
|
48
|
+
register_unicorn_hook
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def register_passenger_hook
|
53
|
+
@logger.info("Registered Phusion Passenger fork hook")
|
54
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
55
|
+
start_polling
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def register_unicorn_hook
|
60
|
+
@logger.info("Registered Unicorn fork hook")
|
61
|
+
poller = @poller
|
62
|
+
Unicorn::HttpServer.class_eval do
|
63
|
+
alias_method :worker_loop_without_copycopter, :worker_loop
|
64
|
+
define_method :worker_loop do |worker|
|
65
|
+
poller.start
|
66
|
+
worker_loop_without_copycopter(worker)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def register_exit_hooks
|
72
|
+
at_exit do
|
73
|
+
@cache.flush
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def register_job_hooks
|
78
|
+
if defined?(Resque::Job)
|
79
|
+
@logger.info("Registered Resque after_perform hook")
|
80
|
+
cache = @cache
|
81
|
+
Resque::Job.class_eval do
|
82
|
+
alias_method :perform_without_copycopter, :perform
|
83
|
+
define_method :perform do
|
84
|
+
job_was_performed = perform_without_copycopter
|
85
|
+
cache.flush
|
86
|
+
job_was_performed
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -5,18 +5,18 @@ module CopycopterClient
|
|
5
5
|
class RequestSync
|
6
6
|
# @param app [Rack] the upstream app into whose responses to inject the editor
|
7
7
|
# @param options [Hash]
|
8
|
-
# @option options [
|
8
|
+
# @option options [Cache] :cache agent that should be flushed after each request
|
9
9
|
def initialize(app, options)
|
10
10
|
@app = app
|
11
|
-
@
|
11
|
+
@cache = options[:cache]
|
12
12
|
end
|
13
13
|
|
14
|
-
# Invokes the upstream Rack application and flushes the
|
14
|
+
# Invokes the upstream Rack application and flushes the cache after each
|
15
15
|
# request.
|
16
16
|
def call(env)
|
17
|
-
@
|
17
|
+
@cache.download
|
18
18
|
response = @app.call(env)
|
19
|
-
@
|
19
|
+
@cache.flush
|
20
20
|
response
|
21
21
|
end
|
22
22
|
end
|
data/lib/copycopter_client.rb
CHANGED
@@ -17,9 +17,13 @@ module CopycopterClient
|
|
17
17
|
# configuration options. Usually set when {.configure} is called.
|
18
18
|
attr_accessor :configuration
|
19
19
|
|
20
|
-
# @return [
|
20
|
+
# @return [Cache] instance used to synchronize changes.
|
21
21
|
# This is set when {.configure} is called.
|
22
|
-
attr_accessor :
|
22
|
+
attr_accessor :cache
|
23
|
+
|
24
|
+
# @return [Poller] instance used to poll for changes.
|
25
|
+
# This is set when {.configure} is called.
|
26
|
+
attr_accessor :poller
|
23
27
|
end
|
24
28
|
|
25
29
|
# Issues a new deploy, marking all draft blurbs as published.
|
@@ -29,15 +33,13 @@ module CopycopterClient
|
|
29
33
|
end
|
30
34
|
|
31
35
|
# Starts the polling process.
|
32
|
-
|
33
|
-
|
34
|
-
sync.start
|
36
|
+
def self.start_poller
|
37
|
+
poller.start
|
35
38
|
end
|
36
39
|
|
37
40
|
# Flush queued changed synchronously
|
38
|
-
# This is called from the Resque after perform "hook"
|
39
41
|
def self.flush
|
40
|
-
|
42
|
+
cache.flush
|
41
43
|
end
|
42
44
|
|
43
45
|
# Call this method to modify defaults in your initializers.
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopycopterClient::Cache do
|
4
|
+
let(:client) { FakeClient.new }
|
5
|
+
|
6
|
+
def build_cache(config = {})
|
7
|
+
config[:logger] ||= FakeLogger.new
|
8
|
+
default_config = CopycopterClient::Configuration.new.to_hash
|
9
|
+
CopycopterClient::Cache.new(client, default_config.update(config))
|
10
|
+
end
|
11
|
+
|
12
|
+
it "provides access to downloaded data" do
|
13
|
+
client['en.test.key'] = 'expected'
|
14
|
+
client['en.test.other_key'] = 'expected'
|
15
|
+
|
16
|
+
cache = build_cache
|
17
|
+
|
18
|
+
cache.download
|
19
|
+
|
20
|
+
cache['en.test.key'].should == 'expected'
|
21
|
+
cache.keys.should =~ %w(en.test.key en.test.other_key)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "doesn't upload without changes" do
|
25
|
+
cache = build_cache
|
26
|
+
cache.flush
|
27
|
+
client.should_not be_uploaded
|
28
|
+
end
|
29
|
+
|
30
|
+
it "uploads changes when flushed" do
|
31
|
+
cache = build_cache
|
32
|
+
cache['test.key'] = 'test value'
|
33
|
+
|
34
|
+
cache.flush
|
35
|
+
|
36
|
+
client.uploaded.should == { 'test.key' => 'test value' }
|
37
|
+
end
|
38
|
+
|
39
|
+
it "downloads changes" do
|
40
|
+
client['test.key'] = 'test value'
|
41
|
+
cache = build_cache
|
42
|
+
|
43
|
+
cache.download
|
44
|
+
|
45
|
+
cache['test.key'].should == 'test value'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "downloads and uploads when synced" do
|
49
|
+
cache = build_cache
|
50
|
+
client['test.key'] = 'test value'
|
51
|
+
cache['other.key'] = 'other value'
|
52
|
+
|
53
|
+
cache.sync
|
54
|
+
|
55
|
+
client.uploaded.should == { 'other.key' => 'other value' }
|
56
|
+
cache['test.key'].should == 'test value'
|
57
|
+
end
|
58
|
+
|
59
|
+
it "handles connection errors when flushing" do
|
60
|
+
failure = "server is napping"
|
61
|
+
logger = FakeLogger.new
|
62
|
+
client.stubs(:upload).raises(CopycopterClient::ConnectionError.new(failure))
|
63
|
+
cache = build_cache(:logger => logger)
|
64
|
+
cache['upload.key'] = 'upload'
|
65
|
+
|
66
|
+
cache.flush
|
67
|
+
|
68
|
+
logger.should have_entry(:error, failure)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "handles connection errors when downloading" do
|
72
|
+
failure = "server is napping"
|
73
|
+
logger = FakeLogger.new
|
74
|
+
client.stubs(:download).raises(CopycopterClient::ConnectionError.new(failure))
|
75
|
+
cache = build_cache(:logger => logger)
|
76
|
+
|
77
|
+
cache.download
|
78
|
+
|
79
|
+
logger.should have_entry(:error, failure)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "blocks until the first download is complete" do
|
83
|
+
logger = FakeLogger.new
|
84
|
+
logger.stubs(:flush)
|
85
|
+
client.delay = 0.5
|
86
|
+
cache = build_cache(:logger => logger)
|
87
|
+
|
88
|
+
Thread.new { cache.download }
|
89
|
+
|
90
|
+
finished = false
|
91
|
+
Thread.new do
|
92
|
+
cache.wait_for_download
|
93
|
+
finished = true
|
94
|
+
end
|
95
|
+
|
96
|
+
sleep(1)
|
97
|
+
|
98
|
+
finished.should == true
|
99
|
+
logger.should have_entry(:info, "Waiting for first download")
|
100
|
+
logger.should have_received(:flush)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "doesn't block if the first download fails" do
|
104
|
+
client.delay = 0.5
|
105
|
+
client.error = StandardError.new("Failure")
|
106
|
+
cache = build_cache
|
107
|
+
|
108
|
+
Thread.new { cache.download }
|
109
|
+
|
110
|
+
finished = false
|
111
|
+
Thread.new do
|
112
|
+
cache.wait_for_download
|
113
|
+
finished = true
|
114
|
+
end
|
115
|
+
|
116
|
+
sleep(1)
|
117
|
+
|
118
|
+
expect { cache.download }.to raise_error(StandardError, "Failure")
|
119
|
+
finished.should == true
|
120
|
+
end
|
121
|
+
|
122
|
+
it "doesn't block before downloading" do
|
123
|
+
logger = FakeLogger.new
|
124
|
+
cache = build_cache(:logger => logger)
|
125
|
+
|
126
|
+
finished = false
|
127
|
+
Thread.new do
|
128
|
+
cache.wait_for_download
|
129
|
+
finished = true
|
130
|
+
end
|
131
|
+
|
132
|
+
sleep(1)
|
133
|
+
|
134
|
+
finished.should == true
|
135
|
+
logger.should_not have_entry(:info, "Waiting for first download")
|
136
|
+
end
|
137
|
+
|
138
|
+
it "doesn't return blank copy" do
|
139
|
+
client['en.test.key'] = ''
|
140
|
+
cache = build_cache
|
141
|
+
|
142
|
+
cache.download
|
143
|
+
|
144
|
+
cache['en.test.key'].should be_nil
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "given locked mutex" do
|
148
|
+
RSpec::Matchers.define :finish_after_unlocking do |mutex|
|
149
|
+
match do |thread|
|
150
|
+
sleep(0.1)
|
151
|
+
|
152
|
+
if thread.status === false
|
153
|
+
violated("finished before unlocking")
|
154
|
+
else
|
155
|
+
mutex.unlock
|
156
|
+
sleep(0.1)
|
157
|
+
|
158
|
+
if thread.status === false
|
159
|
+
true
|
160
|
+
else
|
161
|
+
violated("still running after unlocking")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def violated(failure)
|
167
|
+
@failure_message = failure
|
168
|
+
false
|
169
|
+
end
|
170
|
+
|
171
|
+
failure_message_for_should do
|
172
|
+
@failure_message
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
let(:mutex) { Mutex.new }
|
177
|
+
let(:cache) { build_cache }
|
178
|
+
|
179
|
+
before do
|
180
|
+
mutex.lock
|
181
|
+
Mutex.stubs(:new => mutex)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "synchronizes read access to keys between threads" do
|
185
|
+
Thread.new { cache['test.key'] }.should finish_after_unlocking(mutex)
|
186
|
+
end
|
187
|
+
|
188
|
+
it "synchronizes read access to the key list between threads" do
|
189
|
+
Thread.new { cache.keys }.should finish_after_unlocking(mutex)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "synchronizes write access to keys between threads" do
|
193
|
+
Thread.new { cache['test.key'] = 'value' }.should finish_after_unlocking(mutex)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it "flushes from the top level" do
|
198
|
+
cache = build_cache
|
199
|
+
CopycopterClient.cache = cache
|
200
|
+
cache.stubs(:flush)
|
201
|
+
|
202
|
+
CopycopterClient.flush
|
203
|
+
|
204
|
+
cache.should have_received(:flush)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|