rapns 1.0.7 → 2.0.0rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +7 -0
- data/LICENSE +7 -0
- data/README.md +58 -41
- data/bin/rapns +23 -5
- data/lib/generators/rapns_generator.rb +2 -4
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/create_rapns_apps.rb +15 -0
- data/lib/rapns/app.rb +9 -0
- data/lib/rapns/daemon/app_runner.rb +131 -0
- data/lib/rapns/daemon/connection.rb +5 -3
- data/lib/rapns/daemon/delivery_handler.rb +13 -15
- data/lib/rapns/daemon/delivery_handler_pool.rb +8 -10
- data/lib/rapns/daemon/delivery_queue.rb +36 -4
- data/lib/rapns/daemon/feedback_receiver.rb +19 -12
- data/lib/rapns/daemon/feeder.rb +8 -10
- data/lib/rapns/daemon/logger.rb +5 -3
- data/lib/rapns/daemon.rb +52 -38
- data/lib/rapns/notification.rb +16 -5
- data/lib/rapns/patches.rb +2 -2
- data/lib/rapns/version.rb +1 -1
- data/lib/rapns.rb +2 -1
- data/spec/rapns/daemon/app_runner_spec.rb +207 -0
- data/spec/rapns/daemon/connection_spec.rb +177 -236
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +10 -14
- data/spec/rapns/daemon/delivery_handler_spec.rb +92 -79
- data/spec/rapns/daemon/feedback_receiver_spec.rb +29 -23
- data/spec/rapns/daemon/feeder_spec.rb +40 -44
- data/spec/rapns/daemon/logger_spec.rb +21 -3
- data/spec/rapns/daemon_spec.rb +65 -125
- data/spec/rapns/notification_spec.rb +16 -0
- data/spec/spec_helper.rb +4 -1
- metadata +14 -15
- data/History.md +0 -5
- data/lib/generators/templates/rapns.yml +0 -31
- data/lib/rapns/daemon/certificate.rb +0 -27
- data/lib/rapns/daemon/configuration.rb +0 -98
- data/lib/rapns/daemon/pool.rb +0 -36
- data/spec/rapns/daemon/certificate_spec.rb +0 -22
- data/spec/rapns/daemon/configuration_spec.rb +0 -231
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012 Ian Leitch
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,17 +1,23 @@
|
|
1
|
-
|
1
|
+
[![Build Status](https://secure.travis-ci.org/ileitch/rapns.png)](http://travis-ci.org/ileitch/rapns)
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
## Features
|
3
|
+
# Features
|
6
4
|
|
7
5
|
* Works with Rails 3 and Ruby 1.9 & 1.8.
|
8
|
-
*
|
9
|
-
*
|
10
|
-
*
|
6
|
+
* Supports multiple iOS apps.
|
7
|
+
* [Add & remove apps](#hot-app-updates) without restarting or affecting the delivery of notifications to other apps.
|
8
|
+
* Uses a daemon process to keep open persistent connections to the APNs, as recommended by Apple.
|
9
|
+
* Uses the enhanced binary format so that [delivery errors can be reported](#delivery-failures).
|
10
|
+
* Records feedback from [The Feedback Service](#delivery-failures).
|
11
11
|
* [Airbrake](http://airbrakeapp.com/) (Hoptoad) integration.
|
12
|
-
* Support for [dictionary `alert` properties](
|
13
|
-
*
|
14
|
-
* Reconnects to your database if
|
12
|
+
* Support for [dictionary `alert` properties](#assigning-a-hash-to-alert).
|
13
|
+
* [Mobile Device Management (MDM)](#mobile-device-management)
|
14
|
+
* Stable. Reconnects to the APNs and your database if connections are lost.
|
15
|
+
|
16
|
+
### Who uses rapns?
|
17
|
+
|
18
|
+
[GateGuru](http://gateguruapp.com), among others!
|
19
|
+
|
20
|
+
*I'd love to hear if you use rapns - @ileitch on twitter.*
|
15
21
|
|
16
22
|
## Getting Started
|
17
23
|
|
@@ -31,53 +37,46 @@ Generate the migration, rapns.yml and migrate:
|
|
31
37
|
3. Select both the certificate and private key.
|
32
38
|
4. Right click and select `Export 2 items...`.
|
33
39
|
5. Save the file as `cert.p12`, make sure the File Format is `Personal Information Exchange (p12)`.
|
34
|
-
6. If you decide to set a password for your exported certificate, please read the
|
40
|
+
6. If you decide to set a password for your exported certificate, please read the 'Adding Apps' section below.
|
35
41
|
7. Convert the certificate to a .pem, where `<environment>` should be `development` or `production`, depending on the certificate you exported.
|
36
42
|
|
37
43
|
`openssl pkcs12 -nodes -clcerts -in cert.p12 -out <environment>.pem`
|
38
|
-
|
39
|
-
8. Move the .pem file into your Rails application under `config/rapns`.
|
40
|
-
|
41
|
-
## Configuration
|
42
44
|
|
43
|
-
|
45
|
+
## Create an App
|
44
46
|
|
45
|
-
|
47
|
+
app = Rapns::App.new
|
48
|
+
app.key = "my_app"
|
49
|
+
app.certificate = File.read("/path/to/development.pem")
|
50
|
+
app.password = "certificate password"
|
51
|
+
app.connections = 1
|
52
|
+
app.save!
|
46
53
|
|
47
|
-
|
54
|
+
* `key` is a symbolic name to tie this app to notifications.
|
55
|
+
* `certificate` is the contents of your PEM certificate, NOT its path on disk.
|
56
|
+
* `password` should be left blank if you did not password protect your certificate.
|
57
|
+
* `connections` (default: 1) the number of connections to keep open to the APNs. Consider increasing this if you are sending a large number of notifications to this app.
|
48
58
|
|
49
|
-
|
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
|
-
|
60
|
-
* `certificate` The path to your .pem certificate, `config/rapns` is automatically checked if a relative path is given.
|
61
|
-
* `certificate_password` (default: blank) the password you used when exporting your certificate, if any.
|
62
|
-
* `airbrake_notify` (default: true) Enables/disables error notifications via Airbrake.
|
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.
|
59
|
+
The APNs environment is automatically detected from the app certificate, you do not need to configure push and feedback hosts.
|
64
60
|
|
65
61
|
## Starting the rapns Daemon
|
66
62
|
|
67
63
|
cd /path/to/rails/app
|
68
|
-
bundle exec rapns <Rails environment>
|
64
|
+
bundle exec rapns <Rails environment> [options]
|
69
65
|
|
70
66
|
### Options
|
71
67
|
|
72
|
-
* `--foreground`
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
68
|
+
* `-f` `--foreground` Prevent rapns from forking into a daemon.
|
69
|
+
* `-P N` `--db-poll N` Frequency in seconds to check for new notifications. Default: 2.
|
70
|
+
* `-F N` `--feedback-poll N` Frequency in seconds to check for feedback. Default: 60.
|
71
|
+
* `-e` `--no-error-checks` Disables [error checking](#immediately-when-processing-a-notification-for-delivery) after notification delivery. You may want to disable this if you are sending a very high number of notifications.
|
72
|
+
* `-n` `--no-airbrake-notify` Disables error notifications via Airbrake.
|
73
|
+
* `-p PATH` `--pid-file PATH` Path to write PID file. Relative to Rails root unless absolute.
|
74
|
+
* `-b N` `--batch-size N` ActiveRecord batch size of notifications. Increase for possible higher throughput but higher memory footprint. Default: 5000.
|
77
75
|
|
78
76
|
## Sending a Notification
|
79
77
|
|
80
78
|
n = Rapns::Notification.new
|
79
|
+
n.app = "my_app"
|
81
80
|
n.device_token = "934f7a..."
|
82
81
|
n.alert = "This is the message shown on the device."
|
83
82
|
n.badge = 1
|
@@ -87,15 +86,32 @@ rapns logs activity to `rapns.log` in your Rails log directory. This is also pri
|
|
87
86
|
n.deliver_after = 1.hour.from_now
|
88
87
|
n.save!
|
89
88
|
|
89
|
+
* `app` must match `key` on an `Rapns::App`.
|
90
90
|
* `sound` defaults to `1.aiff`. You can either set it to a custom .aiff file, or `nil` for no sound.
|
91
91
|
* `expiry` is the time in seconds the APNs (not rapns) will spend trying to deliver the notification to the device. The notification is discarded if it has not been delivered in this time. Default is 1 day.
|
92
92
|
* `attributes_for_device` is the `NSDictionary` argument passed to your iOS app in either `didFinishLaunchingWithOptions` or `didReceiveRemoteNotification`.
|
93
93
|
* `deliver_after` is not required, but may be set if you'd like to delay delivery of the notification to a specific time in the future.
|
94
94
|
|
95
|
+
### Mobile Device Management
|
96
|
+
|
97
|
+
n = Rapns::Notification.new
|
98
|
+
n.mdm = "magic"
|
99
|
+
n.save!
|
100
|
+
|
95
101
|
### Assigning a Hash to alert
|
96
102
|
|
97
103
|
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).
|
98
104
|
|
105
|
+
## Hot App Updates
|
106
|
+
|
107
|
+
If you signal the rapns process with `HUP` it will synchronize with the current `Rapns::App` configurations. This includes adding an app, removing and increasing/decreasing the number of connections an app uses.
|
108
|
+
|
109
|
+
This synchronization process does not pause the delivery of notifications to other apps.
|
110
|
+
|
111
|
+
## Logging
|
112
|
+
|
113
|
+
rapns logs activity to `rapns.log` in your Rails log directory. This is also printed to STDOUT when running in the foreground. When running as a daemon rapns does not print to STDOUT or STDERR.
|
114
|
+
|
99
115
|
## Delivery Failures
|
100
116
|
|
101
117
|
The APNs provides two mechanism for delivery failure notification:
|
@@ -121,7 +137,7 @@ It is your responsibility to avoid creating new notifications for devices that n
|
|
121
137
|
|
122
138
|
## Updating rapns
|
123
139
|
|
124
|
-
After updating you should run `rails g rapns` to check for any new migrations
|
140
|
+
After updating you should run `rails g rapns` to check for any new migrations.
|
125
141
|
|
126
142
|
## Wiki
|
127
143
|
|
@@ -142,4 +158,5 @@ Thank you to the following wonderful people for contributing to rapns:
|
|
142
158
|
* [@blakewatters](https://github.com/blakewatters)
|
143
159
|
* [@forresty](https://github.com/forresty)
|
144
160
|
* [@sjmadsen](https://github.com/sjmadsen)
|
145
|
-
* [@ivanyv](https://github.com/ivanyv)
|
161
|
+
* [@ivanyv](https://github.com/ivanyv)
|
162
|
+
* [@taybenlor](https://github.com/taybenlor)
|
data/bin/rapns
CHANGED
@@ -2,20 +2,33 @@
|
|
2
2
|
|
3
3
|
require 'optparse'
|
4
4
|
require 'rapns'
|
5
|
-
require 'rapns/daemon'
|
6
5
|
|
7
|
-
foreground = false
|
8
6
|
environment = ARGV[0]
|
7
|
+
|
8
|
+
config = Struct.new(:foreground, :push_poll, :feedback_poll, :airbrake_notify, :check_for_errors, :pid_file, :batch_size).new
|
9
|
+
config.foreground = false
|
10
|
+
config.push_poll = 2
|
11
|
+
config.feedback_poll = 60
|
12
|
+
config.airbrake_notify = true
|
13
|
+
config.check_for_errors = true
|
14
|
+
config.batch_size = 5000
|
15
|
+
|
9
16
|
banner = 'Usage: rapns <Rails environment> [options]'
|
10
17
|
ARGV.options do |opts|
|
11
18
|
opts.banner = banner
|
12
|
-
opts.on('-f', '--foreground', 'Run in the foreground.') { foreground = true }
|
19
|
+
opts.on('-f', '--foreground', 'Run in the foreground.') { config.foreground = true }
|
20
|
+
opts.on('-P N', '--db-poll N', Integer, "Frequency in seconds to check for new notifications. Default: #{config.push_poll}.") { |n| config.push_poll = n }
|
21
|
+
opts.on('-F N', '--feedback-poll N', Integer, "Frequency in seconds to check for feedback. Default: #{config.feedback_poll}.") { |n| config.feedback_poll = n }
|
22
|
+
opts.on('-e', '--no-error-checks', 'Disable error checking after notification delivery.') { config.airbrake_notify = false }
|
23
|
+
opts.on('-n', '--no-airbrake-notify', 'Disables error notifications via Airbrake.') { config.check_for_errors = false }
|
24
|
+
opts.on('-p PATH', '--pid-file PATH', String, 'Path to write PID file. Relative to Rails root unless absolute.') { |path| config.pid_file = path }
|
25
|
+
opts.on('-b N', '--batch-size N', Integer, 'ActiveRecord notifications batch size.') { |n| config.batch_size = n }
|
13
26
|
opts.on('-v', '--version', 'Print this version of rapns.') { puts "rapns #{Rapns::VERSION}"; exit }
|
14
27
|
opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
|
15
28
|
opts.parse!
|
16
29
|
end
|
17
30
|
|
18
|
-
if environment.nil?
|
31
|
+
if environment.nil? || environment =~ /^-/
|
19
32
|
puts banner
|
20
33
|
exit 1
|
21
34
|
end
|
@@ -23,6 +36,11 @@ end
|
|
23
36
|
ENV['RAILS_ENV'] = environment
|
24
37
|
load 'config/environment.rb'
|
25
38
|
|
39
|
+
require 'rapns/daemon'
|
26
40
|
require 'rapns/patches'
|
27
41
|
|
28
|
-
|
42
|
+
if config.pid_file && !Pathname.new(config.pid_file).absolute?
|
43
|
+
config.pid_file = File.join(Rails.root, config.pid_file)
|
44
|
+
end
|
45
|
+
|
46
|
+
Rapns::Daemon.start(environment, config)
|
@@ -13,10 +13,8 @@ class RapnsGenerator < Rails::Generators::Base
|
|
13
13
|
add_rapns_migration('create_rapns_notifications')
|
14
14
|
add_rapns_migration('create_rapns_feedback')
|
15
15
|
add_rapns_migration('add_alert_is_json_to_rapns_notifications')
|
16
|
-
|
17
|
-
|
18
|
-
def copy_config
|
19
|
-
copy_file 'rapns.yml', 'config/rapns/rapns.yml'
|
16
|
+
add_rapns_migration('add_app_to_rapns')
|
17
|
+
add_rapns_migration('create_rapns_apps')
|
20
18
|
end
|
21
19
|
|
22
20
|
protected
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class AddAppToRapns < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_column :rapns_notifications, :app, :string, :null => true
|
4
|
+
add_column :rapns_feedback, :app, :string, :null => true
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.down
|
8
|
+
remove_column :rapns_notifications, :app
|
9
|
+
remove_column :rapns_feedback, :app
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateRapnsApps < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :rapns_apps do |t|
|
4
|
+
t.string :key, :null => false
|
5
|
+
t.text :certificate, :null => false
|
6
|
+
t.string :password, :null => true
|
7
|
+
t.integer :connections, :null => false, :default => 1
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.down
|
13
|
+
drop_table :rapns_apps
|
14
|
+
end
|
15
|
+
end
|
data/lib/rapns/app.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
module Rapns
|
2
|
+
class App < ActiveRecord::Base
|
3
|
+
self.table_name = 'rapns_apps'
|
4
|
+
|
5
|
+
validates :key, :presence => true, :uniqueness => true
|
6
|
+
validates :certificate, :presence => true
|
7
|
+
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class AppRunner
|
4
|
+
HOSTS = {
|
5
|
+
:production => {
|
6
|
+
:push => ['gateway.push.apple.com', 2195],
|
7
|
+
:feedback => ['feedback.push.apple.com', 2196]
|
8
|
+
},
|
9
|
+
:development => {
|
10
|
+
:push => ['gateway.sandbox.push.apple.com', 2195],
|
11
|
+
:feedback => ['feedback.sandbox.push.apple.com', 2196]
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_reader :all
|
17
|
+
end
|
18
|
+
|
19
|
+
@all = {}
|
20
|
+
|
21
|
+
def self.ready
|
22
|
+
ready = []
|
23
|
+
@all.each { |app, runner| ready << app if runner.ready? }
|
24
|
+
ready
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.deliver(notification)
|
28
|
+
if app = @all[notification.app]
|
29
|
+
app.deliver(notification)
|
30
|
+
else
|
31
|
+
Rapns::Daemon.logger.error("No such app '#{notification.app}' for notification #{notification.id}.")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sync
|
36
|
+
apps = Rapns::App.all
|
37
|
+
apps.each do |app|
|
38
|
+
if @all[app.key]
|
39
|
+
@all[app.key].sync(app)
|
40
|
+
else
|
41
|
+
environment = detect_environment(app)
|
42
|
+
next unless environment
|
43
|
+
push_host, push_port = HOSTS[environment][:push]
|
44
|
+
feedback_host, feedback_port = HOSTS[environment][:feedback]
|
45
|
+
feedback_poll = Rapns::Daemon.config.feedback_poll
|
46
|
+
runner = AppRunner.new(app, push_host, push_port, feedback_host, feedback_port, feedback_poll)
|
47
|
+
runner.start
|
48
|
+
@all[app.key] = runner
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
removed = @all.keys - apps.map(&:key)
|
53
|
+
removed.each { |key| @all.delete(key).stop }
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.stop
|
57
|
+
@all.values.map(&:stop)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.debug
|
61
|
+
@all.values.map(&:debug)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.detect_environment(app)
|
65
|
+
if app.certificate =~ /Apple Development IOS Push Services/i
|
66
|
+
:development
|
67
|
+
elsif app.certificate =~ /Apple Production IOS Push Services/i
|
68
|
+
:production
|
69
|
+
else
|
70
|
+
Rapns::Daemon.logger.error("Could not detect environment for app '#{app.key}'.")
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def initialize(app, push_host, push_port, feedback_host, feedback_port, feedback_poll)
|
76
|
+
@app = app
|
77
|
+
@push_host = push_host
|
78
|
+
@push_port = push_port
|
79
|
+
@feedback_host = feedback_host
|
80
|
+
@feedback_port = feedback_port
|
81
|
+
@feedback_poll = feedback_poll
|
82
|
+
|
83
|
+
@queue = DeliveryQueue.new
|
84
|
+
@feedback_receiver = nil
|
85
|
+
@handlers = []
|
86
|
+
end
|
87
|
+
|
88
|
+
def start
|
89
|
+
@feedback_receiver = FeedbackReceiver.new(@app.key, @feedback_host, @feedback_port, @feedback_poll, @app.certificate, @app.password)
|
90
|
+
@feedback_receiver.start
|
91
|
+
|
92
|
+
@app.connections.times { @handlers << start_handler }
|
93
|
+
end
|
94
|
+
|
95
|
+
def deliver(notification)
|
96
|
+
@queue.push(notification)
|
97
|
+
end
|
98
|
+
|
99
|
+
def stop
|
100
|
+
@handlers.map(&:stop)
|
101
|
+
@feedback_receiver.stop if @feedback_receiver
|
102
|
+
end
|
103
|
+
|
104
|
+
def sync(app)
|
105
|
+
@app = app
|
106
|
+
diff = @handlers.size - app.connections
|
107
|
+
if diff > 0
|
108
|
+
diff.times { @handlers.pop.stop }
|
109
|
+
else
|
110
|
+
diff.abs.times { @handlers << start_handler }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def ready?
|
115
|
+
@queue.notifications_processed?
|
116
|
+
end
|
117
|
+
|
118
|
+
def debug
|
119
|
+
Rapns::Daemon.logger.info("\nAppRunner State:\n#{@app.key}:\n handlers: #{@handlers.size}\n backlog: #{@queue.size}\n ready: #{ready?}")
|
120
|
+
end
|
121
|
+
|
122
|
+
protected
|
123
|
+
|
124
|
+
def start_handler
|
125
|
+
handler = DeliveryHandler.new(@queue, @app.key, @push_host, @push_port, @app.certificate, @app.password)
|
126
|
+
handler.start
|
127
|
+
handler
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -9,10 +9,12 @@ module Rapns
|
|
9
9
|
30.minutes
|
10
10
|
end
|
11
11
|
|
12
|
-
def initialize(name, host, port)
|
12
|
+
def initialize(name, host, port, certificate, password)
|
13
13
|
@name = name
|
14
14
|
@host = host
|
15
15
|
@port = port
|
16
|
+
@certificate = certificate
|
17
|
+
@password = password
|
16
18
|
written
|
17
19
|
end
|
18
20
|
|
@@ -89,8 +91,8 @@ module Rapns
|
|
89
91
|
|
90
92
|
def setup_ssl_context
|
91
93
|
ssl_context = OpenSSL::SSL::SSLContext.new
|
92
|
-
ssl_context.key = OpenSSL::PKey::RSA.new(
|
93
|
-
ssl_context.cert = OpenSSL::X509::Certificate.new(
|
94
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(@certificate, @password)
|
95
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(@certificate)
|
94
96
|
ssl_context
|
95
97
|
end
|
96
98
|
|
@@ -3,8 +3,7 @@ module Rapns
|
|
3
3
|
class DeliveryHandler
|
4
4
|
include DatabaseReconnectable
|
5
5
|
|
6
|
-
|
7
|
-
SELECT_TIMEOUT = 0.5
|
6
|
+
SELECT_TIMEOUT = 0.2
|
8
7
|
ERROR_TUPLE_BYTES = 6
|
9
8
|
APN_ERRORS = {
|
10
9
|
1 => "Processing error",
|
@@ -20,17 +19,16 @@ module Rapns
|
|
20
19
|
|
21
20
|
attr_reader :name
|
22
21
|
|
23
|
-
def initialize(
|
24
|
-
@
|
25
|
-
|
26
|
-
|
27
|
-
@connection = Connection.new(@name, host, port)
|
22
|
+
def initialize(queue, name, host, port, certificate, password)
|
23
|
+
@queue = queue
|
24
|
+
@name = "DeliveryHandler:#{name}"
|
25
|
+
@connection = Connection.new(@name, host, port, certificate, password)
|
28
26
|
end
|
29
27
|
|
30
28
|
def start
|
31
29
|
@connection.connect
|
32
30
|
|
33
|
-
Thread.new do
|
31
|
+
@thread = Thread.new do
|
34
32
|
loop do
|
35
33
|
break if @stop
|
36
34
|
handle_next_notification
|
@@ -40,7 +38,7 @@ module Rapns
|
|
40
38
|
|
41
39
|
def stop
|
42
40
|
@stop = true
|
43
|
-
|
41
|
+
@queue.wakeup(@thread)
|
44
42
|
end
|
45
43
|
|
46
44
|
protected
|
@@ -48,7 +46,7 @@ module Rapns
|
|
48
46
|
def deliver(notification)
|
49
47
|
begin
|
50
48
|
@connection.write(notification.to_binary)
|
51
|
-
check_for_error
|
49
|
+
check_for_error if Rapns::Daemon.config.check_for_errors
|
52
50
|
|
53
51
|
with_database_reconnect_and_retry do
|
54
52
|
notification.delivered = true
|
@@ -56,7 +54,7 @@ module Rapns
|
|
56
54
|
notification.save!(:validate => false)
|
57
55
|
end
|
58
56
|
|
59
|
-
Rapns::Daemon.logger.info("
|
57
|
+
Rapns::Daemon.logger.info("[#{@name}] #{notification.id} sent to #{notification.device_token}")
|
60
58
|
rescue Rapns::DeliveryError, Rapns::DisconnectionError => error
|
61
59
|
handle_delivery_error(notification, error)
|
62
60
|
raise
|
@@ -98,9 +96,9 @@ module Rapns
|
|
98
96
|
end
|
99
97
|
|
100
98
|
def handle_next_notification
|
101
|
-
|
102
|
-
|
103
|
-
|
99
|
+
begin
|
100
|
+
notification = @queue.pop
|
101
|
+
rescue DeliveryQueue::WakeupError
|
104
102
|
@connection.close
|
105
103
|
return
|
106
104
|
end
|
@@ -110,7 +108,7 @@ module Rapns
|
|
110
108
|
rescue StandardError => e
|
111
109
|
Rapns::Daemon.logger.error(e)
|
112
110
|
ensure
|
113
|
-
|
111
|
+
@queue.notification_processed
|
114
112
|
end
|
115
113
|
end
|
116
114
|
end
|
@@ -1,19 +1,17 @@
|
|
1
1
|
module Rapns
|
2
2
|
module Daemon
|
3
|
-
class DeliveryHandlerPool
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def new_object_for_pool(i)
|
8
|
-
DeliveryHandler.new(i)
|
3
|
+
class DeliveryHandlerPool
|
4
|
+
def initialize
|
5
|
+
@handlers = []
|
9
6
|
end
|
10
7
|
|
11
|
-
def
|
12
|
-
|
8
|
+
def <<(handler)
|
9
|
+
@handlers << handler
|
10
|
+
handler.start
|
13
11
|
end
|
14
12
|
|
15
|
-
def
|
16
|
-
|
13
|
+
def drain
|
14
|
+
@handlers.pop.stop while !@handlers.empty?
|
17
15
|
end
|
18
16
|
end
|
19
17
|
end
|
@@ -1,19 +1,51 @@
|
|
1
1
|
module Rapns
|
2
2
|
module Daemon
|
3
3
|
class DeliveryQueue
|
4
|
+
class WakeupError < StandardError; end
|
5
|
+
|
4
6
|
def initialize
|
5
7
|
@mutex = Mutex.new
|
6
8
|
@num_notifications = 0
|
7
|
-
@queue =
|
9
|
+
@queue = []
|
10
|
+
@waiting = []
|
8
11
|
end
|
9
12
|
|
10
13
|
def push(notification)
|
11
|
-
@mutex.synchronize
|
12
|
-
|
14
|
+
@mutex.synchronize do
|
15
|
+
@num_notifications += 1
|
16
|
+
@queue.push(notification)
|
17
|
+
|
18
|
+
begin
|
19
|
+
t = @waiting.shift
|
20
|
+
t.wakeup if t
|
21
|
+
rescue ThreadError
|
22
|
+
retry
|
23
|
+
end
|
24
|
+
end
|
13
25
|
end
|
14
26
|
|
15
27
|
def pop
|
16
|
-
@
|
28
|
+
@mutex.synchronize do
|
29
|
+
while true
|
30
|
+
if @queue.empty?
|
31
|
+
@waiting.push Thread.current
|
32
|
+
@mutex.sleep
|
33
|
+
else
|
34
|
+
return @queue.shift
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def wakeup(thread)
|
41
|
+
@mutex.synchronize do
|
42
|
+
t = @waiting.delete(thread)
|
43
|
+
t.raise WakeupError if t
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def size
|
48
|
+
@mutex.synchronize { @queue.size }
|
17
49
|
end
|
18
50
|
|
19
51
|
def notification_processed
|
@@ -1,32 +1,39 @@
|
|
1
1
|
module Rapns
|
2
2
|
module Daemon
|
3
3
|
class FeedbackReceiver
|
4
|
-
|
4
|
+
include InterruptibleSleep
|
5
5
|
|
6
6
|
FEEDBACK_TUPLE_BYTES = 38
|
7
7
|
|
8
|
-
def
|
8
|
+
def initialize(app, host, port, poll, certificate, password)
|
9
|
+
@app = app
|
10
|
+
@host = host
|
11
|
+
@port = port
|
12
|
+
@poll = poll
|
13
|
+
@certificate = certificate
|
14
|
+
@password = password
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
9
18
|
@thread = Thread.new do
|
10
19
|
loop do
|
11
20
|
break if @stop
|
12
21
|
check_for_feedback
|
13
|
-
interruptible_sleep
|
22
|
+
interruptible_sleep @poll
|
14
23
|
end
|
15
24
|
end
|
16
25
|
end
|
17
26
|
|
18
|
-
def
|
27
|
+
def stop
|
19
28
|
@stop = true
|
20
29
|
interrupt_sleep
|
21
30
|
@thread.join if @thread
|
22
31
|
end
|
23
32
|
|
24
|
-
def
|
33
|
+
def check_for_feedback
|
25
34
|
connection = nil
|
26
35
|
begin
|
27
|
-
|
28
|
-
port = Rapns::Daemon.configuration.feedback.port
|
29
|
-
connection = Connection.new("FeedbackReceiver", host, port)
|
36
|
+
connection = Connection.new("FeedbackReceiver:#{@app}", @host, @port, @certificate, @password)
|
30
37
|
connection.connect
|
31
38
|
|
32
39
|
while tuple = connection.read(FEEDBACK_TUPLE_BYTES)
|
@@ -42,15 +49,15 @@ module Rapns
|
|
42
49
|
|
43
50
|
protected
|
44
51
|
|
45
|
-
def
|
52
|
+
def parse_tuple(tuple)
|
46
53
|
failed_at, _, device_token = tuple.unpack("N1n1H*")
|
47
54
|
[Time.at(failed_at).utc, device_token]
|
48
55
|
end
|
49
56
|
|
50
|
-
def
|
57
|
+
def create_feedback(failed_at, device_token)
|
51
58
|
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)
|
59
|
+
Rapns::Daemon.logger.info("[FeedbackReceiver:#{@app}] Delivery failed at #{formatted_failed_at} for #{device_token}")
|
60
|
+
Rapns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
|
54
61
|
end
|
55
62
|
end
|
56
63
|
end
|