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