rpush 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +99 -0
- data/LICENSE +7 -0
- data/README.md +189 -0
- data/bin/rpush +36 -0
- data/config/database.yml +44 -0
- data/lib/generators/rpush_generator.rb +44 -0
- data/lib/generators/templates/add_adm.rb +23 -0
- data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/add_fail_after_to_rpush_notifications.rb +9 -0
- data/lib/generators/templates/add_gcm.rb +102 -0
- data/lib/generators/templates/add_rpush.rb +349 -0
- data/lib/generators/templates/add_wpns.rb +16 -0
- data/lib/generators/templates/create_rapns_apps.rb +16 -0
- data/lib/generators/templates/create_rapns_feedback.rb +18 -0
- data/lib/generators/templates/create_rapns_notifications.rb +29 -0
- data/lib/generators/templates/rename_rapns_to_rpush.rb +63 -0
- data/lib/generators/templates/rpush.rb +104 -0
- data/lib/rpush.rb +62 -0
- data/lib/rpush/TODO +3 -0
- data/lib/rpush/adm/app.rb +15 -0
- data/lib/rpush/adm/data_validator.rb +11 -0
- data/lib/rpush/adm/notification.rb +29 -0
- data/lib/rpush/apns/app.rb +29 -0
- data/lib/rpush/apns/binary_notification_validator.rb +12 -0
- data/lib/rpush/apns/device_token_format_validator.rb +12 -0
- data/lib/rpush/apns/feedback.rb +16 -0
- data/lib/rpush/apns/notification.rb +84 -0
- data/lib/rpush/apns_feedback.rb +13 -0
- data/lib/rpush/app.rb +18 -0
- data/lib/rpush/configuration.rb +75 -0
- data/lib/rpush/daemon.rb +140 -0
- data/lib/rpush/daemon/adm.rb +9 -0
- data/lib/rpush/daemon/adm/delivery.rb +222 -0
- data/lib/rpush/daemon/apns.rb +16 -0
- data/lib/rpush/daemon/apns/certificate_expired_error.rb +20 -0
- data/lib/rpush/daemon/apns/delivery.rb +64 -0
- data/lib/rpush/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rpush/daemon/apns/feedback_receiver.rb +79 -0
- data/lib/rpush/daemon/app_runner.rb +187 -0
- data/lib/rpush/daemon/batch.rb +115 -0
- data/lib/rpush/daemon/constants.rb +59 -0
- data/lib/rpush/daemon/delivery.rb +28 -0
- data/lib/rpush/daemon/delivery_error.rb +19 -0
- data/lib/rpush/daemon/dispatcher/http.rb +21 -0
- data/lib/rpush/daemon/dispatcher/tcp.rb +30 -0
- data/lib/rpush/daemon/dispatcher_loop.rb +54 -0
- data/lib/rpush/daemon/dispatcher_loop_collection.rb +33 -0
- data/lib/rpush/daemon/feeder.rb +68 -0
- data/lib/rpush/daemon/gcm.rb +9 -0
- data/lib/rpush/daemon/gcm/delivery.rb +222 -0
- data/lib/rpush/daemon/interruptible_sleep.rb +61 -0
- data/lib/rpush/daemon/loggable.rb +31 -0
- data/lib/rpush/daemon/reflectable.rb +13 -0
- data/lib/rpush/daemon/retry_header_parser.rb +23 -0
- data/lib/rpush/daemon/retryable_error.rb +20 -0
- data/lib/rpush/daemon/service_config_methods.rb +33 -0
- data/lib/rpush/daemon/store/active_record.rb +154 -0
- data/lib/rpush/daemon/store/active_record/reconnectable.rb +68 -0
- data/lib/rpush/daemon/tcp_connection.rb +143 -0
- data/lib/rpush/daemon/too_many_requests_error.rb +20 -0
- data/lib/rpush/daemon/wpns.rb +9 -0
- data/lib/rpush/daemon/wpns/delivery.rb +132 -0
- data/lib/rpush/deprecatable.rb +23 -0
- data/lib/rpush/deprecation.rb +23 -0
- data/lib/rpush/embed.rb +28 -0
- data/lib/rpush/gcm/app.rb +11 -0
- data/lib/rpush/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rpush/gcm/notification.rb +30 -0
- data/lib/rpush/logger.rb +63 -0
- data/lib/rpush/multi_json_helper.rb +16 -0
- data/lib/rpush/notification.rb +69 -0
- data/lib/rpush/notifier.rb +52 -0
- data/lib/rpush/payload_data_size_validator.rb +10 -0
- data/lib/rpush/push.rb +16 -0
- data/lib/rpush/railtie.rb +11 -0
- data/lib/rpush/reflection.rb +58 -0
- data/lib/rpush/registration_ids_count_validator.rb +10 -0
- data/lib/rpush/version.rb +3 -0
- data/lib/rpush/wpns/app.rb +9 -0
- data/lib/rpush/wpns/notification.rb +26 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/rpush.rake +16 -0
- data/lib/tasks/test.rake +38 -0
- data/spec/functional/adm_spec.rb +43 -0
- data/spec/functional/apns_spec.rb +58 -0
- data/spec/functional/embed_spec.rb +49 -0
- data/spec/functional/gcm_spec.rb +42 -0
- data/spec/functional/wpns_spec.rb +41 -0
- data/spec/support/cert_with_password.pem +90 -0
- data/spec/support/cert_without_password.pem +59 -0
- data/spec/support/install.sh +32 -0
- data/spec/support/simplecov_helper.rb +20 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/tmp/.gitkeep +0 -0
- data/spec/unit/adm/app_spec.rb +58 -0
- data/spec/unit/adm/notification_spec.rb +45 -0
- data/spec/unit/apns/app_spec.rb +29 -0
- data/spec/unit/apns/feedback_spec.rb +9 -0
- data/spec/unit/apns/notification_spec.rb +208 -0
- data/spec/unit/apns_feedback_spec.rb +21 -0
- data/spec/unit/app_spec.rb +30 -0
- data/spec/unit/configuration_spec.rb +45 -0
- data/spec/unit/daemon/adm/delivery_spec.rb +243 -0
- data/spec/unit/daemon/apns/certificate_expired_error_spec.rb +11 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +101 -0
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +117 -0
- data/spec/unit/daemon/app_runner_spec.rb +292 -0
- data/spec/unit/daemon/batch_spec.rb +232 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_spec.rb +38 -0
- data/spec/unit/daemon/dispatcher/http_spec.rb +33 -0
- data/spec/unit/daemon/dispatcher/tcp_spec.rb +38 -0
- data/spec/unit/daemon/dispatcher_loop_collection_spec.rb +37 -0
- data/spec/unit/daemon/dispatcher_loop_spec.rb +71 -0
- data/spec/unit/daemon/feeder_spec.rb +98 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +310 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +68 -0
- data/spec/unit/daemon/reflectable_spec.rb +27 -0
- data/spec/unit/daemon/retryable_error_spec.rb +14 -0
- data/spec/unit/daemon/service_config_methods_spec.rb +33 -0
- data/spec/unit/daemon/store/active_record/reconnectable_spec.rb +114 -0
- data/spec/unit/daemon/store/active_record_spec.rb +357 -0
- data/spec/unit/daemon/tcp_connection_spec.rb +287 -0
- data/spec/unit/daemon/too_many_requests_error_spec.rb +14 -0
- data/spec/unit/daemon/wpns/delivery_spec.rb +159 -0
- data/spec/unit/daemon_spec.rb +159 -0
- data/spec/unit/deprecatable_spec.rb +32 -0
- data/spec/unit/deprecation_spec.rb +15 -0
- data/spec/unit/embed_spec.rb +50 -0
- data/spec/unit/gcm/app_spec.rb +4 -0
- data/spec/unit/gcm/notification_spec.rb +36 -0
- data/spec/unit/logger_spec.rb +127 -0
- data/spec/unit/notification_shared.rb +105 -0
- data/spec/unit/notification_spec.rb +15 -0
- data/spec/unit/notifier_spec.rb +49 -0
- data/spec/unit/push_spec.rb +43 -0
- data/spec/unit/reflection_spec.rb +30 -0
- data/spec/unit/rpush_spec.rb +9 -0
- data/spec/unit/wpns/app_spec.rb +4 -0
- data/spec/unit/wpns/notification_spec.rb +30 -0
- data/spec/unit_spec_helper.rb +101 -0
- metadata +276 -0
|
@@ -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,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,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
|
|
@@ -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,143 @@
|
|
|
1
|
+
module Rpush
|
|
2
|
+
module Daemon
|
|
3
|
+
class TcpConnectionError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class TcpConnection
|
|
6
|
+
include Reflectable
|
|
7
|
+
include Loggable
|
|
8
|
+
|
|
9
|
+
attr_accessor :last_write
|
|
10
|
+
|
|
11
|
+
def self.idle_period
|
|
12
|
+
30.minutes
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(app, host, port)
|
|
16
|
+
@app = app
|
|
17
|
+
@host = host
|
|
18
|
+
@port = port
|
|
19
|
+
@certificate = app.certificate
|
|
20
|
+
@password = app.password
|
|
21
|
+
written
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def connect
|
|
25
|
+
@ssl_context = setup_ssl_context
|
|
26
|
+
@tcp_socket, @ssl_socket = connect_socket
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def close
|
|
30
|
+
begin
|
|
31
|
+
@ssl_socket.close if @ssl_socket
|
|
32
|
+
@tcp_socket.close if @tcp_socket
|
|
33
|
+
rescue IOError
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read(num_bytes)
|
|
38
|
+
@ssl_socket.read(num_bytes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def select(timeout)
|
|
42
|
+
IO.select([@ssl_socket], nil, nil, timeout)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write(data)
|
|
46
|
+
reconnect_idle if idle_period_exceeded?
|
|
47
|
+
|
|
48
|
+
retry_count = 0
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
write_data(data)
|
|
52
|
+
rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError, IOError => e
|
|
53
|
+
retry_count += 1;
|
|
54
|
+
|
|
55
|
+
if retry_count == 1
|
|
56
|
+
log_error("Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
|
|
57
|
+
reflect(:apns_connection_lost, @app, e) # deprecated
|
|
58
|
+
reflect(:tcp_connection_lost, @app, e)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if retry_count <= 3
|
|
62
|
+
reconnect
|
|
63
|
+
sleep 1
|
|
64
|
+
retry
|
|
65
|
+
else
|
|
66
|
+
raise TcpConnectionError, "#{@app.name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reconnect
|
|
72
|
+
close
|
|
73
|
+
@tcp_socket, @ssl_socket = connect_socket
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
protected
|
|
77
|
+
|
|
78
|
+
def reconnect_idle
|
|
79
|
+
log_info("Idle period exceeded, reconnecting...")
|
|
80
|
+
reconnect
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def idle_period_exceeded?
|
|
84
|
+
Time.now - last_write > self.class.idle_period
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def write_data(data)
|
|
88
|
+
@ssl_socket.write(data)
|
|
89
|
+
@ssl_socket.flush
|
|
90
|
+
written
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def written
|
|
94
|
+
self.last_write = Time.now
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def setup_ssl_context
|
|
98
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
99
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
|
|
100
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
|
|
101
|
+
ssl_context
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def connect_socket
|
|
105
|
+
check_certificate_expiration
|
|
106
|
+
|
|
107
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
|
108
|
+
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
|
109
|
+
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
110
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
|
111
|
+
ssl_socket.sync = true
|
|
112
|
+
ssl_socket.connect
|
|
113
|
+
log_info("Connected to #{@host}:#{@port}")
|
|
114
|
+
[tcp_socket, ssl_socket]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def check_certificate_expiration
|
|
118
|
+
cert = @ssl_context.cert
|
|
119
|
+
if certificate_expired?
|
|
120
|
+
log_error(certificate_msg('expired'))
|
|
121
|
+
raise Rpush::Apns::CertificateExpiredError.new(@app, cert.not_after)
|
|
122
|
+
elsif certificate_expires_soon?
|
|
123
|
+
log_warn(certificate_msg('will expire'))
|
|
124
|
+
reflect(:apns_certificate_will_expire, @app, cert.not_after) # deprecated
|
|
125
|
+
reflect(:ssl_certificate_will_expire, @app, cert.not_after)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def certificate_msg(msg)
|
|
130
|
+
time = @ssl_context.cert.not_after.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
131
|
+
"Certificate #{msg} at #{time}."
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def certificate_expired?
|
|
135
|
+
@ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < Time.now.utc
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def certificate_expires_soon?
|
|
139
|
+
@ssl_context.cert.not_after && @ssl_context.cert.not_after.utc < (Time.now + 1.month).utc
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|