rpush 2.2.0-java → 2.3.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +17 -9
  4. data/lib/generators/rpush_migration_generator.rb +1 -1
  5. data/lib/rpush.rb +4 -1
  6. data/lib/rpush/apns_feedback.rb +1 -1
  7. data/lib/rpush/cli.rb +24 -21
  8. data/lib/rpush/client/active_model/apns/app.rb +1 -1
  9. data/lib/rpush/client/active_model/apns/notification.rb +2 -2
  10. data/lib/rpush/client/active_model/wpns/notification.rb +7 -1
  11. data/lib/rpush/configuration.rb +32 -22
  12. data/lib/rpush/daemon.rb +18 -9
  13. data/lib/rpush/daemon/adm/delivery.rb +4 -1
  14. data/lib/rpush/daemon/apns/delivery.rb +3 -0
  15. data/lib/rpush/daemon/apns/feedback_receiver.rb +6 -2
  16. data/lib/rpush/daemon/app_runner.rb +11 -16
  17. data/lib/rpush/daemon/batch.rb +9 -0
  18. data/lib/rpush/daemon/delivery.rb +10 -2
  19. data/lib/rpush/daemon/delivery_error.rb +3 -3
  20. data/lib/rpush/daemon/dispatcher/apns_tcp.rb +11 -11
  21. data/lib/rpush/daemon/dispatcher/tcp.rb +2 -10
  22. data/lib/rpush/daemon/gcm/delivery.rb +13 -7
  23. data/lib/rpush/daemon/signal_handler.rb +5 -3
  24. data/lib/rpush/daemon/store/active_record.rb +14 -0
  25. data/lib/rpush/daemon/store/interface.rb +2 -1
  26. data/lib/rpush/daemon/store/redis.rb +3 -0
  27. data/lib/rpush/daemon/tcp_connection.rb +47 -17
  28. data/lib/rpush/daemon/wpns/delivery.rb +19 -10
  29. data/lib/rpush/logger.rb +30 -12
  30. data/lib/rpush/plugin.rb +44 -0
  31. data/lib/rpush/push.rb +1 -1
  32. data/lib/rpush/reflectable.rb +13 -0
  33. data/lib/rpush/{reflection.rb → reflection_collection.rb} +1 -9
  34. data/lib/rpush/reflection_public_methods.rb +9 -0
  35. data/lib/rpush/version.rb +1 -1
  36. data/lib/tasks/test.rake +6 -4
  37. data/spec/functional/adm_spec.rb +12 -0
  38. data/spec/functional/apns_spec.rb +61 -42
  39. data/spec/functional/gcm_spec.rb +9 -0
  40. data/spec/functional/retry_spec.rb +1 -1
  41. data/spec/functional/wpns_spec.rb +44 -11
  42. data/spec/spec_helper.rb +2 -0
  43. data/spec/unit/apns_feedback_spec.rb +2 -2
  44. data/spec/unit/client/active_record/apns/app_spec.rb +2 -2
  45. data/spec/unit/client/active_record/apns/notification_spec.rb +1 -2
  46. data/spec/unit/client/active_record/wpns/notification_spec.rb +9 -3
  47. data/spec/unit/configuration_spec.rb +1 -0
  48. data/spec/unit/daemon/adm/delivery_spec.rb +0 -1
  49. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +0 -1
  50. data/spec/unit/daemon/app_runner_spec.rb +14 -9
  51. data/spec/unit/daemon/delivery_spec.rb +0 -1
  52. data/spec/unit/daemon/dispatcher/tcp_spec.rb +0 -7
  53. data/spec/unit/daemon/gcm/delivery_spec.rb +1 -1
  54. data/spec/unit/daemon/signal_handler_spec.rb +5 -1
  55. data/spec/unit/daemon/store/active_record_spec.rb +1 -1
  56. data/spec/unit/daemon/tcp_connection_spec.rb +18 -18
  57. data/spec/unit/daemon/wpns/delivery_spec.rb +1 -1
  58. data/spec/unit/daemon_spec.rb +10 -2
  59. data/spec/unit/logger_spec.rb +0 -1
  60. data/spec/unit/plugin_spec.rb +36 -0
  61. data/spec/unit/push_spec.rb +2 -2
  62. data/spec/unit/{daemon/reflectable_spec.rb → reflectable_spec.rb} +6 -6
  63. data/spec/unit/{reflection_spec.rb → reflection_collection_spec.rb} +4 -8
  64. metadata +34 -36
  65. data/lib/rpush/daemon/reflectable.rb +0 -11
  66. data/spec/integration/rpush_spec.rb +0 -13
  67. data/spec/integration/support/gcm_success_response.json +0 -1
  68. 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("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
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
- log_info("APNs Feedback Receiver started.")
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("%Y-%m-%d %H:%M:%S UTC")
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
- @runners[app.id].start
34
- puts green('✔') if Rpush.config.foreground
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 start
100
+ def start_dispatchers
101
101
  app.connections.times { @dispatcher_loops.push(new_dispatcher_loop) }
102
- start_loops
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 = []
@@ -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("%Y-%m-%d %H:%M:%S")}.")
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
- other.code == code && \
23
- other.notification_id == notification_id && \
24
- other.to_s == to_s
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
- return unless connection.select(SELECT_TIMEOUT)
64
- rescue Errno::EBADF
65
- # Connection closed, daemon is shutting down.
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('Error received, reconnecting...')
82
- connection.reconnect
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('The APNs disconnected without returning an error. Marking all notifications delivered via this connection as failed.')
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 = ENV["RPUSH_GCM_HOST"] || "https://android.googleapis.com"
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("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
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.total_fail = failures.count == @registration_ids.count
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 :total_fail, :description
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 @total_fail
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 hndling signal '#{signal}'")
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.info("Received HUP signal.")
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("Received USR2 signal.")
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)
@@ -103,6 +103,9 @@ module Rpush
103
103
  def release_connection
104
104
  end
105
105
 
106
+ def reopen_log
107
+ end
108
+
106
109
  private
107
110
 
108
111
  def create_gcm_like_notification(notification, attrs, data, registration_ids, deliver_after, app) # rubocop:disable ParameterLists
@@ -6,7 +6,14 @@ module Rpush
6
6
  include Reflectable
7
7
  include Loggable
8
8
 
9
- attr_accessor :last_write
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
- written
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 Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError, IOError => e
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
- reconnect
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 - last_write > self.class.idle_period
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
- written
103
+ touch
88
104
  end
89
105
 
90
- def written
91
- self.last_write = Time.now
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, 1)
106
- tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
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 StandardError => error
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("%Y-%m-%d %H:%M:%S UTC")
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