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
@@ -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
|
-
|