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
data/lib/rapns/app.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
class App < ActiveRecord::Base
|
|
3
|
+
self.table_name = 'rapns_apps'
|
|
4
|
+
|
|
5
|
+
attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key
|
|
6
|
+
|
|
7
|
+
has_many :notifications
|
|
8
|
+
|
|
9
|
+
validates :name, :presence => true, :uniqueness => { :scope => [:type, :environment] }
|
|
10
|
+
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
|
|
11
|
+
|
|
12
|
+
validate :certificate_has_matching_private_key
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def certificate_has_matching_private_key
|
|
17
|
+
result = false
|
|
18
|
+
if certificate.present?
|
|
19
|
+
x509 = OpenSSL::X509::Certificate.new certificate rescue nil
|
|
20
|
+
pkey = OpenSSL::PKey::RSA.new certificate rescue nil
|
|
21
|
+
result = !x509.nil? && !pkey.nil?
|
|
22
|
+
unless result
|
|
23
|
+
errors.add :certificate, 'Certificate value must contain a certificate and a private key.'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
def self.config
|
|
3
|
+
@config ||= Rapns::Configuration.new
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def self.configure
|
|
7
|
+
yield config if block_given?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll,
|
|
11
|
+
:airbrake_notify, :check_for_errors, :pid_file, :batch_size]
|
|
12
|
+
|
|
13
|
+
class Configuration < Struct.new(*CONFIG_ATTRS)
|
|
14
|
+
attr_accessor :apns_feedback_callback
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
self.foreground = false
|
|
20
|
+
self.push_poll = 2
|
|
21
|
+
self.feedback_poll = 60
|
|
22
|
+
self.airbrake_notify = true
|
|
23
|
+
self.check_for_errors = true
|
|
24
|
+
self.batch_size = 5000
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update(other)
|
|
28
|
+
CONFIG_ATTRS.each do |attr|
|
|
29
|
+
other_value = other.send(attr)
|
|
30
|
+
send("#{attr}=", other_value) unless other_value.nil?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_apns_feedback(&block)
|
|
35
|
+
self.apns_feedback_callback = block
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pid_file=(path)
|
|
39
|
+
if path && !Pathname.new(path).absolute?
|
|
40
|
+
super(File.join(Rails.root, path))
|
|
41
|
+
else
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Apns
|
|
4
|
+
class AppRunner < Rapns::Daemon::AppRunner
|
|
5
|
+
ENVIRONMENTS = {
|
|
6
|
+
:production => {
|
|
7
|
+
:push => ['gateway.push.apple.com', 2195],
|
|
8
|
+
:feedback => ['feedback.push.apple.com', 2196]
|
|
9
|
+
},
|
|
10
|
+
:development => {
|
|
11
|
+
:push => ['gateway.sandbox.push.apple.com', 2195],
|
|
12
|
+
:feedback => ['feedback.sandbox.push.apple.com', 2196]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
def started
|
|
19
|
+
poll = Rapns.config[:feedback_poll]
|
|
20
|
+
host, port = ENVIRONMENTS[app.environment.to_sym][:feedback]
|
|
21
|
+
@feedback_receiver = FeedbackReceiver.new(app, host, port, poll)
|
|
22
|
+
@feedback_receiver.start
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stopped
|
|
26
|
+
@feedback_receiver.stop if @feedback_receiver
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def new_delivery_handler
|
|
30
|
+
push_host, push_port = ENVIRONMENTS[app.environment.to_sym][:push]
|
|
31
|
+
DeliveryHandler.new(app, push_host, push_port)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Apns
|
|
4
|
+
class ConnectionError < StandardError; end
|
|
5
|
+
|
|
6
|
+
class Connection
|
|
7
|
+
attr_accessor :last_write
|
|
8
|
+
|
|
9
|
+
def self.idle_period
|
|
10
|
+
30.minutes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(name, host, port, certificate, password)
|
|
14
|
+
@name = name
|
|
15
|
+
@host = host
|
|
16
|
+
@port = port
|
|
17
|
+
@certificate = certificate
|
|
18
|
+
@password = password
|
|
19
|
+
written
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def connect
|
|
23
|
+
@ssl_context = setup_ssl_context
|
|
24
|
+
@tcp_socket, @ssl_socket = connect_socket
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def close
|
|
28
|
+
begin
|
|
29
|
+
@ssl_socket.close if @ssl_socket
|
|
30
|
+
@tcp_socket.close if @tcp_socket
|
|
31
|
+
rescue IOError
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read(num_bytes)
|
|
36
|
+
@ssl_socket.read(num_bytes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def select(timeout)
|
|
40
|
+
IO.select([@ssl_socket], nil, nil, timeout)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write(data)
|
|
44
|
+
reconnect_idle if idle_period_exceeded?
|
|
45
|
+
|
|
46
|
+
retry_count = 0
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
write_data(data)
|
|
50
|
+
rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
|
|
51
|
+
retry_count += 1;
|
|
52
|
+
|
|
53
|
+
if retry_count == 1
|
|
54
|
+
Rapns::Daemon.logger.error("[#{@name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if retry_count <= 3
|
|
58
|
+
reconnect
|
|
59
|
+
sleep 1
|
|
60
|
+
retry
|
|
61
|
+
else
|
|
62
|
+
raise ConnectionError, "#{@name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def reconnect
|
|
68
|
+
close
|
|
69
|
+
@tcp_socket, @ssl_socket = connect_socket
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def reconnect_idle
|
|
75
|
+
Rapns::Daemon.logger.info("[#{@name}] Idle period exceeded, reconnecting...")
|
|
76
|
+
reconnect
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def idle_period_exceeded?
|
|
80
|
+
Time.now - last_write > self.class.idle_period
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def write_data(data)
|
|
84
|
+
@ssl_socket.write(data)
|
|
85
|
+
@ssl_socket.flush
|
|
86
|
+
written
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def written
|
|
90
|
+
self.last_write = Time.now
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def setup_ssl_context
|
|
94
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
95
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
|
|
96
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
|
|
97
|
+
ssl_context
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def connect_socket
|
|
101
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
|
102
|
+
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
|
103
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
104
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
|
105
|
+
ssl_socket.sync = true
|
|
106
|
+
ssl_socket.connect
|
|
107
|
+
Rapns::Daemon.logger.info("[#{@name}] Connected to #{@host}:#{@port}")
|
|
108
|
+
[tcp_socket, ssl_socket]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Apns
|
|
4
|
+
class Delivery < Rapns::Daemon::Delivery
|
|
5
|
+
SELECT_TIMEOUT = 0.2
|
|
6
|
+
ERROR_TUPLE_BYTES = 6
|
|
7
|
+
APN_ERRORS = {
|
|
8
|
+
1 => "Processing error",
|
|
9
|
+
2 => "Missing device token",
|
|
10
|
+
3 => "Missing topic",
|
|
11
|
+
4 => "Missing payload",
|
|
12
|
+
5 => "Missing token size",
|
|
13
|
+
6 => "Missing topic size",
|
|
14
|
+
7 => "Missing payload size",
|
|
15
|
+
8 => "Invalid token",
|
|
16
|
+
255 => "None (unknown error)"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def initialize(app, conneciton, notification)
|
|
20
|
+
@app = app
|
|
21
|
+
@connection = conneciton
|
|
22
|
+
@notification = notification
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def perform
|
|
26
|
+
begin
|
|
27
|
+
@connection.write(@notification.to_binary)
|
|
28
|
+
check_for_error if Rapns.config.check_for_errors
|
|
29
|
+
mark_delivered
|
|
30
|
+
Rapns::Daemon.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.device_token}")
|
|
31
|
+
rescue Rapns::DeliveryError, Rapns::Apns::DisconnectionError => error
|
|
32
|
+
mark_failed(error.code, error.description)
|
|
33
|
+
raise
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def check_for_error
|
|
40
|
+
if @connection.select(SELECT_TIMEOUT)
|
|
41
|
+
error = nil
|
|
42
|
+
|
|
43
|
+
if tuple = @connection.read(ERROR_TUPLE_BYTES)
|
|
44
|
+
cmd, code, notification_id = tuple.unpack("ccN")
|
|
45
|
+
|
|
46
|
+
description = APN_ERRORS[code.to_i] || "Unknown error. Possible rapns bug?"
|
|
47
|
+
error = Rapns::DeliveryError.new(code, notification_id, description)
|
|
48
|
+
else
|
|
49
|
+
error = Rapns::Apns::DisconnectionError.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
Rapns::Daemon.logger.error("[#{@app.name}] Error received, reconnecting...")
|
|
54
|
+
@connection.reconnect
|
|
55
|
+
ensure
|
|
56
|
+
raise error if error
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Apns
|
|
4
|
+
class DeliveryHandler < Rapns::Daemon::DeliveryHandler
|
|
5
|
+
def initialize(app, host, port)
|
|
6
|
+
@app = app
|
|
7
|
+
@connection = Connection.new(@app.name, host, port, @app.certificate, @app.password)
|
|
8
|
+
@connection.connect
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def deliver(notification)
|
|
12
|
+
Rapns::Daemon::Apns::Delivery.perform(@app, @connection, notification)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stopped
|
|
16
|
+
@connection.close
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Apns
|
|
3
|
+
class DisconnectionError < StandardError
|
|
4
|
+
attr_reader :code, :description
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@code = nil
|
|
8
|
+
@description = "APNs disconnected without returning an error."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
message
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def message
|
|
16
|
+
"The APNs disconnected without returning an error. This may indicate you are using an invalid certificate for the host."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Apns
|
|
4
|
+
class FeedbackReceiver
|
|
5
|
+
include InterruptibleSleep
|
|
6
|
+
include DatabaseReconnectable
|
|
7
|
+
|
|
8
|
+
FEEDBACK_TUPLE_BYTES = 38
|
|
9
|
+
|
|
10
|
+
def initialize(app, host, port, poll)
|
|
11
|
+
@app = app
|
|
12
|
+
@host = host
|
|
13
|
+
@port = port
|
|
14
|
+
@poll = poll
|
|
15
|
+
@certificate = app.certificate
|
|
16
|
+
@password = app.password
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
@thread = Thread.new do
|
|
21
|
+
loop do
|
|
22
|
+
break if @stop
|
|
23
|
+
check_for_feedback
|
|
24
|
+
interruptible_sleep @poll
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stop
|
|
30
|
+
@stop = true
|
|
31
|
+
interrupt_sleep
|
|
32
|
+
@thread.join if @thread
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check_for_feedback
|
|
36
|
+
connection = nil
|
|
37
|
+
begin
|
|
38
|
+
connection = Connection.new("FeedbackReceiver:#{@app.name}", @host, @port, @certificate, @password)
|
|
39
|
+
connection.connect
|
|
40
|
+
|
|
41
|
+
while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
|
|
42
|
+
timestamp, device_token = parse_tuple(tuple)
|
|
43
|
+
create_feedback(timestamp, device_token)
|
|
44
|
+
end
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Rapns::Daemon.logger.error(e)
|
|
47
|
+
ensure
|
|
48
|
+
connection.close if connection
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
def parse_tuple(tuple)
|
|
55
|
+
failed_at, _, device_token = tuple.unpack("N1n1H*")
|
|
56
|
+
[Time.at(failed_at).utc, device_token]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def create_feedback(failed_at, device_token)
|
|
60
|
+
formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
61
|
+
with_database_reconnect_and_retry do
|
|
62
|
+
Rapns::Daemon.logger.info("[FeedbackReceiver:#{@app.name}] Delivery failed at #{formatted_failed_at} for #{device_token}")
|
|
63
|
+
feedback = Rapns::Apns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
|
|
64
|
+
begin
|
|
65
|
+
Rapns.config.apns_feedback_callback.call(feedback) if Rapns.config.apns_feedback_callback
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
Rapns::Daemon.logger.error(e)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class AppRunner
|
|
4
|
+
class << self
|
|
5
|
+
attr_reader :runners # TODO: Needed?
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
@runners = {}
|
|
9
|
+
|
|
10
|
+
def self.enqueue(notification)
|
|
11
|
+
if app = runners[notification.app_id]
|
|
12
|
+
app.enqueue(notification)
|
|
13
|
+
else
|
|
14
|
+
Rapns::Daemon.logger.error("No such app '#{notification.app_id}' for notification #{notification.id}.")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.sync
|
|
19
|
+
apps = Rapns::App.all
|
|
20
|
+
apps.each { |app| sync_app(app) }
|
|
21
|
+
removed = runners.keys - apps.map(&:id)
|
|
22
|
+
removed.each { |app_id| runners.delete(app_id).stop }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.sync_app(app)
|
|
26
|
+
if runners[app.id]
|
|
27
|
+
runners[app.id].sync(app)
|
|
28
|
+
else
|
|
29
|
+
runner = new_runner(app)
|
|
30
|
+
begin
|
|
31
|
+
runner.start
|
|
32
|
+
runners[app.id] = runner
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
Rapns::Daemon.logger.error("[#{app.name}] Exception raised during startup. Notifications will not be delivered for this app.")
|
|
35
|
+
Rapns::Daemon.logger.error(e)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.new_runner(app)
|
|
41
|
+
type = app.class.parent.name.demodulize
|
|
42
|
+
"Rapns::Daemon::#{type}::AppRunner".constantize.new(app)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.stop
|
|
46
|
+
runners.values.map(&:stop)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.debug
|
|
50
|
+
runners.values.map(&:debug)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.idle
|
|
54
|
+
runners.values.select { |runner| runner.idle? }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :app
|
|
58
|
+
|
|
59
|
+
def initialize(app)
|
|
60
|
+
@app = app
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def started
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def stopped
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def start
|
|
70
|
+
app.connections.times { handlers << start_handler }
|
|
71
|
+
started
|
|
72
|
+
Rapns::Daemon.logger.info("[#{app.name}] Started, #{handlers_str}.")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stop
|
|
76
|
+
handlers.map(&:stop)
|
|
77
|
+
stopped
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def enqueue(notification)
|
|
81
|
+
queue.push(notification)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sync(app)
|
|
85
|
+
@app = app
|
|
86
|
+
diff = handlers.size - app.connections
|
|
87
|
+
return if diff == 0
|
|
88
|
+
if diff > 0
|
|
89
|
+
diff.times { handlers.pop.stop }
|
|
90
|
+
Rapns::Daemon.logger.info("[#{app.name}] Terminated #{handlers_str(diff)}. #{handlers_str} remaining.")
|
|
91
|
+
else
|
|
92
|
+
diff.abs.times { handlers << start_handler }
|
|
93
|
+
Rapns::Daemon.logger.info("[#{app.name}] Added #{handlers_str(diff)}. #{handlers_str} remaining.")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def debug
|
|
98
|
+
Rapns::Daemon.logger.info <<-EOS
|
|
99
|
+
|
|
100
|
+
#{@app.name}:
|
|
101
|
+
handlers: #{handlers.size}
|
|
102
|
+
queued: #{queue.size}
|
|
103
|
+
idle: #{idle?}
|
|
104
|
+
EOS
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def idle?
|
|
108
|
+
queue.notifications_processed?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
protected
|
|
112
|
+
|
|
113
|
+
def start_handler
|
|
114
|
+
handler = new_delivery_handler
|
|
115
|
+
handler.queue = queue
|
|
116
|
+
handler.start
|
|
117
|
+
handler
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def queue
|
|
121
|
+
@queue ||= Rapns::Daemon::DeliveryQueue.new
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handlers
|
|
125
|
+
@handler ||= []
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def handlers_str(count = app.connections)
|
|
129
|
+
count = count.abs
|
|
130
|
+
str = count == 1 ? 'handler' : 'handlers'
|
|
131
|
+
"#{count} #{str}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class PGError < StandardError; end if !defined?(PGError)
|
|
2
|
+
class Mysql; class Error < StandardError; end; end if !defined?(Mysql)
|
|
3
|
+
module Mysql2; class Error < StandardError; end; end if !defined?(Mysql2)
|
|
4
|
+
module ActiveRecord; end
|
|
5
|
+
class ActiveRecord::JDBCError < StandardError; end if !defined?(ActiveRecord::JDBCError)
|
|
6
|
+
|
|
7
|
+
module Rapns
|
|
8
|
+
module Daemon
|
|
9
|
+
module DatabaseReconnectable
|
|
10
|
+
ADAPTER_ERRORS = [ActiveRecord::StatementInvalid, PGError, Mysql::Error,
|
|
11
|
+
Mysql2::Error, ActiveRecord::JDBCError]
|
|
12
|
+
|
|
13
|
+
def with_database_reconnect_and_retry
|
|
14
|
+
begin
|
|
15
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
|
16
|
+
yield
|
|
17
|
+
end
|
|
18
|
+
rescue *ADAPTER_ERRORS => e
|
|
19
|
+
Rapns::Daemon.logger.error(e)
|
|
20
|
+
database_connection_lost
|
|
21
|
+
retry
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def database_connection_lost
|
|
26
|
+
Rapns::Daemon.logger.warn("Lost connection to database, reconnecting...")
|
|
27
|
+
attempts = 0
|
|
28
|
+
loop do
|
|
29
|
+
begin
|
|
30
|
+
Rapns::Daemon.logger.warn("Attempt #{attempts += 1}")
|
|
31
|
+
reconnect_database
|
|
32
|
+
check_database_is_connected
|
|
33
|
+
break
|
|
34
|
+
rescue *ADAPTER_ERRORS => e
|
|
35
|
+
Rapns::Daemon.logger.error(e, :airbrake_notify => false)
|
|
36
|
+
sleep_to_avoid_thrashing
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
Rapns::Daemon.logger.warn("Database reconnected")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reconnect_database
|
|
43
|
+
ActiveRecord::Base.clear_all_connections!
|
|
44
|
+
ActiveRecord::Base.establish_connection
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_database_is_connected
|
|
48
|
+
# Simply asking the adapter for the connection state is not sufficient.
|
|
49
|
+
Rapns::Notification.count
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sleep_to_avoid_thrashing
|
|
53
|
+
sleep 2
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class Delivery
|
|
4
|
+
include DatabaseReconnectable
|
|
5
|
+
|
|
6
|
+
def self.perform(*args)
|
|
7
|
+
new(*args).perform
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def retry_after(notification, deliver_after)
|
|
11
|
+
with_database_reconnect_and_retry do
|
|
12
|
+
notification.retries += 1
|
|
13
|
+
notification.deliver_after = deliver_after
|
|
14
|
+
notification.save!(:validate => false)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def retry_exponentially(notification)
|
|
19
|
+
retry_after(notification, Time.now + 2 ** (notification.retries + 1))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def mark_delivered
|
|
23
|
+
with_database_reconnect_and_retry do
|
|
24
|
+
@notification.delivered = true
|
|
25
|
+
@notification.delivered_at = Time.now
|
|
26
|
+
@notification.save!(:validate => false)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def mark_failed(code, description)
|
|
31
|
+
with_database_reconnect_and_retry do
|
|
32
|
+
@notification.delivered = false
|
|
33
|
+
@notification.delivered_at = nil
|
|
34
|
+
@notification.failed = true
|
|
35
|
+
@notification.failed_at = Time.now
|
|
36
|
+
@notification.error_code = code
|
|
37
|
+
@notification.error_description = description
|
|
38
|
+
@notification.save!(:validate => false)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
class DeliveryError < StandardError
|
|
3
|
+
attr_reader :code, :description
|
|
4
|
+
|
|
5
|
+
def initialize(code, notification_id, description)
|
|
6
|
+
@code = code
|
|
7
|
+
@notification_id = notification_id
|
|
8
|
+
@description = description
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
message
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def message
|
|
16
|
+
"Unable to deliver notification #{@notification_id}, received error #{@code} (#{@description})"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|