rapns_rails_2 3.4.3
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.
- checksums.yaml +15 -0
- data/CHANGELOG.md +83 -0
- data/LICENSE +7 -0
- data/README.md +168 -0
- data/bin/rapns +37 -0
- data/config/database.yml +44 -0
- data/lib/generators/rapns_generator.rb +25 -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 +95 -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 +87 -0
- data/lib/rapns/TODO +3 -0
- data/lib/rapns/apns/app.rb +25 -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 +16 -0
- data/lib/rapns/apns/notification.rb +91 -0
- data/lib/rapns/apns_feedback.rb +13 -0
- data/lib/rapns/app.rb +16 -0
- data/lib/rapns/configuration.rb +89 -0
- data/lib/rapns/daemon/apns/app_runner.rb +26 -0
- data/lib/rapns/daemon/apns/certificate_expired_error.rb +20 -0
- data/lib/rapns/daemon/apns/connection.rb +142 -0
- data/lib/rapns/daemon/apns/delivery.rb +64 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +35 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +89 -0
- data/lib/rapns/daemon/app_runner.rb +179 -0
- data/lib/rapns/daemon/batch.rb +112 -0
- data/lib/rapns/daemon/delivery.rb +23 -0
- data/lib/rapns/daemon/delivery_error.rb +19 -0
- data/lib/rapns/daemon/delivery_handler.rb +52 -0
- data/lib/rapns/daemon/delivery_handler_collection.rb +33 -0
- data/lib/rapns/daemon/feeder.rb +65 -0
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +228 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon/interruptible_sleep.rb +65 -0
- data/lib/rapns/daemon/reflectable.rb +13 -0
- data/lib/rapns/daemon/store/active_record/reconnectable.rb +66 -0
- data/lib/rapns/daemon/store/active_record.rb +128 -0
- data/lib/rapns/daemon.rb +129 -0
- data/lib/rapns/deprecatable.rb +23 -0
- data/lib/rapns/deprecation.rb +23 -0
- data/lib/rapns/embed.rb +28 -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 +37 -0
- data/lib/rapns/gcm/payload_data_size_validator.rb +13 -0
- data/lib/rapns/gcm/registration_ids_count_validator.rb +13 -0
- data/lib/rapns/logger.rb +76 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +62 -0
- data/lib/rapns/notifier.rb +35 -0
- data/lib/rapns/push.rb +17 -0
- data/lib/rapns/rails-2-compatibility.rb +34 -0
- data/lib/rapns/reflection.rb +44 -0
- data/lib/rapns/upgraded.rb +31 -0
- data/lib/rapns/version.rb +3 -0
- data/lib/rapns_rails_2.rb +67 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/test.rake +38 -0
- data/spec/support/cert_with_password.pem +90 -0
- data/spec/support/cert_without_password.pem +59 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/tmp/.gitkeep +0 -0
- data/spec/unit/apns/app_spec.rb +29 -0
- data/spec/unit/apns/feedback_spec.rb +9 -0
- data/spec/unit/apns/notification_spec.rb +215 -0
- data/spec/unit/apns_feedback_spec.rb +21 -0
- data/spec/unit/app_spec.rb +16 -0
- data/spec/unit/configuration_spec.rb +55 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +45 -0
- data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
- data/spec/unit/daemon/apns/connection_spec.rb +287 -0
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +59 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +134 -0
- data/spec/unit/daemon/app_runner_shared.rb +83 -0
- data/spec/unit/daemon/app_runner_spec.rb +170 -0
- data/spec/unit/daemon/batch_spec.rb +219 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_handler_collection_spec.rb +37 -0
- data/spec/unit/daemon/delivery_handler_shared.rb +45 -0
- data/spec/unit/daemon/feeder_spec.rb +81 -0
- data/spec/unit/daemon/gcm/app_runner_spec.rb +19 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +44 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +289 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
- data/spec/unit/daemon/reflectable_spec.rb +27 -0
- data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
- data/spec/unit/daemon/store/active_record_spec.rb +281 -0
- data/spec/unit/daemon_spec.rb +157 -0
- data/spec/unit/deprecatable_spec.rb +32 -0
- data/spec/unit/deprecation_spec.rb +15 -0
- data/spec/unit/embed_spec.rb +50 -0
- data/spec/unit/gcm/app_spec.rb +4 -0
- data/spec/unit/gcm/notification_spec.rb +52 -0
- data/spec/unit/logger_spec.rb +180 -0
- data/spec/unit/notification_shared.rb +45 -0
- data/spec/unit/notification_spec.rb +4 -0
- data/spec/unit/notifier_spec.rb +32 -0
- data/spec/unit/push_spec.rb +44 -0
- data/spec/unit/rapns_spec.rb +9 -0
- data/spec/unit/reflection_spec.rb +30 -0
- data/spec/unit/upgraded_spec.rb +40 -0
- data/spec/unit_spec_helper.rb +137 -0
- metadata +232 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Gcm
|
|
4
|
+
# http://developer.android.com/guide/google/gcm/gcm.html#response
|
|
5
|
+
class Delivery < Rapns::Daemon::Delivery
|
|
6
|
+
include Rapns::MultiJsonHelper
|
|
7
|
+
|
|
8
|
+
GCM_URI = URI.parse('https://android.googleapis.com/gcm/send')
|
|
9
|
+
UNAVAILABLE_STATES = ['Unavailable', 'InternalServerError']
|
|
10
|
+
INVALID_REGISTRATION_ID_STATES = ['InvalidRegistration', 'MismatchSenderId', 'NotRegistered', 'InvalidPackageName']
|
|
11
|
+
|
|
12
|
+
def initialize(app, http, notification, batch)
|
|
13
|
+
@app = app
|
|
14
|
+
@http = http
|
|
15
|
+
@notification = notification
|
|
16
|
+
@batch = batch
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def perform
|
|
20
|
+
begin
|
|
21
|
+
handle_response(do_post)
|
|
22
|
+
rescue Rapns::DeliveryError => error
|
|
23
|
+
mark_failed(error.code, error.description)
|
|
24
|
+
raise
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
def handle_response(response)
|
|
31
|
+
case response.code.to_i
|
|
32
|
+
when 200
|
|
33
|
+
ok(response)
|
|
34
|
+
when 400
|
|
35
|
+
bad_request(response)
|
|
36
|
+
when 401
|
|
37
|
+
unauthorized(response)
|
|
38
|
+
when 500
|
|
39
|
+
internal_server_error(response)
|
|
40
|
+
when 503
|
|
41
|
+
service_unavailable(response)
|
|
42
|
+
else
|
|
43
|
+
raise Rapns::DeliveryError.new(response.code, @notification.id, HTTP_STATUS_CODES[response.code.to_i])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ok(response)
|
|
48
|
+
body = multi_json_load(response.body)
|
|
49
|
+
if body['failure'].to_i == 0
|
|
50
|
+
mark_delivered
|
|
51
|
+
Rapns.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.registration_ids.join(', ')}")
|
|
52
|
+
else
|
|
53
|
+
handle_invalid_registration_ids(response, body)
|
|
54
|
+
handle_errors(response, body)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
handle_canonical_ids(response, body)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_errors(response, body)
|
|
61
|
+
errors = {}
|
|
62
|
+
|
|
63
|
+
body['results'].each_with_index do |result, i|
|
|
64
|
+
errors[i] = result['error'] if result['error'] && ! INVALID_REGISTRATION_ID_STATES.include?(result['error'])
|
|
65
|
+
end
|
|
66
|
+
return if errors.empty?
|
|
67
|
+
|
|
68
|
+
if body['success'].to_i == 0 && errors.values.all? { |error| UNAVAILABLE_STATES.include?(error) }
|
|
69
|
+
all_devices_unavailable(response)
|
|
70
|
+
elsif errors.values.any? { |error| UNAVAILABLE_STATES.include?(error) }
|
|
71
|
+
some_devices_unavailable(response, errors)
|
|
72
|
+
else
|
|
73
|
+
raise Rapns::DeliveryError.new(nil, @notification.id, describe_errors(errors))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def handle_invalid_registration_ids(response, body)
|
|
78
|
+
body['results'].each_with_index do |result, i|
|
|
79
|
+
next unless INVALID_REGISTRATION_ID_STATES.include?(result['error'])
|
|
80
|
+
|
|
81
|
+
registration_id = @notification.registration_ids[i]
|
|
82
|
+
reflect(:gcm_invalid_registration_id, @app, result['error'], registration_id)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_canonical_ids(response, body)
|
|
87
|
+
if body['canonical_ids'] && body['canonical_ids'].to_i > 0
|
|
88
|
+
body['results'].each_with_index do |result, i|
|
|
89
|
+
if result['message_id'] && result['registration_id']
|
|
90
|
+
old_id = @notification.registration_ids[i]
|
|
91
|
+
reflect(:gcm_canonical_id, old_id, result['registration_id'])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def bad_request(response)
|
|
98
|
+
raise Rapns::DeliveryError.new(400, @notification.id, 'GCM failed to parse the JSON request. Possibly an rapns bug, please open an issue.')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def unauthorized(response)
|
|
102
|
+
raise Rapns::DeliveryError.new(401, @notification.id, 'Unauthorized, check your App auth_key.')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def internal_server_error(response)
|
|
106
|
+
retry_delivery(@notification, response)
|
|
107
|
+
Rapns.logger.warn("GCM responded with an Internal Error. " + retry_message)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def service_unavailable(response)
|
|
111
|
+
retry_delivery(@notification, response)
|
|
112
|
+
Rapns.logger.warn("GCM responded with an Service Unavailable Error. " + retry_message)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def all_devices_unavailable(response)
|
|
116
|
+
retry_delivery(@notification, response)
|
|
117
|
+
Rapns.logger.warn("All recipients unavailable. " + retry_message)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def some_devices_unavailable(response, errors)
|
|
121
|
+
unavailable_idxs = errors.find_all { |i, error| UNAVAILABLE_STATES.include?(error) }.map(&:first)
|
|
122
|
+
new_notification = create_new_notification(response, unavailable_idxs)
|
|
123
|
+
raise Rapns::DeliveryError.new(nil, @notification.id,
|
|
124
|
+
describe_errors(errors) + " #{unavailable_idxs.join(', ')} will be retried as notification #{new_notification.id}.")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def create_new_notification(response, unavailable_idxs)
|
|
128
|
+
attrs = @notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle')
|
|
129
|
+
registration_ids = unavailable_idxs.map { |i| @notification.registration_ids[i] }
|
|
130
|
+
Rapns::Daemon.store.create_gcm_notification(attrs, @notification.data,
|
|
131
|
+
registration_ids, deliver_after_header(response), @notification.app)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def deliver_after_header(response)
|
|
135
|
+
if response.header['retry-after']
|
|
136
|
+
if response.header['retry-after'].to_s =~ /^[0-9]+$/
|
|
137
|
+
Time.now + response.header['retry-after'].to_i
|
|
138
|
+
else
|
|
139
|
+
Time.httpdate(response.header['retry-after'])
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def retry_delivery(notification, response)
|
|
145
|
+
if time = deliver_after_header(response)
|
|
146
|
+
mark_retryable(notification, time)
|
|
147
|
+
else
|
|
148
|
+
mark_retryable_exponential(notification)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def describe_errors(errors)
|
|
153
|
+
description = if errors.size == @notification.registration_ids.size
|
|
154
|
+
"Failed to deliver to all recipients. Errors: #{errors.values.join(', ')}."
|
|
155
|
+
else
|
|
156
|
+
"Failed to deliver to recipients #{errors.keys.join(', ')}. Errors: #{errors.values.join(', ')}."
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def retry_message
|
|
161
|
+
"Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def do_post
|
|
165
|
+
post = Net::HTTP::Post.new(GCM_URI.path, initheader = {'Content-Type' => 'application/json',
|
|
166
|
+
'Authorization' => "key=#{@notification.app.auth_key}"})
|
|
167
|
+
post.body = @notification.as_json.to_json
|
|
168
|
+
@http.request(GCM_URI, post)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
HTTP_STATUS_CODES = {
|
|
172
|
+
100 => 'Continue',
|
|
173
|
+
101 => 'Switching Protocols',
|
|
174
|
+
102 => 'Processing',
|
|
175
|
+
200 => 'OK',
|
|
176
|
+
201 => 'Created',
|
|
177
|
+
202 => 'Accepted',
|
|
178
|
+
203 => 'Non-Authoritative Information',
|
|
179
|
+
204 => 'No Content',
|
|
180
|
+
205 => 'Reset Content',
|
|
181
|
+
206 => 'Partial Content',
|
|
182
|
+
207 => 'Multi-Status',
|
|
183
|
+
226 => 'IM Used',
|
|
184
|
+
300 => 'Multiple Choices',
|
|
185
|
+
301 => 'Moved Permanently',
|
|
186
|
+
302 => 'Found',
|
|
187
|
+
303 => 'See Other',
|
|
188
|
+
304 => 'Not Modified',
|
|
189
|
+
305 => 'Use Proxy',
|
|
190
|
+
306 => 'Reserved',
|
|
191
|
+
307 => 'Temporary Redirect',
|
|
192
|
+
400 => 'Bad Request',
|
|
193
|
+
401 => 'Unauthorized',
|
|
194
|
+
402 => 'Payment Required',
|
|
195
|
+
403 => 'Forbidden',
|
|
196
|
+
404 => 'Not Found',
|
|
197
|
+
405 => 'Method Not Allowed',
|
|
198
|
+
406 => 'Not Acceptable',
|
|
199
|
+
407 => 'Proxy Authentication Required',
|
|
200
|
+
408 => 'Request Timeout',
|
|
201
|
+
409 => 'Conflict',
|
|
202
|
+
410 => 'Gone',
|
|
203
|
+
411 => 'Length Required',
|
|
204
|
+
412 => 'Precondition Failed',
|
|
205
|
+
413 => 'Request Entity Too Large',
|
|
206
|
+
414 => 'Request-URI Too Long',
|
|
207
|
+
415 => 'Unsupported Media Type',
|
|
208
|
+
416 => 'Requested Range Not Satisfiable',
|
|
209
|
+
417 => 'Expectation Failed',
|
|
210
|
+
418 => "I'm a Teapot",
|
|
211
|
+
422 => 'Unprocessable Entity',
|
|
212
|
+
423 => 'Locked',
|
|
213
|
+
424 => 'Failed Dependency',
|
|
214
|
+
426 => 'Upgrade Required',
|
|
215
|
+
500 => 'Internal Server Error',
|
|
216
|
+
501 => 'Not Implemented',
|
|
217
|
+
502 => 'Bad Gateway',
|
|
218
|
+
503 => 'Service Unavailable',
|
|
219
|
+
504 => 'Gateway Timeout',
|
|
220
|
+
505 => 'HTTP Version Not Supported',
|
|
221
|
+
506 => 'Variant Also Negotiates',
|
|
222
|
+
507 => 'Insufficient Storage',
|
|
223
|
+
510 => 'Not Extended',
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Gcm
|
|
4
|
+
class DeliveryHandler < Rapns::Daemon::DeliveryHandler
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
@http = Net::HTTP::Persistent.new('rapns')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def deliver(notification, batch)
|
|
11
|
+
Rapns::Daemon::Gcm::Delivery.new(@app, @http, notification, batch).perform
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def stopped
|
|
15
|
+
@http.shutdown
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class InterruptibleSleep
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@sleep_reader, @wake_writer = IO.pipe
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# enable wake on receiving udp packets at the given address and port
|
|
10
|
+
# this returns the host,port used by bind in case an ephemeral port
|
|
11
|
+
# was indicated by specifying 0 as the port number.
|
|
12
|
+
# @return [String,Integer] host,port of bound UDP socket.
|
|
13
|
+
def enable_wake_on_udp(host, port)
|
|
14
|
+
@udp_wakeup = UDPSocket.new
|
|
15
|
+
@udp_wakeup.bind(host, port)
|
|
16
|
+
@udp_wakeup.addr.values_at(3,1)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# wait for the given timeout in seconds, or data was written to the pipe
|
|
20
|
+
# or the udp wakeup port if enabled.
|
|
21
|
+
# @return [boolean] true if the sleep was interrupted, or false
|
|
22
|
+
def sleep(timeout)
|
|
23
|
+
read_ports = [@sleep_reader]
|
|
24
|
+
read_ports << @udp_wakeup if @udp_wakeup
|
|
25
|
+
rs, = IO.select(read_ports, nil, nil, timeout) rescue nil
|
|
26
|
+
|
|
27
|
+
# consume all data on the readable io's so that our next call will wait for more data
|
|
28
|
+
if rs && rs.include?(@sleep_reader)
|
|
29
|
+
while true
|
|
30
|
+
begin
|
|
31
|
+
@sleep_reader.read_nonblock(1)
|
|
32
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
|
33
|
+
# rescue IO::WaitReadable
|
|
34
|
+
break
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if rs && rs.include?(@udp_wakeup)
|
|
40
|
+
while true
|
|
41
|
+
begin
|
|
42
|
+
@udp_wakeup.recv_nonblock(1)
|
|
43
|
+
rescue Errno::EWOULDBLOCK, Errno::EAGAIN
|
|
44
|
+
# rescue IO::WaitReadable
|
|
45
|
+
break
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
!rs.nil? && rs.any?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# writing to the pipe will wake the sleeping thread
|
|
54
|
+
def interrupt_sleep
|
|
55
|
+
@wake_writer.write('.')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def close
|
|
59
|
+
@sleep_reader.close rescue nil
|
|
60
|
+
@wake_writer.close rescue nil
|
|
61
|
+
@udp_wakeup.close if @udp_wakeup rescue nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
if !defined?(::SQLite3::Exception)
|
|
7
|
+
module SQLite3
|
|
8
|
+
class Exception < StandardError; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Rapns
|
|
13
|
+
module Daemon
|
|
14
|
+
module Store
|
|
15
|
+
class ActiveRecord
|
|
16
|
+
module Reconnectable
|
|
17
|
+
ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error,
|
|
18
|
+
Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]
|
|
19
|
+
|
|
20
|
+
def with_database_reconnect_and_retry
|
|
21
|
+
begin
|
|
22
|
+
::ActiveRecord::Base.connection_pool.with_connection do
|
|
23
|
+
yield
|
|
24
|
+
end
|
|
25
|
+
rescue *ADAPTER_ERRORS => e
|
|
26
|
+
Rapns.logger.error(e)
|
|
27
|
+
database_connection_lost
|
|
28
|
+
retry
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def database_connection_lost
|
|
33
|
+
Rapns.logger.warn("Lost connection to database, reconnecting...")
|
|
34
|
+
attempts = 0
|
|
35
|
+
loop do
|
|
36
|
+
begin
|
|
37
|
+
Rapns.logger.warn("Attempt #{attempts += 1}")
|
|
38
|
+
reconnect_database
|
|
39
|
+
check_database_is_connected
|
|
40
|
+
break
|
|
41
|
+
rescue *ADAPTER_ERRORS => e
|
|
42
|
+
Rapns.logger.error(e, :airbrake_notify => false)
|
|
43
|
+
sleep_to_avoid_thrashing
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
Rapns.logger.warn("Database reconnected")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reconnect_database
|
|
50
|
+
::ActiveRecord::Base.clear_all_connections!
|
|
51
|
+
::ActiveRecord::Base.establish_connection
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def check_database_is_connected
|
|
55
|
+
# Simply asking the adapter for the connection state is not sufficient.
|
|
56
|
+
Rapns::Notification.count
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sleep_to_avoid_thrashing
|
|
60
|
+
sleep 2
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require 'active_record'
|
|
2
|
+
|
|
3
|
+
require 'rapns/daemon/store/active_record/reconnectable'
|
|
4
|
+
|
|
5
|
+
module Rapns
|
|
6
|
+
module Daemon
|
|
7
|
+
module Store
|
|
8
|
+
class ActiveRecord
|
|
9
|
+
include Reconnectable
|
|
10
|
+
|
|
11
|
+
def deliverable_notifications(apps)
|
|
12
|
+
with_database_reconnect_and_retry do
|
|
13
|
+
batch_size = Rapns.config.batch_size
|
|
14
|
+
relation = Rapns::Notification.ready_for_delivery.for_apps(apps)
|
|
15
|
+
if Rapns.config.push
|
|
16
|
+
relation.all
|
|
17
|
+
else
|
|
18
|
+
relation.find(:all, :limit => batch_size)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def mark_retryable(notification, deliver_after)
|
|
24
|
+
with_database_reconnect_and_retry do
|
|
25
|
+
notification.retries += 1
|
|
26
|
+
notification.deliver_after = deliver_after
|
|
27
|
+
notification.save(false)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mark_batch_retryable(notifications, deliver_after)
|
|
32
|
+
ids = []
|
|
33
|
+
notifications.each do |n|
|
|
34
|
+
# Update attrs for reflections, but don't save.
|
|
35
|
+
n.retries += 1
|
|
36
|
+
n.deliver_after = deliver_after
|
|
37
|
+
ids << n.id
|
|
38
|
+
end
|
|
39
|
+
with_database_reconnect_and_retry do
|
|
40
|
+
Rapns::Notification.find(ids).each { |n|
|
|
41
|
+
n.update_attributes(:retries => n.retries + 1, :deliver_after => deliver_after)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def mark_delivered(notification)
|
|
47
|
+
with_database_reconnect_and_retry do
|
|
48
|
+
notification.delivered = true
|
|
49
|
+
notification.delivered_at = Time.now
|
|
50
|
+
notification.save(false)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def mark_batch_delivered(notifications)
|
|
55
|
+
now = Time.now
|
|
56
|
+
ids = []
|
|
57
|
+
notifications.each do |n|
|
|
58
|
+
# Update attrs for reflections, but don't save.
|
|
59
|
+
n.delivered = true
|
|
60
|
+
n.delivered_at = now
|
|
61
|
+
ids << n.id
|
|
62
|
+
end
|
|
63
|
+
with_database_reconnect_and_retry do
|
|
64
|
+
Rapns::Notification.find(ids).each { |n|
|
|
65
|
+
n.update_attributes(:delivered => true, :delivered_at => now)
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def mark_failed(notification, code, description)
|
|
71
|
+
with_database_reconnect_and_retry do
|
|
72
|
+
notification.delivered = false
|
|
73
|
+
notification.delivered_at = nil
|
|
74
|
+
notification.failed = true
|
|
75
|
+
notification.failed_at = Time.now
|
|
76
|
+
notification.error_code = code
|
|
77
|
+
notification.error_description = description
|
|
78
|
+
notification.save(false)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def mark_batch_failed(notifications, code, description)
|
|
83
|
+
now = Time.now
|
|
84
|
+
ids = []
|
|
85
|
+
notifications.each do |n|
|
|
86
|
+
# Update attrs for reflections, but don't save.
|
|
87
|
+
n.delivered = false
|
|
88
|
+
n.delivered_at = nil
|
|
89
|
+
n.failed = true
|
|
90
|
+
n.failed_at = now
|
|
91
|
+
n.error_code = code
|
|
92
|
+
n.error_description = description
|
|
93
|
+
ids << n.id
|
|
94
|
+
end
|
|
95
|
+
with_database_reconnect_and_retry do
|
|
96
|
+
Rapns::Notification.find(ids).each { |n|
|
|
97
|
+
n.update_attributes(:delivered => false, :delivered_at => nil, :failed => true, :failed_at => now, :error_code => code, :error_description => description)
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def create_apns_feedback(failed_at, device_token, app)
|
|
103
|
+
with_database_reconnect_and_retry do
|
|
104
|
+
Rapns::Apns::Feedback.create!(:failed_at => failed_at,
|
|
105
|
+
:device_token => device_token, :app => app)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def create_gcm_notification(attrs, data, registration_ids, deliver_after, app)
|
|
110
|
+
with_database_reconnect_and_retry do
|
|
111
|
+
notification = Rapns::Gcm::Notification.new
|
|
112
|
+
notification.attributes = attrs
|
|
113
|
+
notification.data = data
|
|
114
|
+
notification.registration_ids = registration_ids
|
|
115
|
+
notification.deliver_after = deliver_after
|
|
116
|
+
notification.app = app
|
|
117
|
+
notification.save!
|
|
118
|
+
notification
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def after_daemonize
|
|
123
|
+
reconnect_database
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/rapns/daemon.rb
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
require 'net/http/persistent'
|
|
7
|
+
|
|
8
|
+
require 'rapns/daemon/reflectable'
|
|
9
|
+
require 'rapns/daemon/interruptible_sleep'
|
|
10
|
+
require 'rapns/daemon/delivery_error'
|
|
11
|
+
require 'rapns/daemon/delivery'
|
|
12
|
+
require 'rapns/daemon/feeder'
|
|
13
|
+
require 'rapns/daemon/batch'
|
|
14
|
+
require 'rapns/daemon/app_runner'
|
|
15
|
+
require 'rapns/daemon/delivery_handler'
|
|
16
|
+
require 'rapns/daemon/delivery_handler_collection'
|
|
17
|
+
|
|
18
|
+
require 'rapns/daemon/apns/delivery'
|
|
19
|
+
require 'rapns/daemon/apns/disconnection_error'
|
|
20
|
+
require 'rapns/daemon/apns/certificate_expired_error'
|
|
21
|
+
require 'rapns/daemon/apns/connection'
|
|
22
|
+
require 'rapns/daemon/apns/app_runner'
|
|
23
|
+
require 'rapns/daemon/apns/delivery_handler'
|
|
24
|
+
require 'rapns/daemon/apns/feedback_receiver'
|
|
25
|
+
|
|
26
|
+
require 'rapns/daemon/gcm/delivery'
|
|
27
|
+
require 'rapns/daemon/gcm/app_runner'
|
|
28
|
+
require 'rapns/daemon/gcm/delivery_handler'
|
|
29
|
+
|
|
30
|
+
module Rapns
|
|
31
|
+
module Daemon
|
|
32
|
+
class << self
|
|
33
|
+
attr_accessor :store
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.start
|
|
37
|
+
setup_signal_traps if trap_signals?
|
|
38
|
+
|
|
39
|
+
initialize_store
|
|
40
|
+
return unless store
|
|
41
|
+
|
|
42
|
+
if daemonize?
|
|
43
|
+
daemonize
|
|
44
|
+
store.after_daemonize
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
write_pid_file
|
|
48
|
+
Upgraded.check(:exit => true)
|
|
49
|
+
AppRunner.sync
|
|
50
|
+
Feeder.start
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.shutdown(quiet = false)
|
|
54
|
+
puts "\nShutting down..." unless quiet
|
|
55
|
+
Feeder.stop
|
|
56
|
+
AppRunner.stop
|
|
57
|
+
delete_pid_file
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.initialize_store
|
|
61
|
+
return if store
|
|
62
|
+
begin
|
|
63
|
+
require "rapns/daemon/store/#{Rapns.config.store}"
|
|
64
|
+
klass = "Rapns::Daemon::Store::#{Rapns.config.store.to_s.camelcase}".constantize
|
|
65
|
+
self.store = klass.new
|
|
66
|
+
rescue StandardError, LoadError => e
|
|
67
|
+
Rapns.logger.error("Failed to load '#{Rapns.config.store}' storage backend.")
|
|
68
|
+
Rapns.logger.error(e)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def self.daemonize?
|
|
75
|
+
!(Rapns.config.foreground || Rapns.config.embedded || Rapns.jruby?)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.trap_signals?
|
|
79
|
+
!Rapns.config.embedded
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.setup_signal_traps
|
|
83
|
+
@shutting_down = false
|
|
84
|
+
|
|
85
|
+
Signal.trap('SIGHUP') { AppRunner.sync }
|
|
86
|
+
Signal.trap('SIGUSR2') { AppRunner.debug }
|
|
87
|
+
|
|
88
|
+
['SIGINT', 'SIGTERM'].each do |signal|
|
|
89
|
+
Signal.trap(signal) { handle_shutdown_signal }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.handle_shutdown_signal
|
|
94
|
+
exit 1 if @shutting_down
|
|
95
|
+
@shutting_down = true
|
|
96
|
+
shutdown
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.write_pid_file
|
|
100
|
+
if !Rapns.config.pid_file.blank?
|
|
101
|
+
begin
|
|
102
|
+
File.open(Rapns.config.pid_file, 'w') { |f| f.puts Process.pid }
|
|
103
|
+
rescue SystemCallError => e
|
|
104
|
+
Rapns.logger.error("Failed to write PID to '#{Rapns.config.pid_file}': #{e.inspect}")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.delete_pid_file
|
|
110
|
+
pid_file = Rapns.config.pid_file
|
|
111
|
+
File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# :nocov:
|
|
115
|
+
def self.daemonize
|
|
116
|
+
if RUBY_VERSION < "1.9"
|
|
117
|
+
exit if fork
|
|
118
|
+
Process.setsid
|
|
119
|
+
exit if fork
|
|
120
|
+
Dir.chdir "/"
|
|
121
|
+
STDIN.reopen "/dev/null"
|
|
122
|
+
STDOUT.reopen "/dev/null", "a"
|
|
123
|
+
STDERR.reopen "/dev/null", "a"
|
|
124
|
+
else
|
|
125
|
+
Process.daemon
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Deprecatable
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend ClassMethods
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
def deprecated(method_name, version, msg=nil)
|
|
9
|
+
instance_eval do
|
|
10
|
+
alias_method "#{method_name}_without_warning", method_name
|
|
11
|
+
end
|
|
12
|
+
warning = "#{method_name} is deprecated and will be removed from Rapns #{version}."
|
|
13
|
+
warning << " #{msg}" if msg
|
|
14
|
+
class_eval(<<-RUBY, __FILE__, __LINE__)
|
|
15
|
+
def #{method_name}(*args, &blk)
|
|
16
|
+
Rapns::Deprecation.warn(#{warning.inspect})
|
|
17
|
+
#{method_name}_without_warning(*args, &blk)
|
|
18
|
+
end
|
|
19
|
+
RUBY
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
class Deprecation
|
|
3
|
+
def self.muted
|
|
4
|
+
begin
|
|
5
|
+
orig_val = Thread.current[:rapns_mute_deprecations]
|
|
6
|
+
Thread.current[:rapns_mute_deprecations] = true
|
|
7
|
+
yield
|
|
8
|
+
ensure
|
|
9
|
+
Thread.current[:rapns_mute_deprecations] = orig_val
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.muted?
|
|
14
|
+
Thread.current[:rapns_mute_deprecations] == true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.warn(msg)
|
|
18
|
+
unless Rapns::Deprecation.muted?
|
|
19
|
+
STDERR.puts "DEPRECATION WARNING: #{msg}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|