rapns 3.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/CHANGELOG.md +31 -0
  2. data/LICENSE +7 -0
  3. data/README.md +138 -0
  4. data/bin/rapns +41 -0
  5. data/config/database.yml +39 -0
  6. data/lib/generators/rapns_generator.rb +34 -0
  7. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  8. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  9. data/lib/generators/templates/add_gcm.rb +94 -0
  10. data/lib/generators/templates/create_rapns_apps.rb +16 -0
  11. data/lib/generators/templates/create_rapns_feedback.rb +15 -0
  12. data/lib/generators/templates/create_rapns_notifications.rb +26 -0
  13. data/lib/generators/templates/rapns.rb +39 -0
  14. data/lib/rapns/apns/app.rb +8 -0
  15. data/lib/rapns/apns/binary_notification_validator.rb +12 -0
  16. data/lib/rapns/apns/device_token_format_validator.rb +12 -0
  17. data/lib/rapns/apns/feedback.rb +14 -0
  18. data/lib/rapns/apns/notification.rb +86 -0
  19. data/lib/rapns/apns/required_fields_validator.rb +14 -0
  20. data/lib/rapns/app.rb +29 -0
  21. data/lib/rapns/configuration.rb +46 -0
  22. data/lib/rapns/daemon/apns/app_runner.rb +36 -0
  23. data/lib/rapns/daemon/apns/connection.rb +113 -0
  24. data/lib/rapns/daemon/apns/delivery.rb +63 -0
  25. data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
  26. data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
  27. data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
  28. data/lib/rapns/daemon/app_runner.rb +135 -0
  29. data/lib/rapns/daemon/database_reconnectable.rb +57 -0
  30. data/lib/rapns/daemon/delivery.rb +43 -0
  31. data/lib/rapns/daemon/delivery_error.rb +19 -0
  32. data/lib/rapns/daemon/delivery_handler.rb +46 -0
  33. data/lib/rapns/daemon/delivery_queue.rb +42 -0
  34. data/lib/rapns/daemon/delivery_queue_18.rb +44 -0
  35. data/lib/rapns/daemon/delivery_queue_19.rb +42 -0
  36. data/lib/rapns/daemon/feeder.rb +37 -0
  37. data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
  38. data/lib/rapns/daemon/gcm/delivery.rb +206 -0
  39. data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
  40. data/lib/rapns/daemon/interruptible_sleep.rb +18 -0
  41. data/lib/rapns/daemon/logger.rb +68 -0
  42. data/lib/rapns/daemon.rb +136 -0
  43. data/lib/rapns/gcm/app.rb +7 -0
  44. data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  45. data/lib/rapns/gcm/notification.rb +31 -0
  46. data/lib/rapns/gcm/payload_size_validator.rb +13 -0
  47. data/lib/rapns/multi_json_helper.rb +16 -0
  48. data/lib/rapns/notification.rb +54 -0
  49. data/lib/rapns/patches/rails/3.1.0/postgresql_adapter.rb +12 -0
  50. data/lib/rapns/patches/rails/3.1.1/postgresql_adapter.rb +17 -0
  51. data/lib/rapns/patches.rb +6 -0
  52. data/lib/rapns/version.rb +3 -0
  53. data/lib/rapns.rb +21 -0
  54. data/lib/tasks/cane.rake +18 -0
  55. data/lib/tasks/test.rake +33 -0
  56. data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
  57. data/spec/acceptance_spec_helper.rb +85 -0
  58. data/spec/support/simplecov_helper.rb +13 -0
  59. data/spec/support/simplecov_quality_formatter.rb +8 -0
  60. data/spec/unit/apns/app_spec.rb +15 -0
  61. data/spec/unit/apns/feedback_spec.rb +12 -0
  62. data/spec/unit/apns/notification_spec.rb +198 -0
  63. data/spec/unit/app_spec.rb +18 -0
  64. data/spec/unit/configuration_spec.rb +38 -0
  65. data/spec/unit/daemon/apns/app_runner_spec.rb +39 -0
  66. data/spec/unit/daemon/apns/connection_spec.rb +234 -0
  67. data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
  68. data/spec/unit/daemon/apns/delivery_spec.rb +160 -0
  69. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  70. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +118 -0
  71. data/spec/unit/daemon/app_runner_shared.rb +66 -0
  72. data/spec/unit/daemon/app_runner_spec.rb +129 -0
  73. data/spec/unit/daemon/database_reconnectable_spec.rb +109 -0
  74. data/spec/unit/daemon/delivery_error_spec.rb +13 -0
  75. data/spec/unit/daemon/delivery_handler_shared.rb +28 -0
  76. data/spec/unit/daemon/delivery_queue_spec.rb +29 -0
  77. data/spec/unit/daemon/feeder_spec.rb +95 -0
  78. data/spec/unit/daemon/gcm/app_runner_spec.rb +17 -0
  79. data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
  80. data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
  81. data/spec/unit/daemon/interruptible_sleep_spec.rb +40 -0
  82. data/spec/unit/daemon/logger_spec.rb +156 -0
  83. data/spec/unit/daemon_spec.rb +139 -0
  84. data/spec/unit/gcm/app_spec.rb +5 -0
  85. data/spec/unit/gcm/notification_spec.rb +55 -0
  86. data/spec/unit/notification_shared.rb +38 -0
  87. data/spec/unit/notification_spec.rb +6 -0
  88. data/spec/unit_spec_helper.rb +145 -0
  89. 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