rapns 0.2.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +35 -10
- data/lib/generators/rapns_generator.rb +9 -1
- data/lib/generators/templates/create_rapns_feedback.rb +15 -0
- data/lib/generators/templates/rapns.yml +22 -9
- data/lib/rapns/daemon/configuration.rb +32 -14
- data/lib/rapns/daemon/connection.rb +35 -50
- data/lib/rapns/daemon/delivery_handler.rb +65 -15
- data/lib/rapns/daemon/delivery_handler_pool.rb +1 -5
- data/lib/rapns/daemon/delivery_queue.rb +10 -27
- data/lib/rapns/daemon/feedback_receiver.rb +57 -0
- data/lib/rapns/daemon/feeder.rb +9 -7
- data/lib/rapns/daemon/interruptible_sleep.rb +14 -0
- data/lib/rapns/daemon/pool.rb +0 -5
- data/lib/rapns/daemon.rb +9 -9
- data/lib/rapns/device_token_format_validator.rb +10 -0
- data/lib/rapns/feedback.rb +10 -0
- data/lib/rapns/notification.rb +15 -1
- data/lib/rapns/version.rb +1 -1
- data/lib/rapns.rb +3 -1
- data/spec/rapns/daemon/certificate_spec.rb +6 -0
- data/spec/rapns/daemon/configuration_spec.rb +124 -40
- data/spec/rapns/daemon/connection_spec.rb +81 -129
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +1 -6
- data/spec/rapns/daemon/delivery_handler_spec.rb +117 -30
- data/spec/rapns/daemon/delivery_queue_spec.rb +29 -0
- data/spec/rapns/daemon/feedback_receiver_spec.rb +86 -0
- data/spec/rapns/daemon/feeder_spec.rb +25 -9
- data/spec/rapns/daemon/interruptible_sleep_spec.rb +32 -0
- data/spec/rapns/daemon/logger_spec.rb +34 -14
- data/spec/rapns/daemon_spec.rb +34 -31
- data/spec/rapns/feedback_spec.rb +12 -0
- data/spec/rapns/notification_spec.rb +5 -0
- data/spec/spec_helper.rb +5 -2
- metadata +16 -5
- data/lib/rapns/daemon/connection_pool.rb +0 -31
- data/spec/rapns/daemon/connection_pool_spec.rb +0 -40
data/README.md
CHANGED
@@ -4,11 +4,14 @@ Easy to use library for Apple's Push Notification Service with Rails 3.
|
|
4
4
|
|
5
5
|
## Features
|
6
6
|
|
7
|
-
* Works with Rails 3 and Ruby 1.9.
|
7
|
+
* Works with Rails 3 and Ruby 1.9 & 1.8.
|
8
8
|
* Uses a daemon process to keep open a persistent connection to the Push Notification Service, as recommended by Apple.
|
9
9
|
* Uses the [enhanced binary format](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4) (Figure 5-2) so that delivery errors can be reported.
|
10
|
+
* Records feedback from [The Feedback Service](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3).
|
10
11
|
* [Airbrake](http://airbrakeapp.com/) (Hoptoad) integration.
|
11
12
|
* Support for [dictionary `alert` properties](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) (Table 3-2).
|
13
|
+
* Reconnects to the APNs if connections are lost.
|
14
|
+
* Reconnects to your database if the connect is lost.
|
12
15
|
|
13
16
|
## Getting Started
|
14
17
|
|
@@ -43,13 +46,20 @@ If you want to use rapns in environments other than development or production, y
|
|
43
46
|
|
44
47
|
### Options:
|
45
48
|
|
46
|
-
* `
|
47
|
-
* `
|
49
|
+
* `push` this section contains options to configure the delivery of notifications.
|
50
|
+
* `host` the APNs host to connect to, either `gateway.push.apple.com` or `gateway.sandbox.push.apple.com`.
|
51
|
+
* `port` the APNs port. Currently `2195` for both hosts.
|
52
|
+
* `poll` (default: 2) Frequency in seconds to check for new notifications to deliver.
|
53
|
+
* `connections` (default: 3) the number of connections to keep open to the APNs. Consider increasing this if you are sending a very large number of notifications.
|
54
|
+
|
55
|
+
* `feedback` this section contains options to configure feedback checking.
|
56
|
+
* `host` the APNs host to connect to, either `feedback.push.apple.com` or `feedback.sandbox.push.apple.com`.
|
57
|
+
* `port` the APNs port. Currently `2196` for both hosts.
|
58
|
+
* `poll` (default: 60) Frequency in seconds to check for new feedback.
|
59
|
+
|
48
60
|
* `certificate` The path to your .pem certificate, `config/rapns` is automatically checked if a relative path is given.
|
49
61
|
* `certificate_password` (default: blank) the password you used when exporting your certificate, if any.
|
50
62
|
* `airbrake_notify` (default: true) Enables/disables error notifications via Airbrake.
|
51
|
-
* `poll` (default: 2) Frequency in seconds to check for new notifications to deliver.
|
52
|
-
* `connections` (default: 3) the number of connections to keep open to the APNs. Consider increasing this if you are sending a very large number of notifications.
|
53
63
|
* `pid_file` (default: blank) the file that rapns will write its process ID to. Paths are relative to your project's RAILS_ROOT unless an absolute path is given.
|
54
64
|
|
55
65
|
## Starting the rapns Daemon
|
@@ -86,11 +96,9 @@ rapns logs activity to `rapns.log` in your Rails log directory. This is also pri
|
|
86
96
|
|
87
97
|
Please refer to Apple's [documentation](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) (Tables 3-1 and 3-2).
|
88
98
|
|
89
|
-
Not yet implemented!
|
90
|
-
|
91
99
|
## Delivery Failures
|
92
100
|
|
93
|
-
The
|
101
|
+
The APNs provides two mechanism for delivery failure notification:
|
94
102
|
|
95
103
|
### Immediately, when processing a notification for delivery.
|
96
104
|
|
@@ -105,7 +113,15 @@ rapns will not attempt to deliver the notification again.
|
|
105
113
|
|
106
114
|
### Via the Feedback Service.
|
107
115
|
|
108
|
-
|
116
|
+
rapns checks for feedback periodically and stores results in the `Rapns::Feedback` model. Each record contains the device token and a timestamp of when the APNs determined that the app no longer exists on the device.
|
117
|
+
|
118
|
+
It is your responsibility to avoid creating new notifications for devices that no longer have your app installed. rapns does not and will not check `Rapns::Feedback` before sending notifications.
|
119
|
+
|
120
|
+
*Note: In my testing and from other reports on the Internet, it appears you may not receive feedback when using the APNs sandbox environment.*
|
121
|
+
|
122
|
+
## Updating rapns
|
123
|
+
|
124
|
+
After updating you should run `rails g rapns` to check for any new migrations or configuration changes.
|
109
125
|
|
110
126
|
## Contributing to rapns
|
111
127
|
|
@@ -113,4 +129,13 @@ Fork as usual and go crazy!
|
|
113
129
|
|
114
130
|
When running specs, please note that the ActiveRecord adapter can be changed by setting the `ADAPTER` environment variable. For example: `ADAPTER=postgresql rake`.
|
115
131
|
|
116
|
-
Available adapters for testing are `mysql`, `mysql2` and `postgresql`.
|
132
|
+
Available adapters for testing are `mysql`, `mysql2` and `postgresql`.
|
133
|
+
|
134
|
+
### Contributors
|
135
|
+
|
136
|
+
Thank you to the following wonderful people for contributing to rapns:
|
137
|
+
|
138
|
+
* [@blakewatters](https://github.com/blakewatters)
|
139
|
+
* [@forresty](https://github.com/forresty)
|
140
|
+
* [@sjmadsen](https://github.com/sjmadsen)
|
141
|
+
* [@ivanyv](https://github.com/ivanyv)
|
@@ -7,7 +7,15 @@ class RapnsGenerator < Rails::Generators::Base
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def copy_migration
|
10
|
-
|
10
|
+
migration_dir = File.expand_path("db/migrate")
|
11
|
+
|
12
|
+
if !self.class.migration_exists?(migration_dir, 'create_rapns_notifications')
|
13
|
+
migration_template "create_rapns_notifications.rb", "db/migrate/create_rapns_notifications.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
if !self.class.migration_exists?(migration_dir, 'create_rapns_feedback')
|
17
|
+
migration_template "create_rapns_feedback.rb", "db/migrate/create_rapns_feedback.rb"
|
18
|
+
end
|
11
19
|
end
|
12
20
|
|
13
21
|
def copy_config
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateRapnsFeedback < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :rapns_feedback do |t|
|
4
|
+
t.string :device_token, :null => false, :limit => 64
|
5
|
+
t.timestamp :failed_at, :null => false
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :rapns_feedback, :device_token
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.down
|
13
|
+
drop_table :rapns_feedback
|
14
|
+
end
|
15
|
+
end
|
@@ -1,18 +1,31 @@
|
|
1
1
|
development:
|
2
|
-
host: gateway.sandbox.push.apple.com
|
3
|
-
port: 2195
|
4
2
|
certificate: development.pem
|
5
3
|
certificate_password:
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
|
5
|
+
push:
|
6
|
+
host: gateway.sandbox.push.apple.com
|
7
|
+
port: 2195
|
8
|
+
poll: 2
|
9
|
+
connections: 3
|
10
|
+
|
11
|
+
feedback:
|
12
|
+
host: feedback.sandbox.push.apple.com
|
13
|
+
port: 2196
|
14
|
+
poll: 60
|
9
15
|
|
10
16
|
production:
|
11
|
-
host: gateway.push.apple.com
|
12
|
-
port: 2195
|
13
17
|
certificate: production.pem
|
14
18
|
certificate_password:
|
15
19
|
airbrake_notify: true
|
16
|
-
poll: 2
|
17
|
-
connections: 3
|
18
20
|
pid_file: tmp/pids/rapns.pid
|
21
|
+
|
22
|
+
push:
|
23
|
+
host: gateway.push.apple.com
|
24
|
+
port: 2195
|
25
|
+
poll: 2
|
26
|
+
connections: 3
|
27
|
+
|
28
|
+
feedback:
|
29
|
+
host: feedback.push.apple.com
|
30
|
+
port: 2196
|
31
|
+
poll: 60
|
@@ -5,26 +5,35 @@ module Rapns
|
|
5
5
|
|
6
6
|
module Daemon
|
7
7
|
class Configuration
|
8
|
-
attr_accessor :
|
8
|
+
attr_accessor :push, :feedback
|
9
|
+
attr_accessor :certificate, :certificate_password, :airbrake_notify, :pid_file
|
9
10
|
alias_method :airbrake_notify?, :airbrake_notify
|
10
11
|
|
11
12
|
def initialize(environment, config_path)
|
12
13
|
@environment = environment
|
13
14
|
@config_path = config_path
|
15
|
+
|
16
|
+
self.push = Struct.new(:host, :port, :connections, :poll).new
|
17
|
+
self.feedback = Struct.new(:host, :port, :poll).new
|
14
18
|
end
|
15
19
|
|
16
20
|
def load
|
17
21
|
config = read_config
|
18
22
|
ensure_environment_configured(config)
|
19
23
|
config = config[@environment]
|
20
|
-
set_variable(:host, config)
|
21
|
-
set_variable(:port, config)
|
22
|
-
set_variable(:
|
23
|
-
set_variable(:
|
24
|
-
|
25
|
-
set_variable(:
|
26
|
-
set_variable(:
|
27
|
-
set_variable(:
|
24
|
+
set_variable(:push, :host, config)
|
25
|
+
set_variable(:push, :port, config)
|
26
|
+
set_variable(:push, :poll, config, :optional => true, :default => 2)
|
27
|
+
set_variable(:push, :connections, config, :optional => true, :default => 3)
|
28
|
+
|
29
|
+
set_variable(:feedback, :host, config)
|
30
|
+
set_variable(:feedback, :port, config)
|
31
|
+
set_variable(:feedback, :poll, config, :optional => true, :default => 60)
|
32
|
+
|
33
|
+
set_variable(nil, :certificate, config)
|
34
|
+
set_variable(nil, :airbrake_notify, config, :optional => true, :default => true)
|
35
|
+
set_variable(nil, :certificate_password, config, :optional => true, :default => "")
|
36
|
+
set_variable(nil, :pid_file, config, :optional => true, :default => "")
|
28
37
|
end
|
29
38
|
|
30
39
|
def certificate
|
@@ -52,15 +61,24 @@ module Rapns
|
|
52
61
|
File.open(@config_path) { |fd| YAML.load(fd) }
|
53
62
|
end
|
54
63
|
|
55
|
-
def set_variable(key, config, options = {})
|
56
|
-
if
|
64
|
+
def set_variable(base_key, key, config, options = {})
|
65
|
+
if base_key
|
66
|
+
base = send(base_key)
|
67
|
+
value = config.key?(base_key.to_s) ? config[base_key.to_s][key.to_s] : nil
|
68
|
+
else
|
69
|
+
base = self
|
70
|
+
value = config[key.to_s]
|
71
|
+
end
|
72
|
+
|
73
|
+
if value.to_s.strip == ""
|
57
74
|
if options[:optional]
|
58
|
-
|
75
|
+
base.send("#{key}=", options[:default])
|
59
76
|
else
|
60
|
-
|
77
|
+
key_path = base_key ? "#{base_key}.#{key}" : key
|
78
|
+
raise Rapns::ConfigurationError, "'#{key_path}' not defined for environment '#{@environment}' in #{@config_path}. You may need to run 'rails g rapns' after updating."
|
61
79
|
end
|
62
80
|
else
|
63
|
-
|
81
|
+
base.send("#{key}=", value)
|
64
82
|
end
|
65
83
|
end
|
66
84
|
|
@@ -3,22 +3,10 @@ module Rapns
|
|
3
3
|
class ConnectionError < StandardError; end
|
4
4
|
|
5
5
|
class Connection
|
6
|
-
|
7
|
-
ERROR_PACKET_BYTES = 6
|
8
|
-
APN_ERRORS = {
|
9
|
-
1 => "Processing error",
|
10
|
-
2 => "Missing device token",
|
11
|
-
3 => "Missing topic",
|
12
|
-
4 => "Missing payload",
|
13
|
-
5 => "Missing token size",
|
14
|
-
6 => "Missing topic size",
|
15
|
-
7 => "Missing payload size",
|
16
|
-
8 => "Invalid token",
|
17
|
-
255 => "None (unknown error)"
|
18
|
-
}
|
19
|
-
|
20
|
-
def initialize(name)
|
6
|
+
def initialize(name, host, port)
|
21
7
|
@name = name
|
8
|
+
@host = host
|
9
|
+
@port = port
|
22
10
|
end
|
23
11
|
|
24
12
|
def connect
|
@@ -27,56 +15,53 @@ module Rapns
|
|
27
15
|
end
|
28
16
|
|
29
17
|
def close
|
30
|
-
|
31
|
-
|
18
|
+
begin
|
19
|
+
@ssl_socket.close if @ssl_socket
|
20
|
+
@tcp_socket.close if @tcp_socket
|
21
|
+
rescue IOError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(num_bytes)
|
26
|
+
@ssl_socket.read(num_bytes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def select(timeout)
|
30
|
+
IO.select([@ssl_socket], nil, nil, timeout)
|
32
31
|
end
|
33
32
|
|
34
33
|
def write(data)
|
35
34
|
retry_count = 0
|
36
35
|
|
37
36
|
begin
|
38
|
-
|
39
|
-
|
37
|
+
write_data(data)
|
38
|
+
rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
|
39
|
+
retry_count += 1;
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@tcp_socket, @ssl_socket = connect_socket
|
45
|
-
|
46
|
-
retry_count += 1
|
41
|
+
if retry_count == 1
|
42
|
+
Rapns::Daemon.logger.error("[#{@name}] Lost connection to #{@host}:#{@port} (#{e.class.name}), reconnecting...")
|
43
|
+
end
|
47
44
|
|
48
|
-
if retry_count
|
45
|
+
if retry_count <= 3
|
46
|
+
reconnect
|
49
47
|
sleep 1
|
50
48
|
retry
|
51
49
|
else
|
52
|
-
raise ConnectionError, "#{@name} tried #{retry_count} times to reconnect but failed
|
50
|
+
raise ConnectionError, "#{@name} tried #{retry_count-1} times to reconnect but failed (#{e.class.name})."
|
53
51
|
end
|
54
52
|
end
|
55
53
|
end
|
56
54
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
delivery_error = nil
|
62
|
-
|
63
|
-
if error = @ssl_socket.read(ERROR_PACKET_BYTES)
|
64
|
-
cmd, status, notification_id = error.unpack("ccN")
|
55
|
+
def reconnect
|
56
|
+
close
|
57
|
+
@tcp_socket, @ssl_socket = connect_socket
|
58
|
+
end
|
65
59
|
|
66
|
-
|
67
|
-
description = APN_ERRORS[status] || "Unknown error. Possible rapns bug?"
|
68
|
-
delivery_error = Rapns::DeliveryError.new(status, description, notification_id)
|
69
|
-
end
|
70
|
-
end
|
60
|
+
protected
|
71
61
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
@tcp_socket, @ssl_socket = connect_socket
|
76
|
-
ensure
|
77
|
-
raise delivery_error if delivery_error
|
78
|
-
end
|
79
|
-
end
|
62
|
+
def write_data(data)
|
63
|
+
@ssl_socket.write(data)
|
64
|
+
@ssl_socket.flush
|
80
65
|
end
|
81
66
|
|
82
67
|
def setup_ssl_context
|
@@ -87,13 +72,13 @@ module Rapns
|
|
87
72
|
end
|
88
73
|
|
89
74
|
def connect_socket
|
90
|
-
tcp_socket = TCPSocket.new(
|
75
|
+
tcp_socket = TCPSocket.new(@host, @port)
|
91
76
|
tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
|
92
77
|
tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
93
78
|
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
94
79
|
ssl_socket.sync = true
|
95
80
|
ssl_socket.connect
|
96
|
-
Rapns::Daemon.logger.info("[#{@name}] Connected to #{
|
81
|
+
Rapns::Daemon.logger.info("[#{@name}] Connected to #{@host}:#{@port}")
|
97
82
|
[tcp_socket, ssl_socket]
|
98
83
|
end
|
99
84
|
end
|
@@ -2,9 +2,33 @@ module Rapns
|
|
2
2
|
module Daemon
|
3
3
|
class DeliveryHandler
|
4
4
|
STOP = 0x666
|
5
|
+
ERROR_CMD = 8
|
6
|
+
OK_STATUS = 0
|
7
|
+
SELECT_TIMEOUT = 0.5
|
8
|
+
ERROR_TUPLE_BYTES = 6
|
9
|
+
APN_ERRORS = {
|
10
|
+
1 => "Processing error",
|
11
|
+
2 => "Missing device token",
|
12
|
+
3 => "Missing topic",
|
13
|
+
4 => "Missing payload",
|
14
|
+
5 => "Missing token size",
|
15
|
+
6 => "Missing topic size",
|
16
|
+
7 => "Missing payload size",
|
17
|
+
8 => "Invalid token",
|
18
|
+
255 => "None (unknown error)"
|
19
|
+
}
|
20
|
+
|
21
|
+
def initialize(i)
|
22
|
+
@name = "DeliveryHandler #{i}"
|
23
|
+
host = Rapns::Daemon.configuration.push.host
|
24
|
+
port = Rapns::Daemon.configuration.push.port
|
25
|
+
@connection = Connection.new(@name, host, port)
|
26
|
+
end
|
5
27
|
|
6
28
|
def start
|
7
|
-
|
29
|
+
@connection.connect
|
30
|
+
|
31
|
+
Thread.new do
|
8
32
|
loop do
|
9
33
|
break if @stop
|
10
34
|
handle_next_notification
|
@@ -14,29 +38,28 @@ module Rapns
|
|
14
38
|
|
15
39
|
def stop
|
16
40
|
@stop = true
|
41
|
+
Rapns::Daemon.delivery_queue.push(STOP)
|
17
42
|
end
|
18
43
|
|
19
44
|
protected
|
20
45
|
|
21
46
|
def deliver(notification)
|
22
|
-
|
23
|
-
|
24
|
-
|
47
|
+
begin
|
48
|
+
@connection.write(notification.to_binary)
|
49
|
+
check_for_error
|
25
50
|
|
26
|
-
|
27
|
-
|
28
|
-
|
51
|
+
notification.delivered = true
|
52
|
+
notification.delivered_at = Time.now
|
53
|
+
notification.save!(:validate => false)
|
29
54
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
55
|
+
Rapns::Daemon.logger.info("Notification #{notification.id} delivered to #{notification.device_token}")
|
56
|
+
rescue Rapns::DeliveryError => error
|
57
|
+
handle_delivery_error(notification, error)
|
58
|
+
raise
|
34
59
|
end
|
35
60
|
end
|
36
61
|
|
37
62
|
def handle_delivery_error(notification, error)
|
38
|
-
Rapns::Daemon.logger.error(error)
|
39
|
-
|
40
63
|
notification.delivered = false
|
41
64
|
notification.delivered_at = nil
|
42
65
|
notification.failed = true
|
@@ -46,15 +69,42 @@ module Rapns
|
|
46
69
|
notification.save!(:validate => false)
|
47
70
|
end
|
48
71
|
|
72
|
+
def check_for_error
|
73
|
+
if @connection.select(SELECT_TIMEOUT)
|
74
|
+
delivery_error = nil
|
75
|
+
|
76
|
+
if error = @connection.read(ERROR_TUPLE_BYTES)
|
77
|
+
cmd, status, notification_id = error.unpack("ccN")
|
78
|
+
|
79
|
+
if cmd == ERROR_CMD && status != OK_STATUS
|
80
|
+
description = APN_ERRORS[status] || "Unknown error. Possible rapns bug?"
|
81
|
+
delivery_error = Rapns::DeliveryError.new(status, description, notification_id)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
begin
|
86
|
+
Rapns::Daemon.logger.error("[#{@name}] Error received, reconnecting...")
|
87
|
+
@connection.reconnect
|
88
|
+
ensure
|
89
|
+
raise delivery_error if delivery_error
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
49
94
|
def handle_next_notification
|
50
95
|
notification = Rapns::Daemon.delivery_queue.pop
|
96
|
+
|
97
|
+
if notification == STOP
|
98
|
+
@connection.close
|
99
|
+
return
|
100
|
+
end
|
101
|
+
|
51
102
|
begin
|
52
|
-
return if notification == STOP
|
53
103
|
deliver(notification)
|
54
104
|
rescue StandardError => e
|
55
105
|
Rapns::Daemon.logger.error(e)
|
56
106
|
ensure
|
57
|
-
Rapns::Daemon.delivery_queue.
|
107
|
+
Rapns::Daemon.delivery_queue.notification_processed
|
58
108
|
end
|
59
109
|
end
|
60
110
|
end
|
@@ -5,7 +5,7 @@ module Rapns
|
|
5
5
|
protected
|
6
6
|
|
7
7
|
def new_object_for_pool(i)
|
8
|
-
DeliveryHandler.new
|
8
|
+
DeliveryHandler.new(i)
|
9
9
|
end
|
10
10
|
|
11
11
|
def object_added_to_pool(object)
|
@@ -15,10 +15,6 @@ module Rapns
|
|
15
15
|
def object_removed_from_pool(object)
|
16
16
|
object.stop
|
17
17
|
end
|
18
|
-
|
19
|
-
def drain_started
|
20
|
-
@num_objects.times { Rapns::Daemon.delivery_queue.push(Rapns::Daemon::DeliveryHandler::STOP) }
|
21
|
-
end
|
22
18
|
end
|
23
19
|
end
|
24
20
|
end
|
@@ -1,44 +1,27 @@
|
|
1
1
|
module Rapns
|
2
2
|
module Daemon
|
3
3
|
class DeliveryQueue
|
4
|
-
def initialize
|
5
|
-
@
|
4
|
+
def initialize
|
5
|
+
@mutex = Mutex.new
|
6
|
+
@num_notifications = 0
|
6
7
|
@queue = Queue.new
|
7
|
-
@feeder = nil
|
8
|
-
@handler_mutex = Mutex.new
|
9
8
|
end
|
10
9
|
|
11
|
-
def push(
|
12
|
-
@
|
10
|
+
def push(notification)
|
11
|
+
@mutex.synchronize { @num_notifications += 1 }
|
12
|
+
@queue.push(notification)
|
13
13
|
end
|
14
14
|
|
15
15
|
def pop
|
16
16
|
@queue.pop
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
@
|
21
|
-
signal_feeder if handler_available?
|
22
|
-
end
|
19
|
+
def notification_processed
|
20
|
+
@mutex.synchronize { @num_notifications -= 1 }
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
@
|
27
|
-
end
|
28
|
-
|
29
|
-
def signal_feeder
|
30
|
-
begin
|
31
|
-
@feeder.wakeup if @feeder
|
32
|
-
rescue ThreadError
|
33
|
-
retry
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def wait_for_available_handler
|
38
|
-
if !handler_available?
|
39
|
-
@feeder = Thread.current
|
40
|
-
@feeder.stop
|
41
|
-
end
|
23
|
+
def notifications_processed?
|
24
|
+
@mutex.synchronize { @num_notifications == 0 }
|
42
25
|
end
|
43
26
|
end
|
44
27
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class FeedbackReceiver
|
4
|
+
extend InterruptibleSleep
|
5
|
+
|
6
|
+
FEEDBACK_TUPLE_BYTES = 38
|
7
|
+
|
8
|
+
def self.start
|
9
|
+
@thread = Thread.new do
|
10
|
+
loop do
|
11
|
+
break if @stop
|
12
|
+
check_for_feedback
|
13
|
+
interruptible_sleep Rapns::Daemon.configuration.feedback.poll
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.stop
|
19
|
+
@stop = true
|
20
|
+
interrupt_sleep
|
21
|
+
@thread.join if @thread
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.check_for_feedback
|
25
|
+
connection = nil
|
26
|
+
begin
|
27
|
+
host = Rapns::Daemon.configuration.feedback.host
|
28
|
+
port = Rapns::Daemon.configuration.feedback.port
|
29
|
+
connection = Connection.new("FeedbackReceiver", host, port)
|
30
|
+
connection.connect
|
31
|
+
|
32
|
+
while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
|
33
|
+
timestamp, device_token = parse_tuple(tuple)
|
34
|
+
create_feedback(timestamp, device_token)
|
35
|
+
end
|
36
|
+
rescue StandardError => e
|
37
|
+
Rapns::Daemon.logger.error(e)
|
38
|
+
ensure
|
39
|
+
connection.close if connection
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def self.parse_tuple(tuple)
|
46
|
+
failed_at, _, device_token = tuple.unpack("N1n1H*")
|
47
|
+
[Time.at(failed_at), device_token]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.create_feedback(failed_at, device_token)
|
51
|
+
formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
52
|
+
Rapns::Daemon.logger.info("[FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}")
|
53
|
+
Rapns::Feedback.create!(:failed_at => failed_at, :device_token => device_token)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/rapns/daemon/feeder.rb
CHANGED
@@ -7,36 +7,38 @@ ADAPTER_ERRORS = [PGError, Mysql::Error, Mysql2::Error]
|
|
7
7
|
module Rapns
|
8
8
|
module Daemon
|
9
9
|
class Feeder
|
10
|
+
extend InterruptibleSleep
|
11
|
+
|
10
12
|
def self.start(foreground)
|
11
13
|
connect unless foreground
|
12
14
|
|
13
15
|
loop do
|
14
16
|
break if @stop
|
15
17
|
enqueue_notifications
|
18
|
+
interruptible_sleep Rapns::Daemon.configuration.push.poll
|
16
19
|
end
|
17
20
|
end
|
18
21
|
|
19
22
|
def self.stop
|
20
23
|
@stop = true
|
24
|
+
interrupt_sleep
|
21
25
|
end
|
22
26
|
|
23
27
|
protected
|
24
28
|
|
25
29
|
def self.enqueue_notifications
|
26
30
|
begin
|
27
|
-
Rapns::
|
28
|
-
Rapns::
|
31
|
+
if Rapns::Daemon.delivery_queue.notifications_processed?
|
32
|
+
Rapns::Notification.ready_for_delivery.each do |notification|
|
33
|
+
Rapns::Daemon.delivery_queue.push(notification)
|
34
|
+
end
|
29
35
|
end
|
30
|
-
|
31
|
-
Rapns::Daemon.delivery_queue.wait_for_available_handler
|
32
36
|
rescue ActiveRecord::StatementInvalid, *ADAPTER_ERRORS => e
|
33
37
|
Rapns::Daemon.logger.error(e)
|
34
38
|
reconnect
|
35
39
|
rescue StandardError => e
|
36
40
|
Rapns::Daemon.logger.error(e)
|
37
41
|
end
|
38
|
-
|
39
|
-
sleep Rapns::Daemon.configuration.poll
|
40
42
|
end
|
41
43
|
|
42
44
|
def self.reconnect
|
@@ -62,7 +64,7 @@ module Rapns
|
|
62
64
|
end
|
63
65
|
|
64
66
|
def self.check_is_connected
|
65
|
-
#
|
67
|
+
# Simply asking the adapter for the connection state is not sufficient.
|
66
68
|
Rapns::Notification.count
|
67
69
|
end
|
68
70
|
|