rpush 1.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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +99 -0
  3. data/LICENSE +7 -0
  4. data/README.md +189 -0
  5. data/bin/rpush +36 -0
  6. data/config/database.yml +44 -0
  7. data/lib/generators/rpush_generator.rb +44 -0
  8. data/lib/generators/templates/add_adm.rb +23 -0
  9. data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
  10. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  11. data/lib/generators/templates/add_fail_after_to_rpush_notifications.rb +9 -0
  12. data/lib/generators/templates/add_gcm.rb +102 -0
  13. data/lib/generators/templates/add_rpush.rb +349 -0
  14. data/lib/generators/templates/add_wpns.rb +16 -0
  15. data/lib/generators/templates/create_rapns_apps.rb +16 -0
  16. data/lib/generators/templates/create_rapns_feedback.rb +18 -0
  17. data/lib/generators/templates/create_rapns_notifications.rb +29 -0
  18. data/lib/generators/templates/rename_rapns_to_rpush.rb +63 -0
  19. data/lib/generators/templates/rpush.rb +104 -0
  20. data/lib/rpush/TODO +3 -0
  21. data/lib/rpush/adm/app.rb +15 -0
  22. data/lib/rpush/adm/data_validator.rb +11 -0
  23. data/lib/rpush/adm/notification.rb +29 -0
  24. data/lib/rpush/apns/app.rb +29 -0
  25. data/lib/rpush/apns/binary_notification_validator.rb +12 -0
  26. data/lib/rpush/apns/device_token_format_validator.rb +12 -0
  27. data/lib/rpush/apns/feedback.rb +16 -0
  28. data/lib/rpush/apns/notification.rb +84 -0
  29. data/lib/rpush/apns_feedback.rb +13 -0
  30. data/lib/rpush/app.rb +18 -0
  31. data/lib/rpush/configuration.rb +75 -0
  32. data/lib/rpush/daemon/adm/delivery.rb +222 -0
  33. data/lib/rpush/daemon/adm.rb +9 -0
  34. data/lib/rpush/daemon/apns/certificate_expired_error.rb +20 -0
  35. data/lib/rpush/daemon/apns/delivery.rb +64 -0
  36. data/lib/rpush/daemon/apns/disconnection_error.rb +20 -0
  37. data/lib/rpush/daemon/apns/feedback_receiver.rb +79 -0
  38. data/lib/rpush/daemon/apns.rb +16 -0
  39. data/lib/rpush/daemon/app_runner.rb +187 -0
  40. data/lib/rpush/daemon/batch.rb +115 -0
  41. data/lib/rpush/daemon/constants.rb +59 -0
  42. data/lib/rpush/daemon/delivery.rb +28 -0
  43. data/lib/rpush/daemon/delivery_error.rb +19 -0
  44. data/lib/rpush/daemon/dispatcher/http.rb +21 -0
  45. data/lib/rpush/daemon/dispatcher/tcp.rb +30 -0
  46. data/lib/rpush/daemon/dispatcher_loop.rb +54 -0
  47. data/lib/rpush/daemon/dispatcher_loop_collection.rb +33 -0
  48. data/lib/rpush/daemon/feeder.rb +68 -0
  49. data/lib/rpush/daemon/gcm/delivery.rb +222 -0
  50. data/lib/rpush/daemon/gcm.rb +9 -0
  51. data/lib/rpush/daemon/interruptible_sleep.rb +61 -0
  52. data/lib/rpush/daemon/loggable.rb +31 -0
  53. data/lib/rpush/daemon/reflectable.rb +13 -0
  54. data/lib/rpush/daemon/retry_header_parser.rb +23 -0
  55. data/lib/rpush/daemon/retryable_error.rb +20 -0
  56. data/lib/rpush/daemon/service_config_methods.rb +33 -0
  57. data/lib/rpush/daemon/store/active_record/reconnectable.rb +68 -0
  58. data/lib/rpush/daemon/store/active_record.rb +154 -0
  59. data/lib/rpush/daemon/tcp_connection.rb +143 -0
  60. data/lib/rpush/daemon/too_many_requests_error.rb +20 -0
  61. data/lib/rpush/daemon/wpns/delivery.rb +132 -0
  62. data/lib/rpush/daemon/wpns.rb +9 -0
  63. data/lib/rpush/daemon.rb +140 -0
  64. data/lib/rpush/deprecatable.rb +23 -0
  65. data/lib/rpush/deprecation.rb +23 -0
  66. data/lib/rpush/embed.rb +28 -0
  67. data/lib/rpush/gcm/app.rb +11 -0
  68. data/lib/rpush/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  69. data/lib/rpush/gcm/notification.rb +30 -0
  70. data/lib/rpush/logger.rb +63 -0
  71. data/lib/rpush/multi_json_helper.rb +16 -0
  72. data/lib/rpush/notification.rb +69 -0
  73. data/lib/rpush/notifier.rb +52 -0
  74. data/lib/rpush/payload_data_size_validator.rb +10 -0
  75. data/lib/rpush/push.rb +16 -0
  76. data/lib/rpush/railtie.rb +11 -0
  77. data/lib/rpush/reflection.rb +58 -0
  78. data/lib/rpush/registration_ids_count_validator.rb +10 -0
  79. data/lib/rpush/version.rb +3 -0
  80. data/lib/rpush/wpns/app.rb +9 -0
  81. data/lib/rpush/wpns/notification.rb +26 -0
  82. data/lib/rpush.rb +62 -0
  83. data/lib/tasks/cane.rake +18 -0
  84. data/lib/tasks/rpush.rake +16 -0
  85. data/lib/tasks/test.rake +38 -0
  86. data/spec/functional/adm_spec.rb +43 -0
  87. data/spec/functional/apns_spec.rb +58 -0
  88. data/spec/functional/embed_spec.rb +49 -0
  89. data/spec/functional/gcm_spec.rb +42 -0
  90. data/spec/functional/wpns_spec.rb +41 -0
  91. data/spec/support/cert_with_password.pem +90 -0
  92. data/spec/support/cert_without_password.pem +59 -0
  93. data/spec/support/install.sh +32 -0
  94. data/spec/support/simplecov_helper.rb +20 -0
  95. data/spec/support/simplecov_quality_formatter.rb +8 -0
  96. data/spec/tmp/.gitkeep +0 -0
  97. data/spec/unit/adm/app_spec.rb +58 -0
  98. data/spec/unit/adm/notification_spec.rb +45 -0
  99. data/spec/unit/apns/app_spec.rb +29 -0
  100. data/spec/unit/apns/feedback_spec.rb +9 -0
  101. data/spec/unit/apns/notification_spec.rb +208 -0
  102. data/spec/unit/apns_feedback_spec.rb +21 -0
  103. data/spec/unit/app_spec.rb +30 -0
  104. data/spec/unit/configuration_spec.rb +45 -0
  105. data/spec/unit/daemon/adm/delivery_spec.rb +243 -0
  106. data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
  107. data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
  108. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  109. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +117 -0
  110. data/spec/unit/daemon/app_runner_spec.rb +292 -0
  111. data/spec/unit/daemon/batch_spec.rb +232 -0
  112. data/spec/unit/daemon/delivery_error_spec.rb +13 -0
  113. data/spec/unit/daemon/delivery_spec.rb +38 -0
  114. data/spec/unit/daemon/dispatcher/http_spec.rb +33 -0
  115. data/spec/unit/daemon/dispatcher/tcp_spec.rb +38 -0
  116. data/spec/unit/daemon/dispatcher_loop_collection_spec.rb +37 -0
  117. data/spec/unit/daemon/dispatcher_loop_spec.rb +71 -0
  118. data/spec/unit/daemon/feeder_spec.rb +98 -0
  119. data/spec/unit/daemon/gcm/delivery_spec.rb +310 -0
  120. data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
  121. data/spec/unit/daemon/reflectable_spec.rb +27 -0
  122. data/spec/unit/daemon/retryable_error_spec.rb +14 -0
  123. data/spec/unit/daemon/service_config_methods_spec.rb +33 -0
  124. data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
  125. data/spec/unit/daemon/store/active_record_spec.rb +357 -0
  126. data/spec/unit/daemon/tcp_connection_spec.rb +287 -0
  127. data/spec/unit/daemon/too_many_requests_error_spec.rb +14 -0
  128. data/spec/unit/daemon/wpns/delivery_spec.rb +159 -0
  129. data/spec/unit/daemon_spec.rb +159 -0
  130. data/spec/unit/deprecatable_spec.rb +32 -0
  131. data/spec/unit/deprecation_spec.rb +15 -0
  132. data/spec/unit/embed_spec.rb +50 -0
  133. data/spec/unit/gcm/app_spec.rb +4 -0
  134. data/spec/unit/gcm/notification_spec.rb +36 -0
  135. data/spec/unit/logger_spec.rb +127 -0
  136. data/spec/unit/notification_shared.rb +105 -0
  137. data/spec/unit/notification_spec.rb +15 -0
  138. data/spec/unit/notifier_spec.rb +49 -0
  139. data/spec/unit/push_spec.rb +43 -0
  140. data/spec/unit/reflection_spec.rb +30 -0
  141. data/spec/unit/rpush_spec.rb +9 -0
  142. data/spec/unit/wpns/app_spec.rb +4 -0
  143. data/spec/unit/wpns/notification_spec.rb +30 -0
  144. data/spec/unit_spec_helper.rb +101 -0
  145. metadata +304 -0
