rpush 2.2.0 → 2.3.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +17 -9
- data/lib/generators/rpush_migration_generator.rb +1 -1
- data/lib/rpush.rb +4 -1
- data/lib/rpush/apns_feedback.rb +1 -1
- data/lib/rpush/cli.rb +25 -22
- data/lib/rpush/client/active_model/apns/app.rb +1 -1
- data/lib/rpush/client/active_model/apns/notification.rb +2 -2
- data/lib/rpush/client/active_model/wpns/notification.rb +7 -1
- data/lib/rpush/configuration.rb +32 -22
- data/lib/rpush/daemon.rb +18 -9
- data/lib/rpush/daemon/adm/delivery.rb +4 -1
- data/lib/rpush/daemon/apns/delivery.rb +3 -0
- data/lib/rpush/daemon/apns/feedback_receiver.rb +6 -2
- data/lib/rpush/daemon/app_runner.rb +11 -16
- data/lib/rpush/daemon/batch.rb +9 -0
- data/lib/rpush/daemon/delivery.rb +10 -2
- data/lib/rpush/daemon/delivery_error.rb +3 -3
- data/lib/rpush/daemon/dispatcher/apns_tcp.rb +11 -11
- data/lib/rpush/daemon/dispatcher/tcp.rb +2 -10
- data/lib/rpush/daemon/gcm/delivery.rb +13 -7
- data/lib/rpush/daemon/signal_handler.rb +5 -3
- data/lib/rpush/daemon/store/active_record.rb +14 -0
- data/lib/rpush/daemon/store/interface.rb +2 -1
- data/lib/rpush/daemon/store/redis.rb +3 -0
- data/lib/rpush/daemon/tcp_connection.rb +47 -17
- data/lib/rpush/daemon/wpns/delivery.rb +19 -10
- data/lib/rpush/logger.rb +30 -12
- data/lib/rpush/plugin.rb +44 -0
- data/lib/rpush/push.rb +1 -1
- data/lib/rpush/reflectable.rb +11 -0
- data/lib/rpush/{reflection.rb → reflection_collection.rb} +1 -9
- data/lib/rpush/reflection_public_methods.rb +9 -0
- data/lib/rpush/version.rb +1 -1
- data/lib/tasks/test.rake +6 -4
- data/spec/functional/adm_spec.rb +12 -0
- data/spec/functional/apns_spec.rb +61 -42
- data/spec/functional/gcm_spec.rb +9 -0
- data/spec/functional/retry_spec.rb +1 -1
- data/spec/functional/wpns_spec.rb +44 -11
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/apns_feedback_spec.rb +2 -2
- data/spec/unit/client/active_record/apns/app_spec.rb +2 -2
- data/spec/unit/client/active_record/apns/notification_spec.rb +1 -2
- data/spec/unit/client/active_record/wpns/notification_spec.rb +9 -3
- data/spec/unit/configuration_spec.rb +1 -0
- data/spec/unit/daemon/adm/delivery_spec.rb +0 -1
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +0 -1
- data/spec/unit/daemon/app_runner_spec.rb +14 -9
- data/spec/unit/daemon/delivery_spec.rb +0 -1
- data/spec/unit/daemon/dispatcher/tcp_spec.rb +0 -7
- data/spec/unit/daemon/gcm/delivery_spec.rb +1 -1
- data/spec/unit/daemon/signal_handler_spec.rb +5 -1
- data/spec/unit/daemon/store/active_record_spec.rb +1 -1
- data/spec/unit/daemon/tcp_connection_spec.rb +18 -18
- data/spec/unit/daemon/wpns/delivery_spec.rb +1 -1
- data/spec/unit/daemon_spec.rb +10 -2
- data/spec/unit/logger_spec.rb +0 -1
- data/spec/unit/plugin_spec.rb +36 -0
- data/spec/unit/push_spec.rb +2 -2
- data/spec/unit/{daemon/reflectable_spec.rb → reflectable_spec.rb} +6 -6
- data/spec/unit/{reflection_spec.rb → reflection_collection_spec.rb} +4 -8
- metadata +16 -18
- data/lib/rpush/daemon/reflectable.rb +0 -11
- data/spec/integration/rpush_spec.rb +0 -13
- data/spec/integration/support/gcm_success_response.json +0 -1
- data/spec/support/install.sh +0 -68
@@ -40,6 +40,9 @@ module Rpush
|
|
40
40
|
handle_rate_limited(error)
|
41
41
|
rescue Rpush::RetryableError => error
|
42
42
|
handle_retryable(error)
|
43
|
+
rescue SocketError => error
|
44
|
+
mark_retryable(@notification, Time.now + 10.seconds, error)
|
45
|
+
raise
|
43
46
|
rescue StandardError => error
|
44
47
|
mark_failed(error)
|
45
48
|
raise
|
@@ -176,7 +179,7 @@ module Rpush
|
|
176
179
|
end
|
177
180
|
|
178
181
|
def retry_message
|
179
|
-
"Notification #{@notification.id} will be retired after #{@notification.deliver_after.strftime(
|
182
|
+
"Notification #{@notification.id} will be retired after #{@notification.deliver_after.strftime('%Y-%m-%d %H:%M:%S')} (retry #{@notification.retries})."
|
180
183
|
end
|
181
184
|
|
182
185
|
def do_post(registration_id)
|
@@ -12,6 +12,9 @@ module Rpush
|
|
12
12
|
@connection.write(batch_to_binary)
|
13
13
|
mark_batch_delivered
|
14
14
|
describe_deliveries
|
15
|
+
rescue Rpush::Daemon::TcpConnectionError => error
|
16
|
+
mark_batch_retryable(Time.now + 10.seconds, error)
|
17
|
+
raise
|
15
18
|
rescue StandardError => error
|
16
19
|
mark_batch_failed(error)
|
17
20
|
raise
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
module Rpush
|
2
4
|
module Daemon
|
3
5
|
module Apns
|
@@ -22,7 +24,7 @@ module Rpush
|
|
22
24
|
|
23
25
|
def start
|
24
26
|
return if Rpush.config.push
|
25
|
-
|
27
|
+
Rpush.logger.info("[#{@app.name}] Starting feedback receiver... ", true)
|
26
28
|
@interruptible_sleep.start
|
27
29
|
|
28
30
|
@thread = Thread.new do
|
@@ -34,6 +36,8 @@ module Rpush
|
|
34
36
|
|
35
37
|
Rpush::Daemon.store.release_connection
|
36
38
|
end
|
39
|
+
|
40
|
+
puts ANSI.green { '✔' } if Rpush.config.foreground
|
37
41
|
end
|
38
42
|
|
39
43
|
def stop
|
@@ -70,7 +74,7 @@ module Rpush
|
|
70
74
|
end
|
71
75
|
|
72
76
|
def create_feedback(failed_at, device_token)
|
73
|
-
formatted_failed_at = failed_at.strftime(
|
77
|
+
formatted_failed_at = failed_at.strftime('%Y-%m-%d %H:%M:%S UTC')
|
74
78
|
log_info("[FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}.")
|
75
79
|
|
76
80
|
feedback = Rpush::Daemon.store.create_apns_feedback(failed_at, device_token, @app)
|
@@ -3,8 +3,6 @@
|
|
3
3
|
module Rpush
|
4
4
|
module Daemon
|
5
5
|
class AppRunner
|
6
|
-
extend Term::ANSIColor
|
7
|
-
|
8
6
|
extend Reflectable
|
9
7
|
include Reflectable
|
10
8
|
include Loggable
|
@@ -29,9 +27,10 @@ module Rpush
|
|
29
27
|
|
30
28
|
def self.start_app(app)
|
31
29
|
Rpush.logger.info("[#{app.name}] Starting #{pluralize(app.connections, 'dispatcher')}... ", true)
|
32
|
-
@runners[app.id] = new(app)
|
33
|
-
|
34
|
-
puts green
|
30
|
+
runner = @runners[app.id] = new(app)
|
31
|
+
runner.start_dispatchers
|
32
|
+
puts ANSI.green { '✔' } if Rpush.config.foreground
|
33
|
+
runner.start_loops
|
35
34
|
rescue StandardError => e
|
36
35
|
@runners.delete(app.id)
|
37
36
|
Rpush.logger.error("[#{app.name}] Exception raised during startup. Notifications will not be delivered for this app.")
|
@@ -90,6 +89,7 @@ module Rpush
|
|
90
89
|
end
|
91
90
|
|
92
91
|
attr_reader :app
|
92
|
+
delegate :size, to: :queue, prefix: true
|
93
93
|
|
94
94
|
def initialize(app)
|
95
95
|
@app = app
|
@@ -97,9 +97,13 @@ module Rpush
|
|
97
97
|
@dispatcher_loops = []
|
98
98
|
end
|
99
99
|
|
100
|
-
def
|
100
|
+
def start_dispatchers
|
101
101
|
app.connections.times { @dispatcher_loops.push(new_dispatcher_loop) }
|
102
|
-
|
102
|
+
end
|
103
|
+
|
104
|
+
def start_loops
|
105
|
+
@loops = service.loop_instances(@app)
|
106
|
+
@loops.map(&:start)
|
103
107
|
end
|
104
108
|
|
105
109
|
def stop
|
@@ -151,21 +155,12 @@ module Rpush
|
|
151
155
|
log_info(JSON.pretty_generate(runner_details))
|
152
156
|
end
|
153
157
|
|
154
|
-
def queue_size
|
155
|
-
queue.size
|
156
|
-
end
|
157
|
-
|
158
158
|
def num_dispatcher_loops
|
159
159
|
@dispatcher_loops.size
|
160
160
|
end
|
161
161
|
|
162
162
|
private
|
163
163
|
|
164
|
-
def start_loops
|
165
|
-
@loops = service.loop_instances(@app)
|
166
|
-
@loops.map(&:start)
|
167
|
-
end
|
168
|
-
|
169
164
|
def stop_loops
|
170
165
|
@loops.map(&:stop)
|
171
166
|
@loops = []
|
data/lib/rpush/daemon/batch.rb
CHANGED
@@ -34,6 +34,15 @@ module Rpush
|
|
34
34
|
Rpush::Daemon.store.mark_retryable(notification, deliver_after, persist: false)
|
35
35
|
end
|
36
36
|
|
37
|
+
def mark_all_retryable(deliver_after)
|
38
|
+
@mutex.synchronize do
|
39
|
+
@retryable[deliver_after] = @notifications
|
40
|
+
end
|
41
|
+
each_notification do |notification|
|
42
|
+
Rpush::Daemon.store.mark_retryable(notification, deliver_after, persist: false)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
37
46
|
def mark_delivered(notification)
|
38
47
|
@mutex.synchronize do
|
39
48
|
@delivered << notification
|
@@ -4,10 +4,13 @@ module Rpush
|
|
4
4
|
include Reflectable
|
5
5
|
include Loggable
|
6
6
|
|
7
|
-
def mark_retryable(notification, deliver_after)
|
7
|
+
def mark_retryable(notification, deliver_after, error = nil)
|
8
8
|
if notification.fail_after && notification.fail_after < Time.now
|
9
|
-
@batch.mark_failed(notification, nil, "Notification failed to be delivered before #{notification.fail_after.strftime(
|
9
|
+
@batch.mark_failed(notification, nil, "Notification failed to be delivered before #{notification.fail_after.strftime('%Y-%m-%d %H:%M:%S')}.")
|
10
10
|
else
|
11
|
+
if error
|
12
|
+
log_warn("Will retry notification #{notification.id} after #{deliver_after.strftime('%Y-%m-%d %H:%M:%S')} due to error (#{error.class.name}, #{error.message})")
|
13
|
+
end
|
11
14
|
@batch.mark_retryable(notification, deliver_after)
|
12
15
|
end
|
13
16
|
end
|
@@ -16,6 +19,11 @@ module Rpush
|
|
16
19
|
mark_retryable(notification, Time.now + 2**(notification.retries + 1))
|
17
20
|
end
|
18
21
|
|
22
|
+
def mark_batch_retryable(deliver_after, error)
|
23
|
+
log_warn("Will retry #{@batch.notifications.size} notifications after #{deliver_after.strftime('%Y-%m-%d %H:%M:%S')} due to error (#{error.class.name}, #{error.message})")
|
24
|
+
@batch.mark_all_retryable(deliver_after)
|
25
|
+
end
|
26
|
+
|
19
27
|
def mark_delivered
|
20
28
|
@batch.mark_delivered(@notification)
|
21
29
|
end
|
@@ -19,9 +19,9 @@ module Rpush
|
|
19
19
|
|
20
20
|
def ==(other)
|
21
21
|
other.is_a?(DeliveryError) && \
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
other.code == code && \
|
23
|
+
other.notification_id == notification_id && \
|
24
|
+
other.to_s == to_s
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -28,7 +28,7 @@ module Rpush
|
|
28
28
|
|
29
29
|
def dispatch(payload)
|
30
30
|
@dispatch_mutex.synchronize do
|
31
|
-
@delivery_class.new(@app, connection, payload.batch).perform
|
31
|
+
@delivery_class.new(@app, @connection, payload.batch).perform
|
32
32
|
record_batch(payload.batch)
|
33
33
|
end
|
34
34
|
end
|
@@ -60,13 +60,15 @@ module Rpush
|
|
60
60
|
|
61
61
|
def check_for_error
|
62
62
|
begin
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
# On Linux, select returns nil from a dropped connection.
|
64
|
+
# On OS X, Errno::EBADF is raised following a Errno::EADDRNOTAVAIL from the write call.
|
65
|
+
return unless @connection.select(SELECT_TIMEOUT)
|
66
|
+
rescue SystemCallError
|
67
|
+
# Connection closed.
|
66
68
|
return
|
67
69
|
end
|
68
70
|
|
69
|
-
tuple = connection.read(ERROR_TUPLE_BYTES)
|
71
|
+
tuple = @connection.read(ERROR_TUPLE_BYTES)
|
70
72
|
@dispatch_mutex.synchronize { handle_error_response(tuple) }
|
71
73
|
end
|
72
74
|
|
@@ -78,22 +80,20 @@ module Rpush
|
|
78
80
|
handle_disconnect
|
79
81
|
end
|
80
82
|
|
81
|
-
log_error(
|
82
|
-
connection.
|
83
|
+
log_error("Lost connection to #{@connection.host}:#{@connection.port}, reconnecting...")
|
84
|
+
@connection.reconnect_with_rescue
|
83
85
|
ensure
|
84
86
|
delivered_buffer.clear
|
85
87
|
end
|
86
88
|
|
87
89
|
def handle_disconnect
|
88
|
-
log_error(
|
89
|
-
reason = 'The APNs disconnected without returning an error. This may indicate you are using an invalid certificate.'
|
90
|
-
Rpush::Daemon.store.mark_ids_failed(delivered_buffer, nil, reason, Time.now)
|
91
|
-
delivered_buffer.each { |id| reflect(:notification_id_failed, @app, id, nil, reason) }
|
90
|
+
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
|
92
91
|
end
|
93
92
|
|
94
93
|
def handle_error(code, notification_id)
|
95
94
|
failed_pos = delivered_buffer.index(notification_id)
|
96
95
|
description = APNS_ERRORS[code.to_i] || "Unknown error code #{code.inspect}. Possible Rpush bug?"
|
96
|
+
log_error(description + " (#{code})")
|
97
97
|
Rpush::Daemon.store.mark_ids_failed([notification_id], code, description, Time.now)
|
98
98
|
reflect(:notification_id_failed, @app, notification_id, code, description)
|
99
99
|
|
@@ -6,24 +6,16 @@ module Rpush
|
|
6
6
|
@app = app
|
7
7
|
@delivery_class = delivery_class
|
8
8
|
@host, @port = options[:host].call(@app)
|
9
|
+
@connection = Rpush::Daemon::TcpConnection.new(@app, @host, @port)
|
9
10
|
end
|
10
11
|
|
11
12
|
def dispatch(payload)
|
12
|
-
@delivery_class.new(@app, connection, payload.notification, payload.batch).perform
|
13
|
+
@delivery_class.new(@app, @connection, payload.notification, payload.batch).perform
|
13
14
|
end
|
14
15
|
|
15
16
|
def cleanup
|
16
17
|
@connection.close if @connection
|
17
18
|
end
|
18
|
-
|
19
|
-
protected
|
20
|
-
|
21
|
-
def connection
|
22
|
-
return @connection if defined? @connection
|
23
|
-
connection = Rpush::Daemon::TcpConnection.new(@app, @host, @port)
|
24
|
-
connection.connect
|
25
|
-
@connection = connection
|
26
|
-
end
|
27
19
|
end
|
28
20
|
end
|
29
21
|
end
|
@@ -5,7 +5,7 @@ module Rpush
|
|
5
5
|
class Delivery < Rpush::Daemon::Delivery
|
6
6
|
include MultiJsonHelper
|
7
7
|
|
8
|
-
host =
|
8
|
+
host = 'https://android.googleapis.com'
|
9
9
|
GCM_URI = URI.parse("#{host}/gcm/send")
|
10
10
|
UNAVAILABLE_STATES = %w(Unavailable InternalServerError)
|
11
11
|
INVALID_REGISTRATION_ID_STATES = %w(InvalidRegistration MismatchSenderId NotRegistered InvalidPackageName)
|
@@ -19,6 +19,9 @@ module Rpush
|
|
19
19
|
|
20
20
|
def perform
|
21
21
|
handle_response(do_post)
|
22
|
+
rescue SocketError => error
|
23
|
+
mark_retryable(@notification, Time.now + 10.seconds, error)
|
24
|
+
raise
|
22
25
|
rescue StandardError => error
|
23
26
|
mark_failed(error)
|
24
27
|
raise
|
@@ -47,7 +50,6 @@ module Rpush
|
|
47
50
|
|
48
51
|
def ok(response)
|
49
52
|
results = process_response(response)
|
50
|
-
|
51
53
|
handle_successes(results.successes)
|
52
54
|
|
53
55
|
if results.failures.any?
|
@@ -136,7 +138,7 @@ module Rpush
|
|
136
138
|
end
|
137
139
|
|
138
140
|
def retry_message
|
139
|
-
"Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime(
|
141
|
+
"Notification #{@notification.id} will be retried after #{@notification.deliver_after.strftime('%Y-%m-%d %H:%M:%S')} (retry #{@notification.retries})."
|
140
142
|
end
|
141
143
|
|
142
144
|
def do_post
|
@@ -155,7 +157,7 @@ module Rpush
|
|
155
157
|
@registration_ids = registration_ids
|
156
158
|
end
|
157
159
|
|
158
|
-
def process(failure_partitions = {})
|
160
|
+
def process(failure_partitions = {}) # rubocop:disable Metrics/AbcSize
|
159
161
|
@successes = []
|
160
162
|
@failures = Failures.new
|
161
163
|
failure_partitions.each_key do |category|
|
@@ -178,13 +180,13 @@ module Rpush
|
|
178
180
|
end
|
179
181
|
end
|
180
182
|
end
|
181
|
-
failures.
|
183
|
+
failures.all_failed = failures.count == @registration_ids.count
|
182
184
|
end
|
183
185
|
end
|
184
186
|
|
185
187
|
class Failures < Hash
|
186
188
|
include Enumerable
|
187
|
-
attr_writer :
|
189
|
+
attr_writer :all_failed, :description
|
188
190
|
|
189
191
|
def initialize
|
190
192
|
super[:all] = []
|
@@ -202,10 +204,14 @@ module Rpush
|
|
202
204
|
@description ||= describe
|
203
205
|
end
|
204
206
|
|
207
|
+
def any?
|
208
|
+
self[:all].any?
|
209
|
+
end
|
210
|
+
|
205
211
|
private
|
206
212
|
|
207
213
|
def describe
|
208
|
-
if @
|
214
|
+
if @all_failed
|
209
215
|
error_description = "Failed to deliver to all recipients."
|
210
216
|
else
|
211
217
|
index_list = map { |item| item[:index] }
|
@@ -40,7 +40,7 @@ module Rpush
|
|
40
40
|
Rpush.logger.error("Unhandled signal: #{signal}")
|
41
41
|
end
|
42
42
|
rescue StandardError => e
|
43
|
-
Rpush.logger.error("Error raised when
|
43
|
+
Rpush.logger.error("Error raised when handling signal '#{signal}'")
|
44
44
|
Rpush.logger.error(e)
|
45
45
|
end
|
46
46
|
end
|
@@ -48,13 +48,15 @@ module Rpush
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def self.handle_hup
|
51
|
-
Rpush.logger.
|
51
|
+
Rpush.logger.reopen
|
52
|
+
Rpush.logger.info('Received HUP signal.')
|
53
|
+
Rpush::Daemon.store.reopen_log
|
52
54
|
Synchronizer.sync
|
53
55
|
Feeder.wakeup
|
54
56
|
end
|
55
57
|
|
56
58
|
def self.handle_usr2
|
57
|
-
Rpush.logger.info(
|
59
|
+
Rpush.logger.info('Received USR2 signal.')
|
58
60
|
AppRunner.debug
|
59
61
|
end
|
60
62
|
|
@@ -10,6 +10,14 @@ module Rpush
|
|
10
10
|
|
11
11
|
DEFAULT_MARK_OPTIONS = { persist: true }
|
12
12
|
|
13
|
+
def initialize
|
14
|
+
reopen_log
|
15
|
+
end
|
16
|
+
|
17
|
+
def reopen_log
|
18
|
+
::ActiveRecord::Base.logger = Rpush.logger.internal_logger
|
19
|
+
end
|
20
|
+
|
13
21
|
def app(id)
|
14
22
|
Rpush::Client::ActiveRecord::App.find(id)
|
15
23
|
end
|
@@ -53,6 +61,8 @@ module Rpush
|
|
53
61
|
end
|
54
62
|
|
55
63
|
def mark_ids_retryable(ids, deliver_after)
|
64
|
+
return if ids.empty?
|
65
|
+
|
56
66
|
with_database_reconnect_and_retry do
|
57
67
|
Rpush::Client::ActiveRecord::Notification.where(id: ids).update_all(['processing = ?, delivered = ?, delivered_at = ?, failed = ?, failed_at = ?, retries = retries + 1, deliver_after = ?', false, false, nil, false, nil, deliver_after])
|
58
68
|
end
|
@@ -72,6 +82,8 @@ module Rpush
|
|
72
82
|
end
|
73
83
|
|
74
84
|
def mark_batch_delivered(notifications)
|
85
|
+
return if notifications.empty?
|
86
|
+
|
75
87
|
now = Time.now
|
76
88
|
ids = []
|
77
89
|
notifications.each do |n|
|
@@ -111,6 +123,8 @@ module Rpush
|
|
111
123
|
end
|
112
124
|
|
113
125
|
def mark_ids_failed(ids, code, description, time)
|
126
|
+
return if ids.empty?
|
127
|
+
|
114
128
|
with_database_reconnect_and_retry do
|
115
129
|
Rpush::Client::ActiveRecord::Notification.where(id: ids).update_all(['processing = ?, delivered = ?, delivered_at = NULL, failed = ?, failed_at = ?, error_code = ?, error_description = ?', false, false, true, time, code, description])
|
116
130
|
end
|
@@ -7,7 +7,8 @@ module Rpush
|
|
7
7
|
:mark_failed, :mark_batch_failed, :create_apns_feedback,
|
8
8
|
:create_gcm_notification, :create_adm_notification, :update_app,
|
9
9
|
:update_notification, :release_connection,
|
10
|
-
:all_apps, :app, :mark_ids_failed, :mark_ids_retryable
|
10
|
+
:all_apps, :app, :mark_ids_failed, :mark_ids_retryable,
|
11
|
+
:reopen_log]
|
11
12
|
|
12
13
|
def self.check(klass)
|
13
14
|
missing = PUBLIC_METHODS - klass.instance_methods.map(&:to_sym)
|
@@ -6,7 +6,14 @@ module Rpush
|
|
6
6
|
include Reflectable
|
7
7
|
include Loggable
|
8
8
|
|
9
|
-
|
9
|
+
OSX_TCP_KEEPALIVE = 0x10 # Defined in <netinet/tcp.h>
|
10
|
+
KEEPALIVE_INTERVAL = 5
|
11
|
+
KEEPALIVE_IDLE = 5
|
12
|
+
KEEPALIVE_MAX_FAIL_PROBES = 1
|
13
|
+
TCP_ERRORS = [SystemCallError, OpenSSL::OpenSSLError, IOError]
|
14
|
+
|
15
|
+
attr_accessor :last_touch
|
16
|
+
attr_reader :host, :port
|
10
17
|
|
11
18
|
def self.idle_period
|
12
19
|
30.minutes
|
@@ -18,12 +25,14 @@ module Rpush
|
|
18
25
|
@port = port
|
19
26
|
@certificate = app.certificate
|
20
27
|
@password = app.password
|
21
|
-
|
28
|
+
@connected = false
|
29
|
+
touch
|
22
30
|
end
|
23
31
|
|
24
32
|
def connect
|
25
33
|
@ssl_context = setup_ssl_context
|
26
34
|
@tcp_socket, @ssl_socket = connect_socket
|
35
|
+
@connected = true
|
27
36
|
end
|
28
37
|
|
29
38
|
def close
|
@@ -33,38 +42,45 @@ module Rpush
|
|
33
42
|
end
|
34
43
|
|
35
44
|
def read(num_bytes)
|
36
|
-
@ssl_socket.read(num_bytes)
|
45
|
+
@ssl_socket.read(num_bytes) if @ssl_socket
|
37
46
|
end
|
38
47
|
|
39
48
|
def select(timeout)
|
40
|
-
IO.select([@ssl_socket], nil, nil, timeout)
|
49
|
+
IO.select([@ssl_socket], nil, nil, timeout) if @ssl_socket
|
41
50
|
end
|
42
51
|
|
43
52
|
def write(data)
|
53
|
+
connect unless @connected
|
44
54
|
reconnect_idle if idle_period_exceeded?
|
45
55
|
|
46
56
|
retry_count = 0
|
47
57
|
|
48
58
|
begin
|
49
59
|
write_data(data)
|
50
|
-
rescue
|
60
|
+
rescue *TCP_ERRORS => e
|
51
61
|
retry_count += 1
|
52
62
|
|
53
63
|
if retry_count == 1
|
54
|
-
log_error("Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
|
64
|
+
log_error("Lost connection to #{@host}:#{@port} (#{e.class.name}, #{e.message}), reconnecting...")
|
55
65
|
reflect(:tcp_connection_lost, @app, e)
|
56
66
|
end
|
57
67
|
|
58
68
|
if retry_count <= 3
|
59
|
-
|
69
|
+
reconnect_with_rescue
|
60
70
|
sleep 1
|
61
71
|
retry
|
62
72
|
else
|
63
|
-
raise TcpConnectionError, "#{@app.name} tried #{retry_count - 1} times to reconnect but failed (#{e.class.name})."
|
73
|
+
raise TcpConnectionError, "#{@app.name} tried #{retry_count - 1} times to reconnect but failed (#{e.class.name}, #{e.message})."
|
64
74
|
end
|
65
75
|
end
|
66
76
|
end
|
67
77
|
|
78
|
+
def reconnect_with_rescue
|
79
|
+
reconnect
|
80
|
+
rescue StandardError => e
|
81
|
+
log_error(e)
|
82
|
+
end
|
83
|
+
|
68
84
|
def reconnect
|
69
85
|
close
|
70
86
|
@tcp_socket, @ssl_socket = connect_socket
|
@@ -78,17 +94,17 @@ module Rpush
|
|
78
94
|
end
|
79
95
|
|
80
96
|
def idle_period_exceeded?
|
81
|
-
Time.now -
|
97
|
+
Time.now - last_touch > self.class.idle_period
|
82
98
|
end
|
83
99
|
|
84
100
|
def write_data(data)
|
85
101
|
@ssl_socket.write(data)
|
86
102
|
@ssl_socket.flush
|
87
|
-
|
103
|
+
touch
|
88
104
|
end
|
89
105
|
|
90
|
-
def
|
91
|
-
self.
|
106
|
+
def touch
|
107
|
+
self.last_touch = Time.now
|
92
108
|
end
|
93
109
|
|
94
110
|
def setup_ssl_context
|
@@ -99,21 +115,35 @@ module Rpush
|
|
99
115
|
end
|
100
116
|
|
101
117
|
def connect_socket
|
118
|
+
touch
|
102
119
|
check_certificate_expiration
|
103
120
|
|
104
121
|
tcp_socket = TCPSocket.new(@host, @port)
|
105
|
-
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE,
|
106
|
-
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY,
|
122
|
+
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
123
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
|
124
|
+
|
125
|
+
# Linux
|
126
|
+
if [:SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all? { |c| Socket.const_defined?(c) }
|
127
|
+
tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, KEEPALIVE_IDLE)
|
128
|
+
tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, KEEPALIVE_INTERVAL)
|
129
|
+
tcp_socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, KEEPALIVE_MAX_FAIL_PROBES)
|
130
|
+
end
|
131
|
+
|
132
|
+
# OSX
|
133
|
+
if RUBY_PLATFORM =~ /darwin/
|
134
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, OSX_TCP_KEEPALIVE, KEEPALIVE_IDLE)
|
135
|
+
end
|
136
|
+
|
107
137
|
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
108
138
|
ssl_socket.sync = true
|
109
139
|
ssl_socket.connect
|
110
140
|
[tcp_socket, ssl_socket]
|
111
|
-
rescue
|
141
|
+
rescue *TCP_ERRORS => error
|
112
142
|
if error.message =~ /certificate revoked/i
|
113
143
|
log_warn('Certificate has been revoked.')
|
114
144
|
reflect(:ssl_certificate_revoked, @app, error)
|
115
145
|
end
|
116
|
-
raise
|
146
|
+
raise TcpConnectionError, "#{error.class.name}, #{error.message}"
|
117
147
|
end
|
118
148
|
|
119
149
|
def check_certificate_expiration
|
@@ -128,7 +158,7 @@ module Rpush
|
|
128
158
|
end
|
129
159
|
|
130
160
|
def certificate_msg(msg)
|
131
|
-
time = @ssl_context.cert.not_after.utc.strftime(
|
161
|
+
time = @ssl_context.cert.not_after.utc.strftime('%Y-%m-%d %H:%M:%S UTC')
|
132
162
|
"Certificate #{msg} at #{time}."
|
133
163
|
end
|
134
164
|
|