rapns 3.0.0-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|