rapns 3.0.0-java
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/CHANGELOG.md +31 -0
- data/LICENSE +7 -0
- data/README.md +138 -0
- data/bin/rapns +41 -0
- data/config/database.yml +39 -0
- data/lib/generators/rapns_generator.rb +34 -0
- data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/add_gcm.rb +94 -0
- data/lib/generators/templates/create_rapns_apps.rb +16 -0
- data/lib/generators/templates/create_rapns_feedback.rb +15 -0
- data/lib/generators/templates/create_rapns_notifications.rb +26 -0
- data/lib/generators/templates/rapns.rb +39 -0
- data/lib/rapns/apns/app.rb +8 -0
- data/lib/rapns/apns/binary_notification_validator.rb +12 -0
- data/lib/rapns/apns/device_token_format_validator.rb +12 -0
- data/lib/rapns/apns/feedback.rb +14 -0
- data/lib/rapns/apns/notification.rb +86 -0
- data/lib/rapns/apns/required_fields_validator.rb +14 -0
- data/lib/rapns/app.rb +29 -0
- data/lib/rapns/configuration.rb +46 -0
- data/lib/rapns/daemon/apns/app_runner.rb +36 -0
- data/lib/rapns/daemon/apns/connection.rb +113 -0
- data/lib/rapns/daemon/apns/delivery.rb +63 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
- data/lib/rapns/daemon/app_runner.rb +135 -0
- data/lib/rapns/daemon/database_reconnectable.rb +57 -0
- data/lib/rapns/daemon/delivery.rb +43 -0
- data/lib/rapns/daemon/delivery_error.rb +19 -0
- data/lib/rapns/daemon/delivery_handler.rb +46 -0
- data/lib/rapns/daemon/delivery_queue.rb +42 -0
- data/lib/rapns/daemon/delivery_queue_18.rb +44 -0
- data/lib/rapns/daemon/delivery_queue_19.rb +42 -0
- data/lib/rapns/daemon/feeder.rb +37 -0
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +206 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon/interruptible_sleep.rb +18 -0
- data/lib/rapns/daemon/logger.rb +68 -0
- data/lib/rapns/daemon.rb +136 -0
- data/lib/rapns/gcm/app.rb +7 -0
- data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rapns/gcm/notification.rb +31 -0
- data/lib/rapns/gcm/payload_size_validator.rb +13 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +54 -0
- data/lib/rapns/patches/rails/3.1.0/postgresql_adapter.rb +12 -0
- data/lib/rapns/patches/rails/3.1.1/postgresql_adapter.rb +17 -0
- data/lib/rapns/patches.rb +6 -0
- data/lib/rapns/version.rb +3 -0
- data/lib/rapns.rb +21 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/test.rake +33 -0
- data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
- data/spec/acceptance_spec_helper.rb +85 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/unit/apns/app_spec.rb +15 -0
- data/spec/unit/apns/feedback_spec.rb +12 -0
- data/spec/unit/apns/notification_spec.rb +198 -0
- data/spec/unit/app_spec.rb +18 -0
- data/spec/unit/configuration_spec.rb +38 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +39 -0
- data/spec/unit/daemon/apns/connection_spec.rb +234 -0
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +160 -0
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +118 -0
- data/spec/unit/daemon/app_runner_shared.rb +66 -0
- data/spec/unit/daemon/app_runner_spec.rb +129 -0
- data/spec/unit/daemon/database_reconnectable_spec.rb +109 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_handler_shared.rb +28 -0
- data/spec/unit/daemon/delivery_queue_spec.rb +29 -0
- data/spec/unit/daemon/feeder_spec.rb +95 -0
- data/spec/unit/daemon/gcm/app_runner_spec.rb +17 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +40 -0
- data/spec/unit/daemon/logger_spec.rb +156 -0
- data/spec/unit/daemon_spec.rb +139 -0
- data/spec/unit/gcm/app_spec.rb +5 -0
- data/spec/unit/gcm/notification_spec.rb +55 -0
- data/spec/unit/notification_shared.rb +38 -0
- data/spec/unit/notification_spec.rb +6 -0
- data/spec/unit_spec_helper.rb +145 -0
- metadata +240 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'unit_spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::AppRunner, 'stop' do
|
|
4
|
+
let(:runner) { stub }
|
|
5
|
+
before { Rapns::Daemon::AppRunner.runners['app'] = runner }
|
|
6
|
+
after { Rapns::Daemon::AppRunner.runners.clear }
|
|
7
|
+
|
|
8
|
+
it 'stops all runners' do
|
|
9
|
+
runner.should_receive(:stop)
|
|
10
|
+
Rapns::Daemon::AppRunner.stop
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe Rapns::Daemon::AppRunner, 'deliver' do
|
|
15
|
+
let(:runner) { stub }
|
|
16
|
+
let(:notification) { stub(:app_id => 1) }
|
|
17
|
+
let(:logger) { stub(:error => nil) }
|
|
18
|
+
|
|
19
|
+
before do
|
|
20
|
+
Rapns::Daemon.stub(:logger => logger)
|
|
21
|
+
Rapns::Daemon::AppRunner.runners[1] = runner
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
after { Rapns::Daemon::AppRunner.runners.clear }
|
|
25
|
+
|
|
26
|
+
it 'enqueues the notification' do
|
|
27
|
+
runner.should_receive(:enqueue).with(notification)
|
|
28
|
+
Rapns::Daemon::AppRunner.enqueue(notification)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'logs an error if there is no runner to deliver the notification' do
|
|
32
|
+
notification.stub(:app_id => 2, :id => 123)
|
|
33
|
+
logger.should_receive(:error).with("No such app '#{notification.app_id}' for notification #{notification.id}.")
|
|
34
|
+
Rapns::Daemon::AppRunner.enqueue(notification)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe Rapns::Daemon::AppRunner, 'sync' do
|
|
39
|
+
let(:app) { Rapns::Apns::App.new }
|
|
40
|
+
let(:new_app) { Rapns::Apns::App.new }
|
|
41
|
+
let(:runner) { stub(:sync => nil, :stop => nil, :start => nil) }
|
|
42
|
+
let(:logger) { stub(:error => nil) }
|
|
43
|
+
let(:queue) { Rapns::Daemon::DeliveryQueue.new }
|
|
44
|
+
|
|
45
|
+
before do
|
|
46
|
+
app.stub(:id => 1)
|
|
47
|
+
new_app.stub(:id => 2)
|
|
48
|
+
Rapns::Daemon::DeliveryQueue.stub(:new => queue)
|
|
49
|
+
Rapns::Daemon::AppRunner.runners[app.id] = runner
|
|
50
|
+
Rapns::App.stub(:all => [app])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
after { Rapns::Daemon::AppRunner.runners.clear }
|
|
54
|
+
|
|
55
|
+
it 'loads all apps' do
|
|
56
|
+
Rapns::App.should_receive(:all)
|
|
57
|
+
Rapns::Daemon::AppRunner.sync
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'instructs existing runners to sync' do
|
|
61
|
+
runner.should_receive(:sync).with(app)
|
|
62
|
+
Rapns::Daemon::AppRunner.sync
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'starts a runner for a new app' do
|
|
66
|
+
Rapns::App.stub(:all => [app, new_app])
|
|
67
|
+
new_runner = stub
|
|
68
|
+
Rapns::Daemon::Apns::AppRunner.should_receive(:new).with(new_app).and_return(new_runner)
|
|
69
|
+
new_runner.should_receive(:start)
|
|
70
|
+
Rapns::Daemon::AppRunner.sync
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'deletes old apps' do
|
|
74
|
+
Rapns::App.stub(:all => [])
|
|
75
|
+
runner.should_receive(:stop)
|
|
76
|
+
Rapns::Daemon::AppRunner.sync
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'logs an error if the app could not be started' do
|
|
80
|
+
Rapns::App.stub(:all => [app, new_app])
|
|
81
|
+
new_runner = stub
|
|
82
|
+
Rapns::Daemon::Apns::AppRunner.should_receive(:new).with(new_app).and_return(new_runner)
|
|
83
|
+
new_runner.stub(:start).and_raise(StandardError)
|
|
84
|
+
Rapns::Daemon.logger.should_receive(:error).any_number_of_times
|
|
85
|
+
Rapns::Daemon::AppRunner.sync
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe Rapns::Daemon::AppRunner, 'debug' do
|
|
90
|
+
let!(:app) { Rapns::Apns::App.create!(:name => 'test', :connections => 1,
|
|
91
|
+
:environment => 'development', :certificate => TEST_CERT) }
|
|
92
|
+
let(:logger) { stub(:info => nil) }
|
|
93
|
+
|
|
94
|
+
before do
|
|
95
|
+
Rapns::Daemon.stub(:config => {})
|
|
96
|
+
Rapns::Daemon::Apns::FeedbackReceiver.stub(:new => stub.as_null_object)
|
|
97
|
+
Rapns::Daemon::Apns::Connection.stub(:new => stub.as_null_object)
|
|
98
|
+
Rapns::Daemon.stub(:logger => logger)
|
|
99
|
+
Rapns::Daemon::AppRunner.sync
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
after { Rapns::Daemon::AppRunner.runners.clear }
|
|
103
|
+
|
|
104
|
+
it 'prints debug app states to the log' do
|
|
105
|
+
Rapns::Daemon.logger.should_receive(:info).with("\ntest:\n handlers: 1\n queued: 0\n idle: true\n")
|
|
106
|
+
Rapns::Daemon::AppRunner.debug
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe Rapns::Daemon::AppRunner, 'idle' do
|
|
111
|
+
let!(:app) { Rapns::Apns::App.create!(:name => 'test', :connections => 1,
|
|
112
|
+
:environment => 'development', :certificate => TEST_CERT) }
|
|
113
|
+
let(:logger) { stub(:info => nil) }
|
|
114
|
+
|
|
115
|
+
before do
|
|
116
|
+
Rapns::Daemon.stub(:config => {})
|
|
117
|
+
Rapns::Daemon::Apns::FeedbackReceiver.stub(:new => stub.as_null_object)
|
|
118
|
+
Rapns::Daemon::Apns::Connection.stub(:new => stub.as_null_object)
|
|
119
|
+
Rapns::Daemon.stub(:logger => logger)
|
|
120
|
+
Rapns::Daemon::AppRunner.sync
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
after { Rapns::Daemon::AppRunner.runners.clear }
|
|
124
|
+
|
|
125
|
+
it 'returns idle runners' do
|
|
126
|
+
runner = Rapns::Daemon::AppRunner.runners[app.id]
|
|
127
|
+
Rapns::Daemon::AppRunner.idle.should == [runner]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require "unit_spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::DatabaseReconnectable do
|
|
4
|
+
class TestDouble
|
|
5
|
+
include Rapns::Daemon::DatabaseReconnectable
|
|
6
|
+
|
|
7
|
+
attr_reader :name
|
|
8
|
+
|
|
9
|
+
def initialize(error, max_calls)
|
|
10
|
+
@error = error
|
|
11
|
+
@max_calls = max_calls
|
|
12
|
+
@calls = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def perform
|
|
16
|
+
with_database_reconnect_and_retry do
|
|
17
|
+
@calls += 1
|
|
18
|
+
raise @error if @calls <= @max_calls
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
let(:adapter_error_class) do
|
|
24
|
+
case $adapter
|
|
25
|
+
when 'postgresql'
|
|
26
|
+
PGError
|
|
27
|
+
when 'mysql'
|
|
28
|
+
Mysql::Error
|
|
29
|
+
when 'mysql2'
|
|
30
|
+
Mysql2::Error
|
|
31
|
+
when 'jdbcpostgresql'
|
|
32
|
+
ActiveRecord::JDBCError
|
|
33
|
+
when 'jdbcmysql'
|
|
34
|
+
ActiveRecord::JDBCError
|
|
35
|
+
else
|
|
36
|
+
raise "Please update #{__FILE__} for adapter #{$adapter}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
let(:error) { adapter_error_class.new("db down!") }
|
|
40
|
+
let(:test_double) { TestDouble.new(error, 1) }
|
|
41
|
+
|
|
42
|
+
before do
|
|
43
|
+
@logger = mock("Logger", :info => nil, :error => nil, :warn => nil)
|
|
44
|
+
Rapns::Daemon.stub(:logger).and_return(@logger)
|
|
45
|
+
|
|
46
|
+
ActiveRecord::Base.stub(:clear_all_connections!)
|
|
47
|
+
ActiveRecord::Base.stub(:establish_connection)
|
|
48
|
+
test_double.stub(:sleep)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "should log the error raised" do
|
|
52
|
+
Rapns::Daemon.logger.should_receive(:error).with(error)
|
|
53
|
+
test_double.perform
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "should log that the database is being reconnected" do
|
|
57
|
+
Rapns::Daemon.logger.should_receive(:warn).with("Lost connection to database, reconnecting...")
|
|
58
|
+
test_double.perform
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "should log the reconnection attempt" do
|
|
62
|
+
Rapns::Daemon.logger.should_receive(:warn).with("Attempt 1")
|
|
63
|
+
test_double.perform
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "should clear all connections" do
|
|
67
|
+
ActiveRecord::Base.should_receive(:clear_all_connections!)
|
|
68
|
+
test_double.perform
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "should establish a new connection" do
|
|
72
|
+
ActiveRecord::Base.should_receive(:establish_connection)
|
|
73
|
+
test_double.perform
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "should test out the new connection by performing a count" do
|
|
77
|
+
Rapns::Notification.should_receive(:count)
|
|
78
|
+
test_double.perform
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context "when the reconnection attempt is not successful" do
|
|
82
|
+
before do
|
|
83
|
+
class << Rapns::Notification
|
|
84
|
+
def count
|
|
85
|
+
@count_calls += 1
|
|
86
|
+
return if @count_calls == 2
|
|
87
|
+
raise @error
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
Rapns::Notification.instance_variable_set("@count_calls", 0)
|
|
91
|
+
Rapns::Notification.instance_variable_set("@error", error)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "should log the 2nd attempt" do
|
|
95
|
+
Rapns::Daemon.logger.should_receive(:warn).with("Attempt 2")
|
|
96
|
+
test_double.perform
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "should log errors raised when the reconnection is not successful without notifying airbrake" do
|
|
100
|
+
Rapns::Daemon.logger.should_receive(:error).with(error, :airbrake_notify => false)
|
|
101
|
+
test_double.perform
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "should sleep to avoid thrashing when the database is down" do
|
|
105
|
+
test_double.should_receive(:sleep).with(2)
|
|
106
|
+
test_double.perform
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "unit_spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Rapns::DeliveryError do
|
|
4
|
+
let(:error) { Rapns::DeliveryError.new(4, 12, "Missing payload") }
|
|
5
|
+
|
|
6
|
+
it "returns an informative message" do
|
|
7
|
+
error.to_s.should == "Unable to deliver notification 12, received error 4 (Missing payload)"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "returns the error code" do
|
|
11
|
+
error.code.should == 4
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
shared_examples_for 'an DeliveryHandler subclass' do
|
|
2
|
+
it 'logs all delivery errors' do
|
|
3
|
+
logger = stub
|
|
4
|
+
Rapns::Daemon.stub(:logger => logger)
|
|
5
|
+
error = StandardError.new
|
|
6
|
+
delivery_handler.stub(:deliver).and_raise(error)
|
|
7
|
+
Rapns::Daemon.logger.should_receive(:error).with(error)
|
|
8
|
+
delivery_handler.send(:handle_next_notification)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "instructs the queue to wakeup the thread when told to stop" do
|
|
12
|
+
thread = stub(:join => nil)
|
|
13
|
+
Thread.stub(:new => thread)
|
|
14
|
+
queue.should_receive(:wakeup).with(thread)
|
|
15
|
+
delivery_handler.start
|
|
16
|
+
delivery_handler.stop
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "when being stopped" do
|
|
20
|
+
before { queue.pop }
|
|
21
|
+
|
|
22
|
+
it "does not attempt to deliver a notification when a DeliveryQueue::::WakeupError is raised" do
|
|
23
|
+
queue.stub(:pop).and_raise(Rapns::Daemon::DeliveryQueue::WakeupError)
|
|
24
|
+
delivery_handler.should_not_receive(:deliver)
|
|
25
|
+
delivery_handler.send(:handle_next_notification)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "unit_spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::DeliveryQueue do
|
|
4
|
+
let(:queue) { Rapns::Daemon::DeliveryQueue.new }
|
|
5
|
+
|
|
6
|
+
it 'behaves likes a normal qeue' do
|
|
7
|
+
obj = stub
|
|
8
|
+
queue.push obj
|
|
9
|
+
queue.pop.should == obj
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'returns false if notifications have not all been processed' do
|
|
13
|
+
queue.push stub
|
|
14
|
+
queue.notifications_processed?.should be_false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'returns false if the queue is empty but notifications have not all been processed' do
|
|
18
|
+
queue.push stub
|
|
19
|
+
queue.pop
|
|
20
|
+
queue.notifications_processed?.should be_false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'returns true if all notifications have been processed' do
|
|
24
|
+
queue.push stub
|
|
25
|
+
queue.pop
|
|
26
|
+
queue.notification_processed
|
|
27
|
+
queue.notifications_processed?.should be_true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require "unit_spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::Feeder do
|
|
4
|
+
let(:config) { stub(:batch_size => 5000) }
|
|
5
|
+
let!(:app) { Rapns::Apns::App.create!(:name => 'my_app', :environment => 'development', :certificate => TEST_CERT) }
|
|
6
|
+
let(:notification) { Rapns::Apns::Notification.create!(:device_token => "a" * 64, :app => app) }
|
|
7
|
+
let(:logger) { stub }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
Rapns::Daemon.stub(:logger => logger, :config => config)
|
|
11
|
+
Rapns::Daemon::Feeder.instance_variable_set("@stop", true)
|
|
12
|
+
Rapns::Daemon::AppRunner.stub(:idle => [stub(:app => app)])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def start
|
|
16
|
+
Rapns::Daemon::Feeder.start(0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "checks for new notifications with the ability to reconnect the database" do
|
|
20
|
+
Rapns::Daemon::Feeder.should_receive(:with_database_reconnect_and_retry)
|
|
21
|
+
start
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'loads notifications in batches' do
|
|
25
|
+
relation = stub.as_null_object
|
|
26
|
+
relation.should_receive(:limit).with(5000)
|
|
27
|
+
Rapns::Notification.stub(:ready_for_delivery => relation)
|
|
28
|
+
start
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "enqueue the notification" do
|
|
32
|
+
notification.update_attributes!(:delivered => false)
|
|
33
|
+
Rapns::Daemon::AppRunner.should_receive(:enqueue).with(notification)
|
|
34
|
+
start
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'does not enqueue the notification if the app runner is still processing the previous batch' do
|
|
38
|
+
Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
|
|
39
|
+
start
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "enqueues an undelivered notification without deliver_after set" do
|
|
43
|
+
notification.update_attributes!(:delivered => false, :deliver_after => nil)
|
|
44
|
+
Rapns::Daemon::AppRunner.should_receive(:enqueue).with(notification)
|
|
45
|
+
start
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "enqueues a notification with a deliver_after time in the past" do
|
|
49
|
+
notification.update_attributes!(:delivered => false, :deliver_after => 1.hour.ago)
|
|
50
|
+
Rapns::Daemon::AppRunner.should_receive(:enqueue).with(notification)
|
|
51
|
+
start
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "does not enqueue a notification with a deliver_after time in the future" do
|
|
55
|
+
notification.update_attributes!(:delivered => false, :deliver_after => 1.hour.from_now)
|
|
56
|
+
Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
|
|
57
|
+
start
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "does not enqueue a previously delivered notification" do
|
|
61
|
+
notification.update_attributes!(:delivered => true, :delivered_at => Time.now)
|
|
62
|
+
Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
|
|
63
|
+
start
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "does not enqueue a notification that has previously failed delivery" do
|
|
67
|
+
notification.update_attributes!(:delivered => false, :failed => true)
|
|
68
|
+
Rapns::Daemon::AppRunner.should_not_receive(:enqueue)
|
|
69
|
+
start
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "logs errors" do
|
|
73
|
+
e = StandardError.new("bork")
|
|
74
|
+
Rapns::Notification.stub(:ready_for_delivery).and_raise(e)
|
|
75
|
+
Rapns::Daemon.logger.should_receive(:error).with(e)
|
|
76
|
+
start
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "interrupts sleep when stopped" do
|
|
80
|
+
Rapns::Daemon::Feeder.should_receive(:interrupt_sleep)
|
|
81
|
+
Rapns::Daemon::Feeder.stop
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "enqueues notifications when started" do
|
|
85
|
+
Rapns::Daemon::Feeder.should_receive(:enqueue_notifications).at_least(:once)
|
|
86
|
+
Rapns::Daemon::Feeder.stub(:loop).and_yield
|
|
87
|
+
start
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "sleeps for the given period" do
|
|
91
|
+
Rapns::Daemon::Feeder.should_receive(:interruptible_sleep).with(2)
|
|
92
|
+
Rapns::Daemon::Feeder.stub(:loop).and_yield
|
|
93
|
+
Rapns::Daemon::Feeder.start(2)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'unit_spec_helper'
|
|
2
|
+
require File.dirname(__FILE__) + '/../app_runner_shared.rb'
|
|
3
|
+
|
|
4
|
+
describe Rapns::Daemon::Gcm::AppRunner do
|
|
5
|
+
it_behaves_like 'an AppRunner subclass'
|
|
6
|
+
|
|
7
|
+
let(:app_class) { Rapns::Gcm::App }
|
|
8
|
+
let(:app) { app_class.new }
|
|
9
|
+
let(:runner) { Rapns::Daemon::Gcm::AppRunner.new(app) }
|
|
10
|
+
let(:handler) { stub(:start => nil, :stop => nil, :queue= => nil) }
|
|
11
|
+
let(:logger) { stub(:info => nil) }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
Rapns::Daemon.stub(:logger => logger)
|
|
15
|
+
Rapns::Daemon::Gcm::DeliveryHandler.stub(:new => handler)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "unit_spec_helper"
|
|
2
|
+
require File.dirname(__FILE__) + '/../delivery_handler_shared.rb'
|
|
3
|
+
|
|
4
|
+
describe Rapns::Daemon::Gcm::DeliveryHandler do
|
|
5
|
+
it_should_behave_like 'an DeliveryHandler subclass'
|
|
6
|
+
|
|
7
|
+
let(:app) { stub }
|
|
8
|
+
let(:delivery_handler) { Rapns::Daemon::Gcm::DeliveryHandler.new(app) }
|
|
9
|
+
let(:notification) { stub }
|
|
10
|
+
let(:http) { stub(:shutdown => nil)}
|
|
11
|
+
let(:queue) { Rapns::Daemon::DeliveryQueue.new }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
Net::HTTP::Persistent.stub(:new => http)
|
|
15
|
+
Rapns::Daemon::Gcm::Delivery.stub(:perform)
|
|
16
|
+
delivery_handler.queue = queue
|
|
17
|
+
queue.push(notification)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'performs delivery of an notification' do
|
|
21
|
+
Rapns::Daemon::Gcm::Delivery.should_receive(:perform).with(app, http, notification)
|
|
22
|
+
delivery_handler.start
|
|
23
|
+
delivery_handler.stop
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'initiates a persistent connection object' do
|
|
27
|
+
Net::HTTP::Persistent.should_receive(:new).with('rapns')
|
|
28
|
+
Rapns::Daemon::Gcm::DeliveryHandler.new(app)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'shuts down the http connection stopped' do
|
|
32
|
+
http.should_receive(:shutdown)
|
|
33
|
+
delivery_handler.start
|
|
34
|
+
delivery_handler.stop
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
require 'unit_spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Rapns::Daemon::Gcm::Delivery do
|
|
4
|
+
let(:app) { Rapns::Gcm::App.new(:name => 'MyApp', :auth_key => 'abc123') }
|
|
5
|
+
let(:notification) { Rapns::Gcm::Notification.create!(:app => app, :registration_ids => ['xyz']) }
|
|
6
|
+
let(:logger) { stub(:error => nil, :info => nil, :warn => nil) }
|
|
7
|
+
let(:response) { stub(:code => 200, :header => {}) }
|
|
8
|
+
let(:http) { stub(:shutdown => nil, :request => response)}
|
|
9
|
+
let(:now) { Time.parse('2012-10-14 00:00:00') }
|
|
10
|
+
|
|
11
|
+
def perform
|
|
12
|
+
Rapns::Daemon::Gcm::Delivery.perform(app, http, notification)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
Time.stub(:now => now)
|
|
17
|
+
Rapns::Daemon.stub(:logger => logger)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe 'an 200 response' do
|
|
21
|
+
before do
|
|
22
|
+
response.stub(:code => 200)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'marks the notification as delivered if delivered successfully to all devices' do
|
|
26
|
+
response.stub(:body => JSON.dump({ 'failure' => 0 }))
|
|
27
|
+
expect do
|
|
28
|
+
perform
|
|
29
|
+
end.to change(notification, :delivered).to(true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'logs that the notification was delivered' do
|
|
33
|
+
response.stub(:body => JSON.dump({ 'failure' => 0 }))
|
|
34
|
+
logger.should_receive(:info).with("[MyApp] 1 sent to xyz")
|
|
35
|
+
perform
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'marks a notification as failed if any deliveries failed that cannot be retried.' do
|
|
39
|
+
body = {
|
|
40
|
+
'failure' => 1,
|
|
41
|
+
'success' => 1,
|
|
42
|
+
'results' => [
|
|
43
|
+
{ 'message_id' => '1:000' },
|
|
44
|
+
{ 'error' => 'NotRegistered' }
|
|
45
|
+
]}
|
|
46
|
+
response.stub(:body => JSON.dump(body))
|
|
47
|
+
perform rescue Rapns::DeliveryError
|
|
48
|
+
notification.reload
|
|
49
|
+
notification.failed.should be_true
|
|
50
|
+
notification.error_code = nil
|
|
51
|
+
notification.error_description = "Weee"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe 'all deliveries returned Unavailable or InternalServerError' do
|
|
55
|
+
let(:body) {{
|
|
56
|
+
'failure' => 2,
|
|
57
|
+
'success' => 0,
|
|
58
|
+
'results' => [
|
|
59
|
+
{ 'error' => 'Unavailable' },
|
|
60
|
+
{ 'error' => 'Unavailable' }
|
|
61
|
+
]}}
|
|
62
|
+
|
|
63
|
+
before { response.stub(:body => JSON.dump(body)) }
|
|
64
|
+
|
|
65
|
+
it 'retries the notification respecting the Retry-After header' do
|
|
66
|
+
response.stub(:header => { 'retry-after' => 10 })
|
|
67
|
+
perform
|
|
68
|
+
notification.reload
|
|
69
|
+
notification.retries.should == 1
|
|
70
|
+
notification.deliver_after.should == now + 10.seconds
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'retries the notification using exponential back-off if the Retry-After header is not present' do
|
|
74
|
+
notification.update_attribute(:retries, 8)
|
|
75
|
+
perform
|
|
76
|
+
notification.reload
|
|
77
|
+
notification.retries.should == 9
|
|
78
|
+
notification.deliver_after.should == now + 2 ** 9
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'does not mark the notification as failed' do
|
|
82
|
+
expect do
|
|
83
|
+
perform
|
|
84
|
+
notification.reload
|
|
85
|
+
end.to_not change(notification, :failed).to(true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'logs that the notification will be retried' do
|
|
89
|
+
Rapns::Daemon.logger.should_receive(:warn).with("All recipients unavailable. Notification #{notification.id} will be retired after 2012-10-14 00:00:02 (retry 1).")
|
|
90
|
+
perform
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
shared_examples_for 'an notification with some delivery failures' do
|
|
95
|
+
let(:new_notification) { Rapns::Gcm::Notification.where('id != ?', notification.id).first }
|
|
96
|
+
|
|
97
|
+
before { response.stub(:body => JSON.dump(body)) }
|
|
98
|
+
|
|
99
|
+
it 'marks the original notification as failed' do
|
|
100
|
+
perform rescue Rapns::DeliveryError
|
|
101
|
+
notification.reload
|
|
102
|
+
notification.failed.should be_true
|
|
103
|
+
notification.failed_at = now
|
|
104
|
+
notification.error_code.should be_nil
|
|
105
|
+
notification.error_description.should == error_description
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'creates a new notification for the unavailable devices' do
|
|
109
|
+
notification.update_attributes(:registration_ids => ['id_0', 'id_1', 'id_2'], :data => {'one' => 1}, :collapse_key => 'thing', :delay_while_idle => true)
|
|
110
|
+
perform rescue Rapns::DeliveryError
|
|
111
|
+
new_notification.registration_ids.should == ['id_0', 'id_2']
|
|
112
|
+
new_notification.data.should == {'one' => 1}
|
|
113
|
+
new_notification.collapse_key.should == 'thing'
|
|
114
|
+
new_notification.delay_while_idle.should be_true
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'sets the delivery time on the new notification to respect the Retry-After header' do
|
|
118
|
+
response.stub(:header => { 'retry-after' => 10 })
|
|
119
|
+
perform rescue Rapns::DeliveryError
|
|
120
|
+
new_notification.deliver_after.should == now + 10.seconds
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'raises a DeliveryError' do
|
|
124
|
+
expect { perform }.to raise_error(Rapns::DeliveryError)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe 'all deliveries failed with some as Unavailable or InternalServerError' do
|
|
129
|
+
let(:body) {{
|
|
130
|
+
'failure' => 3,
|
|
131
|
+
'success' => 0,
|
|
132
|
+
'results' => [
|
|
133
|
+
{ 'error' => 'Unavailable' },
|
|
134
|
+
{ 'error' => 'NotRegistered' },
|
|
135
|
+
{ 'error' => 'Unavailable' }
|
|
136
|
+
]}}
|
|
137
|
+
let(:error_description) { "Failed to deliver to recipients 0, 1, 2. Errors: Unavailable, NotRegistered, Unavailable. 0, 2 will be retried as notification 2." }
|
|
138
|
+
it_should_behave_like 'an notification with some delivery failures'
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe 'some deliveries failed with Unavailable or InternalServerError' do
|
|
143
|
+
let(:body) {{
|
|
144
|
+
'failure' => 2,
|
|
145
|
+
'success' => 1,
|
|
146
|
+
'results' => [
|
|
147
|
+
{ 'error' => 'Unavailable' },
|
|
148
|
+
{ 'message_id' => '1:000' },
|
|
149
|
+
{ 'error' => 'InternalServerError' }
|
|
150
|
+
]}}
|
|
151
|
+
let(:error_description) { "Failed to deliver to recipients 0, 2. Errors: Unavailable, InternalServerError. 0, 2 will be retried as notification 2." }
|
|
152
|
+
it_should_behave_like 'an notification with some delivery failures'
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
describe 'an 503 response' do
|
|
156
|
+
before { response.stub(:code => 503) }
|
|
157
|
+
|
|
158
|
+
it 'logs a warning that the notification will be retried.' do
|
|
159
|
+
logger.should_receive(:warn).with("GCM responded with an Service Unavailable Error. Notification 1 will be retired after 2012-10-14 00:00:02 (retry 1).")
|
|
160
|
+
perform
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'respects an integer Retry-After header' do
|
|
164
|
+
response.stub(:header => { 'retry-after' => 10 })
|
|
165
|
+
expect do
|
|
166
|
+
perform
|
|
167
|
+
end.to change(notification, :deliver_after).to(now + 10)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'respects a HTTP-date Retry-After header' do
|
|
171
|
+
response.stub(:header => { 'retry-after' => 'Wed, 03 Oct 2012 20:55:11 GMT' })
|
|
172
|
+
expect do
|
|
173
|
+
perform
|
|
174
|
+
end.to change(notification, :deliver_after).to(Time.parse('Wed, 03 Oct 2012 20:55:11 GMT'))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'defaults to exponential back-off if the Retry-After header is not present' do
|
|
178
|
+
expect do
|
|
179
|
+
perform
|
|
180
|
+
end.to change(notification, :deliver_after).to(now + 2 ** 1)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe 'an 500 response' do
|
|
185
|
+
before do
|
|
186
|
+
notification.update_attribute(:retries, 2)
|
|
187
|
+
response.stub(:code => 500)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'logs a warning that the notification has been re-queued.' do
|
|
191
|
+
Rapns::Daemon.logger.should_receive(:warn).with("GCM responded with an Internal Error. Notification #{notification.id} will be retired after #{(now + 2 ** 3).strftime("%Y-%m-%d %H:%M:%S")} (retry 3).")
|
|
192
|
+
perform
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'sets deliver_after on the notification in accordance with the exponential back-off strategy.' do
|
|
196
|
+
expect do
|
|
197
|
+
perform
|
|
198
|
+
notification.reload
|
|
199
|
+
end.to change(notification, :deliver_after).to(now + 2 ** 3)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe 'an 401 response' do
|
|
204
|
+
before { response.stub(:code => 401) }
|
|
205
|
+
|
|
206
|
+
it 'raises an error' do
|
|
207
|
+
expect { perform }.to raise_error(Rapns::DeliveryError)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
describe 'an 400 response' do
|
|
212
|
+
before { response.stub(:code => 400) }
|
|
213
|
+
|
|
214
|
+
it 'marks the notification as failed' do
|
|
215
|
+
perform rescue Rapns::DeliveryError
|
|
216
|
+
notification.reload
|
|
217
|
+
notification.failed.should be_true
|
|
218
|
+
notification.failed_at.should == now
|
|
219
|
+
notification.error_code.should == 400
|
|
220
|
+
notification.error_description.should == 'GCM failed to parse the JSON request. Possibly an rapns bug, please open an issue.'
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
describe 'an un-handled response' do
|
|
225
|
+
before { response.stub(:code => 418) }
|
|
226
|
+
|
|
227
|
+
it 'marks the notification as failed' do
|
|
228
|
+
perform rescue Rapns::DeliveryError
|
|
229
|
+
notification.reload
|
|
230
|
+
notification.failed.should be_true
|
|
231
|
+
notification.failed_at.should == now
|
|
232
|
+
notification.error_code.should == 418
|
|
233
|
+
notification.error_description.should == "I'm a Teapot"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|