copycopter_client 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -2,12 +2,12 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe CopycopterClient::RequestSync do
|
4
4
|
|
5
|
-
let(:
|
5
|
+
let(:cache) { {} }
|
6
6
|
let(:response) { 'response' }
|
7
7
|
let(:env) { 'env' }
|
8
8
|
let(:app) { stub('app', :call => response) }
|
9
|
-
before {
|
10
|
-
subject { CopycopterClient::RequestSync.new(app, :
|
9
|
+
before { cache.stubs(:flush => nil, :download => nil) }
|
10
|
+
subject { CopycopterClient::RequestSync.new(app, :cache => cache) }
|
11
11
|
|
12
12
|
it "invokes the upstream app" do
|
13
13
|
result = subject.call(env)
|
@@ -17,11 +17,11 @@ describe CopycopterClient::RequestSync do
|
|
17
17
|
|
18
18
|
it "flushes defaults" do
|
19
19
|
subject.call(env)
|
20
|
-
|
20
|
+
cache.should have_received(:flush)
|
21
21
|
end
|
22
22
|
|
23
23
|
it "downloads new copy" do
|
24
24
|
subject.call(env)
|
25
|
-
|
25
|
+
cache.should have_received(:download)
|
26
26
|
end
|
27
27
|
end
|
data/spec/support/fake_client.rb
CHANGED
@@ -7,7 +7,7 @@ class FakeClient
|
|
7
7
|
end
|
8
8
|
|
9
9
|
attr_reader :uploaded, :uploads, :downloads
|
10
|
-
attr_accessor :delay
|
10
|
+
attr_accessor :delay, :error
|
11
11
|
|
12
12
|
def []=(key, value)
|
13
13
|
@data[key] = value
|
@@ -15,6 +15,7 @@ class FakeClient
|
|
15
15
|
|
16
16
|
def download
|
17
17
|
wait_for_delay
|
18
|
+
raise_error_if_present
|
18
19
|
@downloads += 1
|
19
20
|
yield @data.dup
|
20
21
|
nil
|
@@ -22,6 +23,7 @@ class FakeClient
|
|
22
23
|
|
23
24
|
def upload(data)
|
24
25
|
wait_for_delay
|
26
|
+
raise_error_if_present
|
25
27
|
@uploaded.update(data)
|
26
28
|
@uploads += 1
|
27
29
|
end
|
@@ -39,5 +41,9 @@ class FakeClient
|
|
39
41
|
def wait_for_delay
|
40
42
|
sleep(delay) if delay
|
41
43
|
end
|
44
|
+
|
45
|
+
def raise_error_if_present
|
46
|
+
raise error if error
|
47
|
+
end
|
42
48
|
end
|
43
49
|
|
@@ -1,11 +1,18 @@
|
|
1
1
|
class FakeResqueJob
|
2
|
-
|
3
|
-
|
4
|
-
@key = hash[:key]
|
5
|
-
@value = hash[:value]
|
2
|
+
def initialize(&action)
|
3
|
+
@action = action || lambda {}
|
6
4
|
end
|
5
|
+
|
6
|
+
def fork_and_perform
|
7
|
+
fork do
|
8
|
+
perform
|
9
|
+
exit!
|
10
|
+
end
|
11
|
+
Process.wait
|
12
|
+
end
|
13
|
+
|
7
14
|
def perform
|
8
|
-
|
15
|
+
@action.call
|
9
16
|
true
|
10
17
|
end
|
11
18
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class WritingCache
|
2
|
+
def flush
|
3
|
+
File.open(path, "w") do |file|
|
4
|
+
file.write(object_id.to_s)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def written?
|
9
|
+
IO.read(path) == object_id.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def path
|
15
|
+
File.join(PROJECT_ROOT, 'tmp', 'written_cache')
|
16
|
+
end
|
17
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: copycopter_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 17
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 1.0.
|
9
|
+
- 3
|
10
|
+
version: 1.0.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- thoughtbot
|
@@ -15,11 +15,12 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-06-21 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
|
22
|
+
name: i18n
|
23
|
+
prerelease: false
|
23
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
25
|
none: false
|
25
26
|
requirements:
|
@@ -31,11 +32,11 @@ dependencies:
|
|
31
32
|
- 5
|
32
33
|
- 0
|
33
34
|
version: 0.5.0
|
35
|
+
type: :runtime
|
34
36
|
version_requirements: *id001
|
35
|
-
name: i18n
|
36
|
-
prerelease: false
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
|
-
|
38
|
+
name: json
|
39
|
+
prerelease: false
|
39
40
|
requirement: &id002 !ruby/object:Gem::Requirement
|
40
41
|
none: false
|
41
42
|
requirements:
|
@@ -45,9 +46,8 @@ dependencies:
|
|
45
46
|
segments:
|
46
47
|
- 0
|
47
48
|
version: "0"
|
49
|
+
type: :runtime
|
48
50
|
version_requirements: *id002
|
49
|
-
name: json
|
50
|
-
prerelease: false
|
51
51
|
description:
|
52
52
|
email: support@thoughtbot.com
|
53
53
|
executables: []
|
@@ -62,24 +62,28 @@ files:
|
|
62
62
|
- Rakefile
|
63
63
|
- init.rb
|
64
64
|
- AddTrustExternalCARoot.crt
|
65
|
+
- lib/copycopter_client/cache.rb
|
65
66
|
- lib/copycopter_client/client.rb
|
66
67
|
- lib/copycopter_client/configuration.rb
|
67
68
|
- lib/copycopter_client/errors.rb
|
68
69
|
- lib/copycopter_client/i18n_backend.rb
|
70
|
+
- lib/copycopter_client/poller.rb
|
69
71
|
- lib/copycopter_client/prefixed_logger.rb
|
72
|
+
- lib/copycopter_client/process_guard.rb
|
70
73
|
- lib/copycopter_client/rails.rb
|
71
74
|
- lib/copycopter_client/railtie.rb
|
72
75
|
- lib/copycopter_client/request_sync.rb
|
73
|
-
- lib/copycopter_client/sync.rb
|
74
76
|
- lib/copycopter_client/version.rb
|
75
77
|
- lib/copycopter_client.rb
|
76
78
|
- lib/tasks/copycopter_client_tasks.rake
|
79
|
+
- spec/copycopter_client/cache_spec.rb
|
77
80
|
- spec/copycopter_client/client_spec.rb
|
78
81
|
- spec/copycopter_client/configuration_spec.rb
|
79
82
|
- spec/copycopter_client/i18n_backend_spec.rb
|
83
|
+
- spec/copycopter_client/poller_spec.rb
|
80
84
|
- spec/copycopter_client/prefixed_logger_spec.rb
|
85
|
+
- spec/copycopter_client/process_guard_spec.rb
|
81
86
|
- spec/copycopter_client/request_sync_spec.rb
|
82
|
-
- spec/copycopter_client/sync_spec.rb
|
83
87
|
- spec/spec_helper.rb
|
84
88
|
- spec/support/client_spec_helpers.rb
|
85
89
|
- spec/support/defines_constants.rb
|
@@ -91,6 +95,7 @@ files:
|
|
91
95
|
- spec/support/fake_resque_job.rb
|
92
96
|
- spec/support/fake_unicorn.rb
|
93
97
|
- spec/support/middleware_stack.rb
|
98
|
+
- spec/support/writing_cache.rb
|
94
99
|
- features/rails.feature
|
95
100
|
- features/step_definitions/copycopter_server_steps.rb
|
96
101
|
- features/step_definitions/rails_steps.rb
|
@@ -126,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
131
|
requirements: []
|
127
132
|
|
128
133
|
rubyforge_project: copycopter_client
|
129
|
-
rubygems_version: 1.
|
134
|
+
rubygems_version: 1.4.1
|
130
135
|
signing_key:
|
131
136
|
specification_version: 3
|
132
137
|
summary: Client for the Copycopter content management service
|
@@ -1,182 +0,0 @@
|
|
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:
|
10
|
-
# * Starting and running the background polling thread
|
11
|
-
# * Locking down access to data used by both threads
|
12
|
-
class Sync
|
13
|
-
# Usually instantiated when {Configuration#apply} is invoked.
|
14
|
-
# @param client [Client] the client used to fetch and upload data
|
15
|
-
# @param options [Hash]
|
16
|
-
# @option options [Fixnum] :polling_delay the number of seconds in between each synchronization with the server
|
17
|
-
# @option options [Logger] :logger where errors should be logged
|
18
|
-
def initialize(client, options)
|
19
|
-
@client = client
|
20
|
-
@blurbs = {}
|
21
|
-
@polling_delay = options[:polling_delay]
|
22
|
-
@stop = false
|
23
|
-
@queued = {}
|
24
|
-
@mutex = Mutex.new
|
25
|
-
@logger = options[:logger]
|
26
|
-
@pending = false
|
27
|
-
end
|
28
|
-
|
29
|
-
# Starts the polling thread. The polling thread doesn't run in test environments.
|
30
|
-
#
|
31
|
-
# If this sync was created from a master spawner (as in the case for
|
32
|
-
# phusion passenger), it will instead register after fork hooks so that the
|
33
|
-
# poller starts in each spawned process.
|
34
|
-
def start
|
35
|
-
if spawner?
|
36
|
-
register_spawn_hooks
|
37
|
-
else
|
38
|
-
register_job_hooks
|
39
|
-
logger.info("Starting poller")
|
40
|
-
@pending = true
|
41
|
-
at_exit { sync }
|
42
|
-
unless Thread.new { poll }
|
43
|
-
logger.error("Couldn't start poller thread")
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Stops the polling thread after the next run.
|
49
|
-
def stop
|
50
|
-
@stop = true
|
51
|
-
end
|
52
|
-
|
53
|
-
# Returns content for the given blurb.
|
54
|
-
# @param key [String] the key of the desired blurb
|
55
|
-
# @return [String] the contents of the blurb
|
56
|
-
def [](key)
|
57
|
-
lock { @blurbs[key] }
|
58
|
-
end
|
59
|
-
|
60
|
-
# Sets content for the given blurb. The content will be pushed to the
|
61
|
-
# server on the next poll.
|
62
|
-
# @param key [String] the key of the blurb to update
|
63
|
-
# @param value [String] the new contents of the blurb
|
64
|
-
def []=(key, value)
|
65
|
-
lock { @queued[key] = value }
|
66
|
-
end
|
67
|
-
|
68
|
-
# Keys for all blurbs stored on the server.
|
69
|
-
# @return [Array<String>] keys
|
70
|
-
def keys
|
71
|
-
lock { @blurbs.keys }
|
72
|
-
end
|
73
|
-
|
74
|
-
# Waits until the first download has finished.
|
75
|
-
def wait_for_download
|
76
|
-
if @pending
|
77
|
-
logger.info("Waiting for first sync")
|
78
|
-
logger.flush if logger.respond_to?(:flush)
|
79
|
-
while @pending
|
80
|
-
sleep(0.1)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def flush
|
86
|
-
with_queued_changes do |queued|
|
87
|
-
client.upload(queued)
|
88
|
-
end
|
89
|
-
rescue ConnectionError => error
|
90
|
-
logger.error(error.message)
|
91
|
-
end
|
92
|
-
|
93
|
-
def download
|
94
|
-
client.download do |downloaded_blurbs|
|
95
|
-
downloaded_blurbs.reject! { |key, value| value == "" }
|
96
|
-
lock { @blurbs = downloaded_blurbs }
|
97
|
-
end
|
98
|
-
rescue ConnectionError => error
|
99
|
-
logger.error(error.message)
|
100
|
-
end
|
101
|
-
|
102
|
-
private
|
103
|
-
|
104
|
-
attr_reader :client, :polling_delay, :logger
|
105
|
-
|
106
|
-
def poll
|
107
|
-
until @stop
|
108
|
-
sync
|
109
|
-
logger.flush if logger.respond_to?(:flush)
|
110
|
-
sleep(polling_delay)
|
111
|
-
end
|
112
|
-
rescue InvalidApiKey => error
|
113
|
-
logger.error(error.message)
|
114
|
-
end
|
115
|
-
|
116
|
-
def sync
|
117
|
-
download
|
118
|
-
flush
|
119
|
-
ensure
|
120
|
-
@pending = false
|
121
|
-
end
|
122
|
-
|
123
|
-
def with_queued_changes
|
124
|
-
changes_to_push = nil
|
125
|
-
lock do
|
126
|
-
unless @queued.empty?
|
127
|
-
changes_to_push = @queued
|
128
|
-
@queued = {}
|
129
|
-
end
|
130
|
-
end
|
131
|
-
yield(changes_to_push) if changes_to_push
|
132
|
-
end
|
133
|
-
|
134
|
-
def lock(&block)
|
135
|
-
@mutex.synchronize(&block)
|
136
|
-
end
|
137
|
-
|
138
|
-
def spawner?
|
139
|
-
passenger_spawner? || unicorn_spawner?
|
140
|
-
end
|
141
|
-
|
142
|
-
def passenger_spawner?
|
143
|
-
$0.include?("ApplicationSpawner")
|
144
|
-
end
|
145
|
-
|
146
|
-
def unicorn_spawner?
|
147
|
-
$0.include?("unicorn") && !caller.any? { |line| line.include?("worker_loop") }
|
148
|
-
end
|
149
|
-
|
150
|
-
def register_job_hooks
|
151
|
-
if defined?(Resque)
|
152
|
-
logger.info("Registered Resque after_perform hook")
|
153
|
-
Resque::Job.class_eval do
|
154
|
-
alias_method :perform_without_copycopter, :perform
|
155
|
-
def perform
|
156
|
-
job_was_performed = perform_without_copycopter
|
157
|
-
CopycopterClient.flush
|
158
|
-
job_was_performed
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def register_spawn_hooks
|
165
|
-
if defined?(PhusionPassenger)
|
166
|
-
logger.info("Registered Phusion Passenger fork hook")
|
167
|
-
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
168
|
-
start
|
169
|
-
end
|
170
|
-
elsif defined?(Unicorn::HttpServer)
|
171
|
-
logger.info("Registered Unicorn fork hook")
|
172
|
-
Unicorn::HttpServer.class_eval do
|
173
|
-
alias_method :worker_loop_without_copycopter, :worker_loop
|
174
|
-
def worker_loop(worker)
|
175
|
-
CopycopterClient.start_sync
|
176
|
-
worker_loop_without_copycopter(worker)
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
@@ -1,415 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe CopycopterClient::Sync do
|
4
|
-
include DefinesConstants
|
5
|
-
|
6
|
-
let(:client) { FakeClient.new }
|
7
|
-
|
8
|
-
def build_sync(config = {})
|
9
|
-
config[:logger] ||= FakeLogger.new
|
10
|
-
default_config = CopycopterClient::Configuration.new.to_hash
|
11
|
-
sync = CopycopterClient::Sync.new(client, default_config.update(config))
|
12
|
-
@syncs << sync
|
13
|
-
sync
|
14
|
-
end
|
15
|
-
|
16
|
-
before do
|
17
|
-
@syncs = []
|
18
|
-
@original_process_name = $0
|
19
|
-
end
|
20
|
-
|
21
|
-
after do
|
22
|
-
$0 = @original_process_name
|
23
|
-
@syncs.each { |sync| sync.stop }
|
24
|
-
end
|
25
|
-
|
26
|
-
it "syncs when the process terminates" do
|
27
|
-
api_key = "12345"
|
28
|
-
FakeCopycopterApp.add_project api_key
|
29
|
-
pid = fork do
|
30
|
-
config = { :logger => FakeLogger.new, :polling_delay => 86400, :api_key => api_key }
|
31
|
-
default_config = CopycopterClient::Configuration.new.to_hash.update(config)
|
32
|
-
real_client = CopycopterClient::Client.new(default_config)
|
33
|
-
sync = CopycopterClient::Sync.new(real_client, default_config)
|
34
|
-
sync.start
|
35
|
-
sleep 2
|
36
|
-
sync['test.key'] = 'value'
|
37
|
-
Signal.trap("INT") { exit }
|
38
|
-
sleep
|
39
|
-
end
|
40
|
-
sleep 3
|
41
|
-
Process.kill("INT", pid)
|
42
|
-
Process.wait
|
43
|
-
project = FakeCopycopterApp.project(api_key)
|
44
|
-
project.draft['test.key'].should == 'value'
|
45
|
-
end
|
46
|
-
|
47
|
-
it "provides access to downloaded data" do
|
48
|
-
client['en.test.key'] = 'expected'
|
49
|
-
client['en.test.other_key'] = 'expected'
|
50
|
-
|
51
|
-
sync = build_sync
|
52
|
-
|
53
|
-
sync.start
|
54
|
-
|
55
|
-
sync['en.test.key'].should == 'expected'
|
56
|
-
sync.keys.should =~ %w(en.test.key en.test.other_key)
|
57
|
-
end
|
58
|
-
|
59
|
-
it "it polls after being started" do
|
60
|
-
sync = build_sync(:polling_delay => 1)
|
61
|
-
sync.start
|
62
|
-
|
63
|
-
sync['test.key'].should be_nil
|
64
|
-
|
65
|
-
client['test.key'] = 'value'
|
66
|
-
sleep(2)
|
67
|
-
|
68
|
-
sync['test.key'].should == 'value'
|
69
|
-
end
|
70
|
-
|
71
|
-
it "stops polling when stopped" do
|
72
|
-
sync = build_sync(:polling_delay => 1)
|
73
|
-
sync.start
|
74
|
-
|
75
|
-
sync['test.key'].should be_nil
|
76
|
-
|
77
|
-
sync.stop
|
78
|
-
|
79
|
-
client['test.key'] = 'value'
|
80
|
-
sleep(2)
|
81
|
-
|
82
|
-
sync['test.key'].should be_nil
|
83
|
-
end
|
84
|
-
|
85
|
-
it "doesn't upload without changes" do
|
86
|
-
sync = build_sync(:polling_delay => 1)
|
87
|
-
sync.start
|
88
|
-
sleep(2)
|
89
|
-
client.should_not be_uploaded
|
90
|
-
end
|
91
|
-
|
92
|
-
it "uploads changes when polling" do
|
93
|
-
sync = build_sync(:polling_delay => 1)
|
94
|
-
sync.start
|
95
|
-
|
96
|
-
sync['test.key'] = 'test value'
|
97
|
-
sleep(2)
|
98
|
-
|
99
|
-
client.uploaded.should == { 'test.key' => 'test value' }
|
100
|
-
end
|
101
|
-
|
102
|
-
it "uploads changes when flushed" do
|
103
|
-
sync = build_sync
|
104
|
-
sync['test.key'] = 'test value'
|
105
|
-
|
106
|
-
sync.flush
|
107
|
-
|
108
|
-
client.uploaded.should == { 'test.key' => 'test value' }
|
109
|
-
end
|
110
|
-
|
111
|
-
it "downloads changes" do
|
112
|
-
client['test.key'] = 'test value'
|
113
|
-
sync = build_sync
|
114
|
-
|
115
|
-
sync.download
|
116
|
-
|
117
|
-
sync['test.key'].should == 'test value'
|
118
|
-
end
|
119
|
-
|
120
|
-
it "handles connection errors when flushing" do
|
121
|
-
failure = "server is napping"
|
122
|
-
logger = FakeLogger.new
|
123
|
-
client.stubs(:upload).raises(CopycopterClient::ConnectionError.new(failure))
|
124
|
-
sync = build_sync(:logger => logger)
|
125
|
-
sync['upload.key'] = 'upload'
|
126
|
-
|
127
|
-
sync.flush
|
128
|
-
|
129
|
-
logger.should have_entry(:error, failure)
|
130
|
-
end
|
131
|
-
|
132
|
-
it "handles connection errors when downloading" do
|
133
|
-
failure = "server is napping"
|
134
|
-
logger = FakeLogger.new
|
135
|
-
client.stubs(:download).raises(CopycopterClient::ConnectionError.new(failure))
|
136
|
-
sync = build_sync(:logger => logger)
|
137
|
-
|
138
|
-
sync.download
|
139
|
-
|
140
|
-
logger.should have_entry(:error, failure)
|
141
|
-
end
|
142
|
-
|
143
|
-
it "handles connection errors when polling" do
|
144
|
-
failure = "server is napping"
|
145
|
-
logger = FakeLogger.new
|
146
|
-
client.stubs(:upload).raises(CopycopterClient::ConnectionError.new(failure))
|
147
|
-
sync = build_sync(:polling_delay => 1, :logger => logger)
|
148
|
-
|
149
|
-
sync['upload.key'] = 'upload'
|
150
|
-
sync.start
|
151
|
-
sleep(2)
|
152
|
-
|
153
|
-
logger.should have_entry(:error, failure),
|
154
|
-
logger.entries.inspect
|
155
|
-
|
156
|
-
client['test.key'] = 'test value'
|
157
|
-
sleep(2)
|
158
|
-
|
159
|
-
sync['test.key'].should == 'test value'
|
160
|
-
end
|
161
|
-
|
162
|
-
it "handles an invalid api key" do
|
163
|
-
failure = "server is napping"
|
164
|
-
logger = FakeLogger.new
|
165
|
-
client.stubs(:upload).raises(CopycopterClient::InvalidApiKey.new(failure))
|
166
|
-
sync = build_sync(:polling_delay => 1, :logger => logger)
|
167
|
-
|
168
|
-
sync['upload.key'] = 'upload'
|
169
|
-
sync.start
|
170
|
-
sleep(2)
|
171
|
-
|
172
|
-
logger.should have_entry(:error, failure),
|
173
|
-
logger.entries.inspect
|
174
|
-
|
175
|
-
client['test.key'] = 'test value'
|
176
|
-
sleep(2)
|
177
|
-
|
178
|
-
sync['test.key'].should be_nil
|
179
|
-
end
|
180
|
-
|
181
|
-
it "starts after spawning when using passenger" do
|
182
|
-
logger = FakeLogger.new
|
183
|
-
passenger = define_constant('PhusionPassenger', FakePassenger.new)
|
184
|
-
passenger.become_master
|
185
|
-
sync = build_sync(:polling_delay => 1, :logger => logger)
|
186
|
-
|
187
|
-
sync.start
|
188
|
-
sleep(2)
|
189
|
-
|
190
|
-
client.should_not be_downloaded
|
191
|
-
logger.should have_entry(:info, "Registered Phusion Passenger fork hook")
|
192
|
-
|
193
|
-
passenger.spawn
|
194
|
-
sleep(2)
|
195
|
-
|
196
|
-
client.should be_downloaded
|
197
|
-
logger.should have_entry(:info, "Starting poller"),
|
198
|
-
"Got entries: #{logger.entries.inspect}"
|
199
|
-
end
|
200
|
-
|
201
|
-
it "flushes after running a resque job" do
|
202
|
-
define_constant('Resque', Module.new)
|
203
|
-
job = define_constant('Resque::Job', FakeResqueJob).new(:key => 'test.key', :value => 'all your base')
|
204
|
-
|
205
|
-
api_key = "12345"
|
206
|
-
FakeCopycopterApp.add_project api_key
|
207
|
-
logger = FakeLogger.new
|
208
|
-
|
209
|
-
config = { :logger => logger, :polling_delay => 86400, :api_key => api_key }
|
210
|
-
default_config = CopycopterClient::Configuration.new.to_hash.update(config)
|
211
|
-
real_client = CopycopterClient::Client.new(default_config)
|
212
|
-
sync = CopycopterClient::Sync.new(real_client, default_config)
|
213
|
-
sync.stubs(:at_exit)
|
214
|
-
CopycopterClient.sync = sync
|
215
|
-
job.sync = sync
|
216
|
-
|
217
|
-
sync.start
|
218
|
-
sleep(2)
|
219
|
-
|
220
|
-
logger.should have_entry(:info, "Registered Resque after_perform hook")
|
221
|
-
|
222
|
-
if fork
|
223
|
-
Process.wait
|
224
|
-
else
|
225
|
-
job.perform
|
226
|
-
exit!
|
227
|
-
end
|
228
|
-
sleep(2)
|
229
|
-
|
230
|
-
project = FakeCopycopterApp.project(api_key)
|
231
|
-
project.draft['test.key'].should == 'all your base'
|
232
|
-
end
|
233
|
-
|
234
|
-
it "starts after spawning when using unicorn" do
|
235
|
-
logger = FakeLogger.new
|
236
|
-
define_constant('Unicorn', Module.new)
|
237
|
-
unicorn = define_constant('Unicorn::HttpServer', FakeUnicornServer).new
|
238
|
-
unicorn.become_master
|
239
|
-
sync = build_sync(:polling_delay => 1, :logger => logger)
|
240
|
-
CopycopterClient.sync = sync
|
241
|
-
|
242
|
-
sync.start
|
243
|
-
sleep(2)
|
244
|
-
|
245
|
-
client.should_not be_downloaded
|
246
|
-
logger.should have_entry(:info, "Registered Unicorn fork hook")
|
247
|
-
|
248
|
-
unicorn.spawn
|
249
|
-
sleep(2)
|
250
|
-
|
251
|
-
client.should be_downloaded
|
252
|
-
logger.should have_entry(:info, "Starting poller")
|
253
|
-
end
|
254
|
-
|
255
|
-
it "blocks until the first download is complete" do
|
256
|
-
logger = FakeLogger.new
|
257
|
-
logger.stubs(:flush)
|
258
|
-
client.delay = 1
|
259
|
-
sync = build_sync(:logger => logger)
|
260
|
-
|
261
|
-
sync.start
|
262
|
-
|
263
|
-
finished = false
|
264
|
-
Thread.new do
|
265
|
-
sync.wait_for_download
|
266
|
-
finished = true
|
267
|
-
end
|
268
|
-
|
269
|
-
logger.should have_entry(:info, "Waiting for first sync")
|
270
|
-
logger.should have_received(:flush)
|
271
|
-
finished.should == false
|
272
|
-
|
273
|
-
sleep(3)
|
274
|
-
|
275
|
-
finished.should == true
|
276
|
-
end
|
277
|
-
|
278
|
-
it "doesn't block if the first download fails" do
|
279
|
-
client.delay = 1
|
280
|
-
client.stubs(:upload).raises(StandardError.new("Failure"))
|
281
|
-
sync = build_sync
|
282
|
-
|
283
|
-
sync['test.key'] = 'value'
|
284
|
-
sync.start
|
285
|
-
|
286
|
-
finished = false
|
287
|
-
Thread.new do
|
288
|
-
sync.wait_for_download
|
289
|
-
finished = true
|
290
|
-
end
|
291
|
-
|
292
|
-
finished.should == false
|
293
|
-
|
294
|
-
sleep(4)
|
295
|
-
|
296
|
-
finished.should == true
|
297
|
-
end
|
298
|
-
|
299
|
-
it "doesn't block before starting" do
|
300
|
-
logger = FakeLogger.new
|
301
|
-
sync = build_sync(:polling_delay => 3, :logger => logger)
|
302
|
-
|
303
|
-
finished = false
|
304
|
-
Thread.new do
|
305
|
-
sync.wait_for_download
|
306
|
-
finished = true
|
307
|
-
end
|
308
|
-
|
309
|
-
sleep(1)
|
310
|
-
|
311
|
-
finished.should == true
|
312
|
-
logger.should_not have_entry(:info, "Waiting for first sync")
|
313
|
-
end
|
314
|
-
|
315
|
-
it "logs an error if the background thread can't start" do
|
316
|
-
Thread.stubs(:new => nil)
|
317
|
-
logger = FakeLogger.new
|
318
|
-
|
319
|
-
build_sync(:logger => logger).start
|
320
|
-
|
321
|
-
logger.should have_entry(:error, "Couldn't start poller thread")
|
322
|
-
end
|
323
|
-
|
324
|
-
it "flushes the log when polling" do
|
325
|
-
logger = FakeLogger.new
|
326
|
-
logger.stubs(:flush)
|
327
|
-
sync = build_sync(:polling_delay => 1, :logger => logger)
|
328
|
-
|
329
|
-
sync.start
|
330
|
-
sleep(2)
|
331
|
-
|
332
|
-
logger.should have_received(:flush).at_least_once
|
333
|
-
end
|
334
|
-
|
335
|
-
it "doesn't return blank copy" do
|
336
|
-
client['en.test.key'] = ''
|
337
|
-
sync = build_sync(:polling_delay => 1)
|
338
|
-
|
339
|
-
sync.start
|
340
|
-
sleep(2)
|
341
|
-
|
342
|
-
sync['en.test.key'].should be_nil
|
343
|
-
end
|
344
|
-
|
345
|
-
describe "given locked mutex" do
|
346
|
-
RSpec::Matchers.define :finish_after_unlocking do |mutex|
|
347
|
-
match do |thread|
|
348
|
-
sleep(0.1)
|
349
|
-
|
350
|
-
if thread.status === false
|
351
|
-
violated("finished before unlocking")
|
352
|
-
else
|
353
|
-
mutex.unlock
|
354
|
-
sleep(0.1)
|
355
|
-
|
356
|
-
if thread.status === false
|
357
|
-
true
|
358
|
-
else
|
359
|
-
violated("still running after unlocking")
|
360
|
-
end
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
def violated(failure)
|
365
|
-
@failure_message = failure
|
366
|
-
false
|
367
|
-
end
|
368
|
-
|
369
|
-
failure_message_for_should do
|
370
|
-
@failure_message
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
|
-
let(:mutex) { Mutex.new }
|
375
|
-
let(:sync) { build_sync(:polling_delay => 0.1) }
|
376
|
-
|
377
|
-
before do
|
378
|
-
mutex.lock
|
379
|
-
Mutex.stubs(:new => mutex)
|
380
|
-
end
|
381
|
-
|
382
|
-
it "synchronizes read access to keys between threads" do
|
383
|
-
Thread.new { sync['test.key'] }.should finish_after_unlocking(mutex)
|
384
|
-
end
|
385
|
-
|
386
|
-
it "synchronizes read access to the key list between threads" do
|
387
|
-
Thread.new { sync.keys }.should finish_after_unlocking(mutex)
|
388
|
-
end
|
389
|
-
|
390
|
-
it "synchronizes write access to keys between threads" do
|
391
|
-
Thread.new { sync['test.key'] = 'value' }.should finish_after_unlocking(mutex)
|
392
|
-
end
|
393
|
-
end
|
394
|
-
|
395
|
-
it "starts from the top-level constant" do
|
396
|
-
sync = build_sync
|
397
|
-
CopycopterClient.sync = sync
|
398
|
-
sync.stubs(:start)
|
399
|
-
|
400
|
-
CopycopterClient.start_sync
|
401
|
-
|
402
|
-
sync.should have_received(:start)
|
403
|
-
end
|
404
|
-
|
405
|
-
it "flushes from the top level" do
|
406
|
-
sync = build_sync
|
407
|
-
CopycopterClient.sync = sync
|
408
|
-
sync.stubs(:flush)
|
409
|
-
|
410
|
-
CopycopterClient.flush
|
411
|
-
|
412
|
-
sync.should have_received(:flush)
|
413
|
-
end
|
414
|
-
end
|
415
|
-
|