@@ -0,0 +1,222 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Gcm
4
+
5
+ # http://developer.android.com/guide/google/gcm/gcm.html#response
6
+ class Delivery < Rpush::Daemon::Delivery
7
+ include MultiJsonHelper
8
+
9
+ host = ENV["RPUSH_GCM_HOST"] || "https://android.googleapis.com"
10
+ GCM_URI = URI.parse("#{host}/gcm/send")
11
+ UNAVAILABLE_STATES = ['Unavailable', 'InternalServerError']
12
+ INVALID_REGISTRATION_ID_STATES = ['InvalidRegistration', 'MismatchSenderId', 'NotRegistered', 'InvalidPackageName']
13
+
14
+ def initialize(app, http, notification, batch)
15
+ @app = app
16
+ @http = http
17
+ @notification = notification
18
+ @batch = batch
19
+ end
20
+
21
+ def perform
22
+ begin
23
+ handle_response(do_post)
24
+ rescue Rpush::DeliveryError => error
25
+ mark_failed(error.code, error.description)
26
+ raise
27
+ end
28
+ end
29
+
30
+ protected
31
+
32
+ def handle_response(response)
33
+ case response.code.to_i
34
+ when 200
35
+ ok(response)
36
+ when 400
37
+ bad_request
38
+ when 401
39
+ unauthorized
40
+ when 500
41
+ internal_server_error(response)
42
+ when 503
43
+ service_unavailable(response)
44
+ else
45
+ raise Rpush::DeliveryError.new(response.code, @notification.id, Rpush::Daemon::HTTP_STATUS_CODES[response.code.to_i])
46
+ end
47
+ end
48
+
49
+ def ok(response)
50
+ results = process_response(response)
51
+
52
+ handle_successes(results.successes)
53
+
54
+ if results.failures.any?
55
+ handle_failures(results.failures, response)
56
+ else
57
+ mark_delivered
58
+ log_info("#{@notification.id} sent to #{@notification.registration_ids.join(', ')}")
59
+ end
60
+ end
61
+
62
+ def process_response(response)
63
+ body = multi_json_load(response.body)
64
+ results = Results.new(body['results'], @notification.registration_ids)
65
+ results.process(invalid: INVALID_REGISTRATION_ID_STATES, unavailable: UNAVAILABLE_STATES)
66
+ results
67
+ end
68
+
69
+ def handle_successes(successes)
70
+ successes.each do |result|
71
+ reflect(:gcm_delivered_to_recipient, @notification, result[:registration_id])
72
+ if result.has_key?(:canonical_id)
73
+ reflect(:gcm_canonical_id, result[:registration_id], result[:canonical_id])
74
+ end
75
+ end
76
+ end
77
+
78
+ def handle_failures(failures, response)
79
+ if failures[:unavailable].count == @notification.registration_ids.count
80
+ retry_delivery(@notification, response)
81
+ log_warn("All recipients unavailable. #{retry_message}")
82
+ else
83
+ if failures[:unavailable].any?
84
+ unavailable_idxs = failures[:unavailable].map { |result| result[:index] }
85
+ new_notification = create_new_notification(response, unavailable_idxs)
86
+ failures.description += " #{unavailable_idxs.join(', ')} will be retried as notification #{new_notification.id}."
87
+ end
88
+ handle_errors(failures)
89
+ raise Rpush::DeliveryError.new(nil, @notification.id, failures.description)
90
+ end
91
+ end
92
+
93
+ def handle_errors(failures)
94
+ failures.each do |result|
95
+ reflect(:gcm_failed_to_recipient, @notification, result[:error], result[:registration_id])
96
+ end
97
+ failures[:invalid].each do |result|
98
+ reflect(:gcm_invalid_registration_id, @app, result[:error], result[:registration_id])
99
+ end
100
+ end
101
+
102
+ def create_new_notification(response, unavailable_idxs)
103
+ attrs = @notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle')
104
+ registration_ids = @notification.registration_ids.values_at(*unavailable_idxs)
105
+ Rpush::Daemon.store.create_gcm_notification(attrs, @notification.data,
106
+ registration_ids, deliver_after_header(response), @notification.app)
107
+ end
108
+
109
+ def bad_request
110
+ raise Rpush::DeliveryError.new(400, @notification.id, 'GCM failed to parse the JSON request. Possibly an Rpush bug, please open an issue.')
111
+ end
112
+
113
+ def unauthorized
114
+ raise Rpush::DeliveryError.new(401, @notification.id, 'Unauthorized, check your App auth_key.')
115
+ end
116
+
117
+ def internal_server_error(response)
118
+ retry_delivery(@notification, response)
119
+ log_warn("GCM responded with an Internal Error. " + retry_message)
120
+ end
121
+
122
+ def service_unavailable(response)
123
+ retry_delivery(@notification, response)
124
+ log_warn("GCM responded with an Service Unavailable Error. " + retry_message)
125
+ end
126
+
127
+ def deliver_after_header(response)
128
+ Rpush::Daemon::RetryHeaderParser.parse(response.header['retry-after'])
129
+ end
130
+
131
+ def retry_delivery(notification, response)
132
+ if time = deliver_after_header(response)
133
+ mark_retryable(notification, time)
134
+ else
135
+ mark_retryable_exponential(notification)
136
+ end
137
+ end
138
+
139
+ def retry_message
140
+ "Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
141
+ end
142
+
143
+ def do_post
144
+ post = Net::HTTP::Post.new(GCM_URI.path, initheader = {'Content-Type' => 'application/json',
145
+ 'Authorization' => "key=#{@notification.app.auth_key}"})
146
+ post.body = @notification.as_json.to_json
147
+ @http.request(GCM_URI, post)
148
+ end
149
+ end
150
+
151
+ class Results
152
+ attr_reader :successes, :failures
153
+
154
+ def initialize(results_data, registration_ids)
155
+ @results_data = results_data
156
+ @registration_ids = registration_ids
157
+ end
158
+
159
+ def process(failure_partitions = {})
160
+ @successes = []
161
+ @failures = Failures.new
162
+ failure_partitions.each_key do |category|
163
+ failures[category] = []
164
+ end
165
+
166
+ @results_data.each_with_index do |result, index|
167
+ entry = {
168
+ registration_id: @registration_ids[index],
169
+ index: index
170
+ }
171
+ if result['message_id']
172
+ entry[:canonical_id] = result['registration_id'] if result['registration_id'].present?
173
+ successes << entry
174
+ elsif result['error']
175
+ entry[:error] = result['error']
176
+ failures << entry
177
+ failure_partitions.each do |category, error_states|
178
+ failures[category] << entry if error_states.include?(result['error'])
179
+ end
180
+ end
181
+ end
182
+ failures.total_fail = failures.count == @registration_ids.count
183
+ end
184
+ end
185
+
186
+ class Failures < Hash
187
+ include Enumerable
188
+ attr_writer :total_fail, :description
189
+
190
+ def initialize
191
+ super[:all] = []
192
+ end
193
+
194
+ def each
195
+ self[:all].each { |x| yield x }
196
+ end
197
+
198
+ def <<(item)
199
+ self[:all] << item
200
+ end
201
+
202
+ def description
203
+ @description ||= describe
204
+ end
205
+
206
+ private
207
+
208
+ def describe
209
+ if @total_fail
210
+ error_description = "Failed to deliver to all recipients."
211
+ else
212
+ index_list = map { |item| item[:index] }
213
+ error_description = "Failed to deliver to recipients #{index_list.join(', ')}."
214
+ end
215
+
216
+ error_list = map { |item| item[:error] }
217
+ error_description + " Errors: #{error_list.join(', ')}."
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,9 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Gcm
4
+ extend ServiceConfigMethods
5
+
6
+ dispatcher :http
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,61 @@
1
+ module Rpush
2
+ module Daemon
3
+ class InterruptibleSleep
4
+
5
+ def initialize
6
+ @sleep_reader, @wake_writer = IO.pipe
7
+ @udp_wakeup = nil
8
+ end
9
+
10
+ # enable wake on receiving udp packets at the given address and port
11
+ # this returns the host,port used by bind in case an ephemeral port
12
+ # was indicated by specifying 0 as the port number.
13
+ # @return [String,Integer] host,port of bound UDP socket.
14
+ def enable_wake_on_udp(host, port)
15
+ @udp_wakeup = UDPSocket.new
16
+ @udp_wakeup.bind(host, port)
17
+ @udp_wakeup.addr.values_at(3,1)
18
+ end
19
+
20
+ # wait for the given timeout in seconds, or data was written to the pipe
21
+ # or the udp wakeup port if enabled.
22
+ # @return [boolean] true if the sleep was interrupted, or false
23
+ def sleep(timeout)
24
+ read_ports = [@sleep_reader]
25
+ read_ports << @udp_wakeup if @udp_wakeup
26
+ rs, = IO.select(read_ports, nil, nil, timeout) rescue nil
27
+
28
+ # consume all data on the readable io's so that our next call will wait for more data
29
+ perform_io(rs, @sleep_reader, :read_nonblock)
30
+ perform_io(rs, @udp_wakeup, :recv_nonblock)
31
+
32
+ !rs.nil? && rs.any?
33
+ end
34
+
35
+ # writing to the pipe will wake the sleeping thread
36
+ def interrupt_sleep
37
+ @wake_writer.write('.')
38
+ end
39
+
40
+ def close
41
+ @sleep_reader.close rescue nil
42
+ @wake_writer.close rescue nil
43
+ @udp_wakeup.close if @udp_wakeup rescue nil
44
+ end
45
+
46
+ private
47
+
48
+ def perform_io(selected, io, meth)
49
+ if selected && selected.include?(io)
50
+ while true
51
+ begin
52
+ io.__send__(meth, 1)
53
+ rescue Errno::EAGAIN, IO::WaitReadable
54
+ break
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Loggable
4
+ def log_info(msg)
5
+ Rpush.logger.info(app_prefix(msg))
6
+ end
7
+
8
+ def log_warn(msg)
9
+ Rpush.logger.warn(app_prefix(msg))
10
+ end
11
+
12
+ def log_error(e)
13
+ if e.is_a?(Exception)
14
+ Rpush.logger.error(e)
15
+ else
16
+ Rpush.logger.error(app_prefix(e))
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def app_prefix(msg)
23
+ if app = instance_variable_get('@app')
24
+ msg = "[#{app.name}] #{msg}"
25
+ end
26
+
27
+ msg
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module Rpush
2
+ module Daemon
3
+ module Reflectable
4
+ def reflect(name, *args)
5
+ begin
6
+ Rpush.reflections.__dispatch(name, *args)
7
+ rescue StandardError => e
8
+ Rpush.logger.error(e)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module Rpush
2
+ module Daemon
3
+ class RetryHeaderParser
4
+ def self.parse(header)
5
+ new(header).parse
6
+ end
7
+
8
+ def initialize(header)
9
+ @header = header
10
+ end
11
+
12
+ def parse
13
+ if @header
14
+ if @header.to_s =~ /^[0-9]+$/
15
+ Time.now + @header.to_i
16
+ else
17
+ Time.httpdate(@header)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module Rpush
2
+ class RetryableError < StandardError
3
+ attr_reader :code, :description, :response
4
+
5
+ def initialize(code, notification_id, description, response)
6
+ @code = code
7
+ @notification_id = notification_id
8
+ @description = description
9
+ @response = response
10
+ end
11
+
12
+ def to_s
13
+ message
14
+ end
15
+
16
+ def message
17
+ "Retryable error for #{@notification_id}, received error #{@code} (#{@description}) - retry after #{@response.header['retry-after']}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ module Rpush
2
+ module Daemon
3
+ module ServiceConfigMethods
4
+ DISPATCHERS = {
5
+ :http => Rpush::Daemon::Dispatcher::Http,
6
+ :tcp => Rpush::Daemon::Dispatcher::Tcp
7
+ }
8
+
9
+ def dispatcher(name = nil, options = {})
10
+ @dispatcher_name = name
11
+ @dispatcher_options = options
12
+ end
13
+
14
+ def dispatcher_class
15
+ DISPATCHERS[@dispatcher_name] || (raise NotImplementedError)
16
+ end
17
+
18
+ def delivery_class
19
+ const_get('Delivery')
20
+ end
21
+
22
+ def new_dispatcher(app)
23
+ dispatcher_class.new(app, delivery_class, @dispatcher_options)
24
+ end
25
+
26
+ def loops(*loops)
27
+ @loops ||= []
28
+ @loops = loops if loops.any?
29
+ @loops
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,68 @@
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
+ # :nocov:
8
+ if !defined?(::SQLite3::Exception)
9
+ module SQLite3
10
+ class Exception < StandardError; end
11
+ end
12
+ end
13
+
14
+ module Rpush
15
+ module Daemon
16
+ module Store
17
+ class ActiveRecord
18
+ module Reconnectable
19
+ ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error,
20
+ Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]
21
+
22
+ def with_database_reconnect_and_retry
23
+ begin
24
+ ::ActiveRecord::Base.connection_pool.with_connection do
25
+ yield
26
+ end
27
+ rescue *ADAPTER_ERRORS => e
28
+ Rpush.logger.error(e)
29
+ database_connection_lost
30
+ retry
31
+ end
32
+ end
33
+
34
+ def database_connection_lost
35
+ Rpush.logger.warn("Lost connection to database, reconnecting...")
36
+ attempts = 0
37
+ loop do
38
+ begin
39
+ Rpush.logger.warn("Attempt #{attempts += 1}")
40
+ reconnect_database
41
+ check_database_is_connected
42
+ break
43
+ rescue *ADAPTER_ERRORS => e
44
+ Rpush.logger.error(e)
45
+ sleep_to_avoid_thrashing
46
+ end
47
+ end
48
+ Rpush.logger.warn("Database reconnected")
49
+ end
50
+
51
+ def reconnect_database
52
+ ::ActiveRecord::Base.clear_all_connections!
53
+ ::ActiveRecord::Base.establish_connection
54
+ end
55
+
56
+ def check_database_is_connected
57
+ # Simply asking the adapter for the connection state is not sufficient.
58
+ Rpush::Notification.count
59
+ end
60
+
61
+ def sleep_to_avoid_thrashing
62
+ sleep 2
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,154 @@
1
+ require 'active_record'
2
+
3
+ require 'rpush/daemon/store/active_record/reconnectable'
4
+
5
+ module Rpush
6
+ module Daemon
7
+ module Store
8
+ class ActiveRecord
9
+ include Reconnectable
10
+
11
+ DEFAULT_MARK_OPTIONS = {:persist => true}
12
+
13
+ def deliverable_notifications(apps)
14
+ with_database_reconnect_and_retry do
15
+ batch_size = Rpush.config.batch_size
16
+ relation = Rpush::Notification.ready_for_delivery.for_apps(apps)
17
+ relation = relation.limit(batch_size) unless Rpush.config.push
18
+ relation.to_a
19
+ end
20
+ end
21
+
22
+ def mark_retryable(notification, deliver_after, opts = {})
23
+ opts = DEFAULT_MARK_OPTIONS.dup.merge(opts)
24
+ notification.retries += 1
25
+ notification.deliver_after = deliver_after
26
+
27
+ if opts[:persist]
28
+ with_database_reconnect_and_retry do
29
+ notification.save!(:validate => false)
30
+ end
31
+ end
32
+ end
33
+
34
+ def mark_batch_retryable(notifications, deliver_after)
35
+ ids = []
36
+ notifications.each do |n|
37
+ mark_retryable(n, deliver_after, :persist => false)
38
+ ids << n.id
39
+ end
40
+ with_database_reconnect_and_retry do
41
+ Rpush::Notification.where(:id => ids).update_all(['retries = retries + 1, deliver_after = ?', deliver_after])
42
+ end
43
+ end
44
+
45
+ def mark_delivered(notification, time, opts = {})
46
+ opts = DEFAULT_MARK_OPTIONS.dup.merge(opts)
47
+ notification.delivered = true
48
+ notification.delivered_at = time
49
+
50
+ if opts[:persist]
51
+ with_database_reconnect_and_retry do
52
+ notification.save!(:validate => false)
53
+ end
54
+ end
55
+ end
56
+
57
+ def mark_batch_delivered(notifications)
58
+ now = Time.now
59
+ ids = []
60
+ notifications.each do |n|
61
+ mark_delivered(n, now, :persist => false)
62
+ ids << n.id
63
+ end
64
+ with_database_reconnect_and_retry do
65
+ Rpush::Notification.where(:id => ids).update_all(['delivered = ?, delivered_at = ?', true, now])
66
+ end
67
+ end
68
+
69
+ def mark_failed(notification, code, description, time, opts = {})
70
+ opts = DEFAULT_MARK_OPTIONS.dup.merge(opts)
71
+ notification.delivered = false
72
+ notification.delivered_at = nil
73
+ notification.failed = true
74
+ notification.failed_at = time
75
+ notification.error_code = code
76
+ notification.error_description = description
77
+
78
+ if opts[:persist]
79
+ with_database_reconnect_and_retry do
80
+ notification.save!(:validate => false)
81
+ end
82
+ end
83
+ end
84
+
85
+ def mark_batch_failed(notifications, code, description)
86
+ now = Time.now
87
+ ids = []
88
+ notifications.each do |n|
89
+ mark_failed(n, code, description, now, :persist => false)
90
+ ids << n.id
91
+ end
92
+ with_database_reconnect_and_retry do
93
+ Rpush::Notification.where(:id => ids).update_all(['delivered = ?, delivered_at = NULL, failed = ?, failed_at = ?, error_code = ?, error_description = ?', false, true, now, code, description])
94
+ end
95
+ end
96
+
97
+ def create_apns_feedback(failed_at, device_token, app)
98
+ with_database_reconnect_and_retry do
99
+ Rpush::Apns::Feedback.create!(:failed_at => failed_at,
100
+ :device_token => device_token, :app => app)
101
+ end
102
+ end
103
+
104
+ def create_gcm_notification(attrs, data, registration_ids, deliver_after, app)
105
+ notification = Rpush::Gcm::Notification.new
106
+ create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
107
+ end
108
+
109
+ def create_adm_notification(attrs, data, registration_ids, deliver_after, app)
110
+ notification = Rpush::Adm::Notification.new
111
+ create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
112
+ end
113
+
114
+ def update_app(app)
115
+ with_database_reconnect_and_retry do
116
+ app.save!
117
+ end
118
+ end
119
+
120
+ def update_notification(notification)
121
+ with_database_reconnect_and_retry do
122
+ notification.save!
123
+ end
124
+ end
125
+
126
+ def after_daemonize
127
+ reconnect_database
128
+ end
129
+
130
+ def release_connection
131
+ begin
132
+ ::ActiveRecord::Base.connection_pool.release_connection
133
+ rescue StandardError => e
134
+ Rpush.logger.error(e)
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app)
141
+ with_database_reconnect_and_retry do
142
+ notification.assign_attributes(attrs)
143
+ notification.data = data
144
+ notification.registration_ids = registration_ids
145
+ notification.deliver_after = deliver_after
146
+ notification.app = app
147
+ notification.save!
148
+ notification
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end