rpush 8.0.0 → 9.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -6
  3. data/README.md +8 -65
  4. data/lib/generators/templates/rpush.rb +9 -16
  5. data/lib/rpush/client/active_model.rb +0 -4
  6. data/lib/rpush/client/active_record.rb +0 -3
  7. data/lib/rpush/client/redis.rb +0 -3
  8. data/lib/rpush/configuration.rb +2 -19
  9. data/lib/rpush/daemon/service_config_methods.rb +0 -2
  10. data/lib/rpush/daemon/store/active_record.rb +2 -14
  11. data/lib/rpush/daemon/store/interface.rb +2 -2
  12. data/lib/rpush/daemon/store/redis.rb +2 -11
  13. data/lib/rpush/daemon.rb +0 -10
  14. data/lib/rpush/reflection_collection.rb +1 -2
  15. data/lib/rpush/version.rb +1 -1
  16. data/lib/rpush.rb +0 -1
  17. data/spec/functional/cli_spec.rb +41 -15
  18. data/spec/functional/embed_spec.rb +57 -26
  19. data/spec/functional/retry_spec.rb +21 -4
  20. data/spec/functional/synchronization_spec.rb +1 -1
  21. data/spec/functional_spec_helper.rb +0 -6
  22. data/spec/spec_helper.rb +2 -0
  23. data/spec/unit/client/active_record/shared/app.rb +1 -1
  24. data/spec/unit/daemon/shared/store.rb +0 -42
  25. metadata +49 -55
  26. data/lib/rpush/apns_feedback.rb +0 -18
  27. data/lib/rpush/client/active_model/gcm/app.rb +0 -19
  28. data/lib/rpush/client/active_model/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +0 -14
  29. data/lib/rpush/client/active_model/gcm/notification.rb +0 -62
  30. data/lib/rpush/client/active_record/gcm/app.rb +0 -11
  31. data/lib/rpush/client/active_record/gcm/notification.rb +0 -11
  32. data/lib/rpush/client/redis/gcm/app.rb +0 -11
  33. data/lib/rpush/client/redis/gcm/notification.rb +0 -11
  34. data/lib/rpush/daemon/apns/delivery.rb +0 -43
  35. data/lib/rpush/daemon/apns/feedback_receiver.rb +0 -91
  36. data/lib/rpush/daemon/apns.rb +0 -17
  37. data/lib/rpush/daemon/dispatcher/apns_tcp.rb +0 -152
  38. data/lib/rpush/daemon/dispatcher/tcp.rb +0 -22
  39. data/lib/rpush/daemon/gcm/delivery.rb +0 -241
  40. data/lib/rpush/daemon/gcm.rb +0 -9
  41. data/lib/rpush/daemon/tcp_connection.rb +0 -190
  42. data/spec/functional/apns_spec.rb +0 -162
  43. data/spec/functional/gcm_priority_spec.rb +0 -40
  44. data/spec/functional/gcm_spec.rb +0 -46
  45. data/spec/functional/new_app_spec.rb +0 -44
  46. data/spec/unit/apns_feedback_spec.rb +0 -39
  47. data/spec/unit/client/active_record/gcm/app_spec.rb +0 -6
  48. data/spec/unit/client/active_record/gcm/notification_spec.rb +0 -14
  49. data/spec/unit/client/redis/gcm/app_spec.rb +0 -5
  50. data/spec/unit/client/redis/gcm/notification_spec.rb +0 -5
  51. data/spec/unit/client/shared/gcm/app.rb +0 -4
  52. data/spec/unit/client/shared/gcm/notification.rb +0 -77
  53. data/spec/unit/daemon/apns/delivery_spec.rb +0 -108
  54. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +0 -137
  55. data/spec/unit/daemon/dispatcher/tcp_spec.rb +0 -32
  56. data/spec/unit/daemon/gcm/delivery_spec.rb +0 -387
  57. data/spec/unit/daemon/tcp_connection_spec.rb +0 -293
