rapns 0.2.3 → 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.
- 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
|
|