rapns 1.0.7 → 2.0.0rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG.md +7 -0
  2. data/LICENSE +7 -0
  3. data/README.md +58 -41
  4. data/bin/rapns +23 -5
  5. data/lib/generators/rapns_generator.rb +2 -4
  6. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  7. data/lib/generators/templates/create_rapns_apps.rb +15 -0
  8. data/lib/rapns/app.rb +9 -0
  9. data/lib/rapns/daemon/app_runner.rb +131 -0
  10. data/lib/rapns/daemon/connection.rb +5 -3
  11. data/lib/rapns/daemon/delivery_handler.rb +13 -15
  12. data/lib/rapns/daemon/delivery_handler_pool.rb +8 -10
  13. data/lib/rapns/daemon/delivery_queue.rb +36 -4
  14. data/lib/rapns/daemon/feedback_receiver.rb +19 -12
  15. data/lib/rapns/daemon/feeder.rb +8 -10
  16. data/lib/rapns/daemon/logger.rb +5 -3
  17. data/lib/rapns/daemon.rb +52 -38
  18. data/lib/rapns/notification.rb +16 -5
  19. data/lib/rapns/patches.rb +2 -2
  20. data/lib/rapns/version.rb +1 -1
  21. data/lib/rapns.rb +2 -1
  22. data/spec/rapns/daemon/app_runner_spec.rb +207 -0
  23. data/spec/rapns/daemon/connection_spec.rb +177 -236
  24. data/spec/rapns/daemon/delivery_handler_pool_spec.rb +10 -14
  25. data/spec/rapns/daemon/delivery_handler_spec.rb +92 -79
  26. data/spec/rapns/daemon/feedback_receiver_spec.rb +29 -23
  27. data/spec/rapns/daemon/feeder_spec.rb +40 -44
  28. data/spec/rapns/daemon/logger_spec.rb +21 -3
  29. data/spec/rapns/daemon_spec.rb +65 -125
  30. data/spec/rapns/notification_spec.rb +16 -0
  31. data/spec/spec_helper.rb +4 -1
  32. metadata +14 -15
  33. data/History.md +0 -5
  34. data/lib/generators/templates/rapns.yml +0 -31
  35. data/lib/rapns/daemon/certificate.rb +0 -27
  36. data/lib/rapns/daemon/configuration.rb +0 -98
  37. data/lib/rapns/daemon/pool.rb +0 -36
  38. data/spec/rapns/daemon/certificate_spec.rb +0 -22
  39. data/spec/rapns/daemon/configuration_spec.rb +0 -231
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## 2.0.0 (unreleased) ##
2
+
3
+ * Support for multiple apps.
4
+ * Hot Updates - add/remove apps without restart.
5
+ * MDM support.
6
+ * Removed rapns.yml in favour of command line options.
7
+ * Started the changelog!
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
- # rapns [![Build Status](https://secure.travis-ci.org/ileitch/rapns.png)](http://travis-ci.org/ileitch/rapns)
1
+ [![Build Status](https://secure.travis-ci.org/ileitch/rapns.png)](http://travis-ci.org/ileitch/rapns)
2
2
 
3
- Easy to use library for Apple's Push Notification Service with Rails 3.
4
-
5
- ## Features
3
+ # Features
6
4
 
7
5
  * Works with Rails 3 and Ruby 1.9 & 1.8.
8
- * Uses a daemon process to keep open a persistent connection to the Push Notification Service, as recommended by Apple.
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).
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](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
+ * 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 Configuration section below.
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
- Environment configuration lives in `config/rapns/rapns.yml`. For common setups you probably wont need to change this file.
45
+ ## Create an App
44
46
 
45
- If you want to use rapns in environments other than development or production, you will need to create an entry for it. Simply duplicate the configuration for development or production, depending on which iOS Push Certificate you wish to use.
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
- ### Options:
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
- * `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
-
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` will prevent rapns from forking into a daemon.
73
-
74
- ## Logging
75
-
76
- 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.
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 or configuration changes.
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
- Rapns::Daemon.start(environment, foreground)
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
- end
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(Rapns::Daemon.certificate.certificate, Rapns::Daemon.configuration.certificate_password)
93
- ssl_context.cert = OpenSSL::X509::Certificate.new(Rapns::Daemon.certificate.certificate)
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
- STOP = 0x666
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(i)
24
- @name = "DeliveryHandler #{i}"
25
- host = Rapns::Daemon.configuration.push.host
26
- port = Rapns::Daemon.configuration.push.port
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
- Rapns::Daemon.delivery_queue.push(STOP)
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("Notification #{notification.id} delivered to #{notification.device_token}")
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
- notification = Rapns::Daemon.delivery_queue.pop
102
-
103
- if notification == STOP
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
- Rapns::Daemon.delivery_queue.notification_processed
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 < Pool
4
-
5
- protected
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 object_added_to_pool(object)
12
- object.start
8
+ def <<(handler)
9
+ @handlers << handler
10
+ handler.start
13
11
  end
14
12
 
15
- def object_removed_from_pool(object)
16
- object.stop
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 = Queue.new
9
+ @queue = []
10
+ @waiting = []
8
11
  end
9
12
 
10
13
  def push(notification)
11
- @mutex.synchronize { @num_notifications += 1 }
12
- @queue.push(notification)
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
- @queue.pop
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
- extend InterruptibleSleep
4
+ include InterruptibleSleep
5
5
 
6
6
  FEEDBACK_TUPLE_BYTES = 38
7
7
 
8
- def self.start
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 Rapns::Daemon.configuration.feedback.poll
22
+ interruptible_sleep @poll
14
23
  end
15
24
  end
16
25
  end
17
26
 
18
- def self.stop
27
+ def stop
19
28
  @stop = true
20
29
  interrupt_sleep
21
30
  @thread.join if @thread
22
31
  end
23
32
 
24
- def self.check_for_feedback
33
+ def check_for_feedback
25
34
  connection = nil
26
35
  begin
27
- host = Rapns::Daemon.configuration.feedback.host
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 self.parse_tuple(tuple)
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 self.create_feedback(failed_at, device_token)
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