@@ -1,43 +0,0 @@
1
- module Rpush
2
- module Daemon
3
- module Apns
4
- class Delivery < Rpush::Daemon::Delivery
5
- def initialize(app, connection, batch)
6
- @app = app
7
- @connection = connection
8
- @batch = batch
9
- end
10
-
11
- def perform
12
- @connection.write(batch_to_binary)
13
- mark_batch_delivered
14
- describe_deliveries
15
- rescue Rpush::Daemon::TcpConnectionError => error
16
- mark_batch_retryable(Time.now + 10.seconds, error)
17
- raise
18
- rescue StandardError => error
19
- mark_batch_failed(error)
20
- raise
21
- ensure
22
- @batch.all_processed
23
- end
24
-
25
- protected
26
-
27
- def batch_to_binary
28
- payload = ""
29
- @batch.each_notification do |notification|
30
- payload << notification.to_binary
31
- end
32
- payload
33
- end
34
-
35
- def describe_deliveries
36
- @batch.each_notification do |notification|
37
- log_info("#{notification.id} sent to #{notification.device_token}")
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,91 +0,0 @@
1
- # encoding: UTF-8
2
-
3
- module Rpush
4
- module Daemon
5
- module Apns
6
- class FeedbackReceiver
7
- include Reflectable
8
- include Loggable
9
-
10
- TUPLE_BYTES = 38
11
- HOSTS = {
12
- production: ['feedback.push.apple.com', 2196],
13
- development: ['feedback.sandbox.push.apple.com', 2196], # deprecated
14
- sandbox: ['feedback.sandbox.push.apple.com', 2196]
15
- }
16
-
17
- def initialize(app)
18
- @app = app
19
- @host, @port = HOSTS[@app.environment.to_sym]
20
- @certificate = app.certificate
21
- @password = app.password
22
- @interruptible_sleep = InterruptibleSleep.new
23
- end
24
-
25
- def start
26
- return if Rpush.config.push
27
- return unless @app.feedback_enabled
28
- Rpush.logger.info("[#{@app.name}] Starting feedback receiver... ", true)
29
-
30
- @thread = Thread.new do
31
- loop do
32
- break if @stop
33
- check_for_feedback
34
- @interruptible_sleep.sleep(Rpush.config.apns.feedback_receiver.frequency)
35
- end
36
-
37
- Rpush::Daemon.store.release_connection
38
- end
39
-
40
- puts Rainbow('✔').green if Rpush.config.foreground && Rpush.config.foreground_logging
41
- end
42
-
43
- def stop
44
- @stop = true
45
- @interruptible_sleep.stop
46
- @thread.join if @thread
47
- rescue StandardError => e
48
- log_error(e)
49
- reflect(:error, e)
50
- ensure
51
- @thread = nil
52
- end
53
-
54
- def check_for_feedback
55
- connection = nil
56
- begin
57
- connection = Rpush::Daemon::TcpConnection.new(@app, @host, @port)
58
- connection.connect
59
- tuple = connection.read(TUPLE_BYTES)
60
-
61
- while tuple
62
- timestamp, device_token = parse_tuple(tuple)
63
- create_feedback(timestamp, device_token)
64
- tuple = connection.read(TUPLE_BYTES)
65
- end
66
- rescue StandardError => e
67
- log_error(e)
68
- reflect(:error, e)
69
- ensure
70
- connection.close if connection
71
- end
72
- end
73
-
74
- protected
75
-
76
- def parse_tuple(tuple)
77
- failed_at, _, device_token = tuple.unpack("N1n1H*")
78
- [Time.at(failed_at).utc, device_token]
79
- end
80
-
81
- def create_feedback(failed_at, device_token)
82
- formatted_failed_at = failed_at.strftime('%Y-%m-%d %H:%M:%S UTC')
83
- log_info("[FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}.")
84
-
85
- feedback = Rpush::Daemon.store.create_apns_feedback(failed_at, device_token, @app)
86
- reflect(:apns_feedback, feedback)
87
- end
88
- end
89
- end
90
- end
91
- end
@@ -1,17 +0,0 @@
1
- module Rpush
2
- module Daemon
3
- module Apns
4
- extend ServiceConfigMethods
5
-
6
- HOSTS = {
7
- production: ['gateway.push.apple.com', 2195],
8
- development: ['gateway.sandbox.push.apple.com', 2195], # deprecated
9
- sandbox: ['gateway.sandbox.push.apple.com', 2195]
10
- }
11
-
12
- batch_deliveries true
13
- dispatcher :apns_tcp, host: proc { |app| HOSTS[app.environment.to_sym] }
14
- loops Rpush::Daemon::Apns::FeedbackReceiver, if: -> { Rpush.config.apns.feedback_receiver.enabled && !Rpush.config.push }
15
- end
16
- end
17
- end
@@ -1,152 +0,0 @@
1
- module Rpush
2
- module Daemon
3
- module Dispatcher
4
- class ApnsTcp < Rpush::Daemon::Dispatcher::Tcp
5
- include Loggable
6
- include Reflectable
7
-
8
- SELECT_TIMEOUT = 10
9
- ERROR_TUPLE_BYTES = 6
10
- APNS_ERRORS = {
11
- 1 => 'Processing error',
12
- 2 => 'Missing device token',
13
- 3 => 'Missing topic',
14
- 4 => 'Missing payload',
15
- 5 => 'Missing token size',
16
- 6 => 'Missing topic size',
17
- 7 => 'Missing payload size',
18
- 8 => 'Invalid device token',
19
- 10 => 'APNs closed connection (possible maintenance)',
20
- 255 => 'None (unknown error)'
21
- }
22
-
23
- def initialize(*args)
24
- super
25
- @dispatch_mutex = Mutex.new
26
- @stop_error_receiver = false
27
- @connection.on_connect { start_error_receiver }
28
- end
29
-
30
- def dispatch(payload)
31
- @dispatch_mutex.synchronize do
32
- @delivery_class.new(@app, @connection, payload.batch).perform
33
- record_batch(payload.batch)
34
- end
35
- end
36
-
37
- def cleanup
38
- if Rpush.config.push
39
- # In push mode only a single batch is sent, followed by immediate shutdown.
40
- # Allow the error receiver time to handle any errors.
41
- @reconnect_disabled = true
42
- sleep 1
43
- end
44
-
45
- @stop_error_receiver = true
46
- super
47
- @error_receiver_thread.join if @error_receiver_thread
48
- rescue StandardError => e
49
- log_error(e)
50
- reflect(:error, e)
51
- ensure
52
- @error_receiver_thread = nil
53
- end
54
-
55
- private
56
-
57
- def start_error_receiver
58
- @error_receiver_thread = Thread.new do
59
- check_for_error until @stop_error_receiver
60
- Rpush::Daemon.store.release_connection
61
- end
62
- end
63
-
64
- def delivered_buffer
65
- @delivered_buffer ||= RingBuffer.new(Rpush.config.batch_size * 10)
66
- end
67
-
68
- def record_batch(batch)
69
- batch.each_delivered do |notification|
70
- delivered_buffer << notification.id
71
- end
72
- end
73
-
74
- def check_for_error
75
- begin
76
- # On Linux, select returns nil from a dropped connection.
77
- # On OS X, Errno::EBADF is raised following a Errno::EADDRNOTAVAIL from the write call.
78
- return unless @connection.select(SELECT_TIMEOUT)
79
- tuple = @connection.read(ERROR_TUPLE_BYTES)
80
- rescue *TcpConnection::TCP_ERRORS
81
- reconnect unless @stop_error_receiver
82
- return
83
- end
84
-
85
- @dispatch_mutex.synchronize { handle_error_response(tuple) }
86
- rescue StandardError => e
87
- log_error(e)
88
- end
89
-
90
- def handle_error_response(tuple)
91
- if tuple
92
- _, code, notification_id = tuple.unpack('ccN')
93
- handle_error(code, notification_id)
94
- else
95
- handle_disconnect
96
- end
97
-
98
- if Rpush.config.push
99
- # Only attempt to handle a single error in Push mode.
100
- @stop_error_receiver = true
101
- return
102
- end
103
-
104
- reconnect
105
- ensure
106
- delivered_buffer.clear
107
- end
108
-
109
- def reconnect
110
- return if @reconnect_disabled
111
- log_error("Lost connection to #{@connection.host}:#{@connection.port}, reconnecting...")
112
- @connection.reconnect_with_rescue
113
- end
114
-
115
- def handle_disconnect
116
- log_error("The APNs disconnected before any notifications could be delivered. This usually indicates you are using an invalid certificate.") if delivered_buffer.size == 0
117
- end
118
-
119
- def handle_error(code, notification_id)
120
- notification_id = Rpush::Daemon.store.translate_integer_notification_id(notification_id)
121
- failed_pos = delivered_buffer.index(notification_id)
122
- description = description_for_code(code)
123
- log_error("Notification #{notification_id} failed with error: " + description)
124
- Rpush::Daemon.store.mark_ids_failed([notification_id], code, description, Time.now)
125
- reflect(:notification_id_failed, @app, notification_id, code, description)
126
-
127
- if failed_pos
128
- retry_ids = delivered_buffer[(failed_pos + 1)..-1]
129
- retry_notification_ids(retry_ids, notification_id)
130
- elsif delivered_buffer.size > 0
131
- log_error("Delivery sequence unknown for notifications following #{notification_id}.")
132
- end
133
- end
134
-
135
- def description_for_code(code)
136
- APNS_ERRORS[code.to_i] ? "#{APNS_ERRORS[code.to_i]} (#{code})" : "Unknown error code #{code.inspect}. Possible Rpush bug?"
137
- end
138
-
139
- def retry_notification_ids(ids, notification_id)
140
- return if ids.size == 0
141
-
142
- now = Time.now
143
- Rpush::Daemon.store.mark_ids_retryable(ids, now)
144
- notifications_str = 'Notification'
145
- notifications_str += 's' if ids.size > 1
146
- log_warn("#{notifications_str} #{ids.join(', ')} will be retried due to the failure of notification #{notification_id}.")
147
- ids.each { |id| reflect(:notification_id_will_retry, @app, id, now) }
148
- end
149
- end
150
- end
151
- end
152
- end
@@ -1,22 +0,0 @@
1
- module Rpush
2
- module Daemon
3
- module Dispatcher
4
- class Tcp
5
- def initialize(app, delivery_class, options = {})
6
- @app = app
7
- @delivery_class = delivery_class
8
- @host, @port = options[:host].call(@app)
9
- @connection = Rpush::Daemon::TcpConnection.new(@app, @host, @port)
10
- end
11
-
12
- def dispatch(payload)
13
- @delivery_class.new(@app, @connection, payload.notification, payload.batch).perform
14
- end
15
-
16
- def cleanup
17
- @connection.close if @connection
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,241 +0,0 @@
1
- module Rpush
2
- module Daemon
3
- module Gcm
4
- # https://firebase.google.com/docs/cloud-messaging/server
5
- class Delivery < Rpush::Daemon::Delivery
6
- include MultiJsonHelper
7
-
8
- host = 'https://fcm.googleapis.com'
9
- FCM_URI = URI.parse("#{host}/fcm/send")
10
- UNAVAILABLE_STATES = %w(Unavailable BadGateway InternalServerError)
11
- INVALID_REGISTRATION_ID_STATES = %w(InvalidRegistration MismatchSenderId NotRegistered InvalidPackageName)
12
-
13
- def initialize(app, http, notification, batch)
14
- @app = app
15
- @http = http
16
- @notification = notification
17
- @batch = batch
18
- end
19
-
20
- def perform
21
- handle_response(do_post)
22
- rescue SocketError => error
23
- mark_retryable(@notification, Time.now + 10.seconds, error)
24
- raise
25
- rescue StandardError => error
26
- mark_failed(error)
27
- raise
28
- ensure
29
- @batch.notification_processed
30
- end
31
-
32
- protected
33
-
34
- def handle_response(response)
35
- case response.code.to_i
36
- when 200
37
- ok(response)
38
- when 400
39
- bad_request
40
- when 401
41
- unauthorized
42
- when 500
43
- internal_server_error(response)
44
- when 502
45
- bad_gateway(response)
46
- when 503
47
- service_unavailable(response)
48
- when 500..599
49
- other_5xx_error(response)
50
- else
51
- fail Rpush::DeliveryError.new(response.code.to_i, @notification.id, Rpush::Daemon::HTTP_STATUS_CODES[response.code.to_i])
52
- end
53
- end
54
-
55
- def ok(response)
56
- results = process_response(response)
57
- handle_successes(results.successes)
58
-
59
- if results.failures.any?
60
- handle_failures(results.failures, response)
61
- else
62
- mark_delivered
63
- log_info("#{@notification.id} sent to #{@notification.registration_ids.join(', ')}")
64
- end
65
- end
66
-
67
- def process_response(response)
68
- body = multi_json_load(response.body)
69
- results = Results.new(body['results'], @notification.registration_ids)
70
- results.process(invalid: INVALID_REGISTRATION_ID_STATES, unavailable: UNAVAILABLE_STATES)
71
- results
72
- end
73
-
74
- def handle_successes(successes)
75
- successes.each do |result|
76
- reflect(:gcm_delivered_to_recipient, @notification, result[:registration_id])
77
- next unless result.key?(:canonical_id)
78
- reflect(:gcm_canonical_id, result[:registration_id], result[:canonical_id])
79
- end
80
- end
81
-
82
- def handle_failures(failures, response)
83
- if failures[:unavailable].count == @notification.registration_ids.count
84
- retry_delivery(@notification, response)
85
- log_warn("All recipients unavailable. #{retry_message}")
86
- else
87
- if failures[:unavailable].any?
88
- unavailable_idxs = failures[:unavailable].map { |result| result[:index] }
89
- new_notification = create_new_notification(response, unavailable_idxs)
90
- failures.description += " #{unavailable_idxs.join(', ')} will be retried as notification #{new_notification.id}."
91
- end
92
- handle_errors(failures)
93
- fail Rpush::DeliveryError.new(nil, @notification.id, failures.description)
94
- end
95
- end
96
-
97
- def handle_errors(failures)
98
- failures.each do |result|
99
- reflect(:gcm_failed_to_recipient, @notification, result[:error], result[:registration_id])
100
- end
101
- failures[:invalid].each do |result|
102
- reflect(:gcm_invalid_registration_id, @app, result[:error], result[:registration_id])
103
- end
104
- end
105
-
106
- def create_new_notification(response, unavailable_idxs)
107
- attrs = { 'app_id' => @notification.app_id, 'collapse_key' => @notification.collapse_key, 'delay_while_idle' => @notification.delay_while_idle }
108
- registration_ids = @notification.registration_ids.values_at(*unavailable_idxs)
109
- Rpush::Daemon.store.create_gcm_notification(attrs, @notification.data,
110
- registration_ids, deliver_after_header(response), @app)
111
- end
112
-
113
- def bad_request
114
- fail Rpush::DeliveryError.new(400, @notification.id, 'GCM failed to parse the JSON request. Possibly an Rpush bug, please open an issue.')
115
- end
116
-
117
- def unauthorized
118
- fail Rpush::DeliveryError.new(401, @notification.id, 'Unauthorized, check your App auth_key.')
119
- end
120
-
121
- def internal_server_error(response)
122
- retry_delivery(@notification, response)
123
- log_warn("GCM responded with an Internal Error. " + retry_message)
124
- end
125
-
126
- def bad_gateway(response)
127
- retry_delivery(@notification, response)
128
- log_warn("GCM responded with a Bad Gateway Error. " + retry_message)
129
- end
130
-
131
- def service_unavailable(response)
132
- retry_delivery(@notification, response)
133
- log_warn("GCM responded with an Service Unavailable Error. " + retry_message)
134
- end
135
-
136
- def other_5xx_error(response)
137
- retry_delivery(@notification, response)
138
- log_warn("GCM responded with a 5xx Error. " + retry_message)
139
- end
140
-
141
- def deliver_after_header(response)
142
- Rpush::Daemon::RetryHeaderParser.parse(response.header['retry-after'])
143
- end
144
-
145
- def retry_delivery(notification, response)
146
- time = deliver_after_header(response)
147
- if time
148
- mark_retryable(notification, time)
149
- else
150
- mark_retryable_exponential(notification)
151
- end
152
- end
153
-
154
- def retry_message
155
- "Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime('%Y-%m-%d %H:%M:%S')} (retry #{@notification.retries})."
156
- end
157
-
158
- def do_post
159
- post = Net::HTTP::Post.new(FCM_URI.path, 'Content-Type' => 'application/json',
160
- 'Authorization' => "key=#{@app.auth_key}")
161
- post.body = @notification.as_json.to_json
162
- @http.request(FCM_URI, post)
163
- end
164
- end
165
-
166
- class Results
167
- attr_reader :successes, :failures
168
-
169
- def initialize(results_data, registration_ids)
170
- @results_data = results_data
171
- @registration_ids = registration_ids
172
- end
173
-
174
- def process(failure_partitions = {}) # rubocop:disable Metrics/AbcSize
175
- @successes = []
176
- @failures = Failures.new
177
- failure_partitions.each_key do |category|
178
- failures[category] = []
179
- end
180
-
181
- @results_data.each_with_index do |result, index|
182
- entry = {
183
- registration_id: @registration_ids[index],
184
- index: index
185
- }
186
- if result['message_id']
187
- entry[:canonical_id] = result['registration_id'] if result['registration_id'].present?
188
- successes << entry
189
- elsif result['error']
190
- entry[:error] = result['error']
191
- failures << entry
192
- failure_partitions.each do |category, error_states|
193
- failures[category] << entry if error_states.include?(result['error'])
194
- end
195
- end
196
- end
197
- failures.all_failed = failures.count == @registration_ids.count
198
- end
199
- end
200
-
201
- class Failures < Hash
202
- include Enumerable
203
- attr_writer :all_failed, :description
204
-
205
- def initialize
206
- super[:all] = []
207
- end
208
-
209
- def each
210
- self[:all].each { |x| yield x }
211
- end
212
-
213
- def <<(item)
214
- self[:all] << item
215
- end
216
-
217
- def description
218
- @description ||= describe
219
- end
220
-
221
- def any?
222
- self[:all].any?
223
- end
224
-
225
- private
226
-
227
- def describe
228
- if @all_failed
229
- error_description = "Failed to deliver to all recipients."
230
- else
231
- index_list = map { |item| item[:index] }
232
- error_description = "Failed to deliver to recipients #{index_list.join(', ')}."
233
- end
234
-
235
- error_list = map { |item| item[:error] }
236
- error_description + " Errors: #{error_list.join(', ')}."
237
- end
238
- end
239
- end
240
- end
241
- end
@@ -1,9 +0,0 @@
1
- module Rpush
2
- module Daemon
3
- module Gcm
4
- extend ServiceConfigMethods
5
-
6
- dispatcher :http
7
- end
8
- end
9
- end