rapns 0.2.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/README.md +35 -10
  2. data/lib/generators/rapns_generator.rb +9 -1
  3. data/lib/generators/templates/create_rapns_feedback.rb +15 -0
  4. data/lib/generators/templates/rapns.yml +22 -9
  5. data/lib/rapns/daemon/configuration.rb +32 -14
  6. data/lib/rapns/daemon/connection.rb +35 -50
  7. data/lib/rapns/daemon/delivery_handler.rb +65 -15
  8. data/lib/rapns/daemon/delivery_handler_pool.rb +1 -5
  9. data/lib/rapns/daemon/delivery_queue.rb +10 -27
  10. data/lib/rapns/daemon/feedback_receiver.rb +57 -0
  11. data/lib/rapns/daemon/feeder.rb +9 -7
  12. data/lib/rapns/daemon/interruptible_sleep.rb +14 -0
  13. data/lib/rapns/daemon/pool.rb +0 -5
  14. data/lib/rapns/daemon.rb +9 -9
  15. data/lib/rapns/device_token_format_validator.rb +10 -0
  16. data/lib/rapns/feedback.rb +10 -0
  17. data/lib/rapns/notification.rb +15 -1
  18. data/lib/rapns/version.rb +1 -1
  19. data/lib/rapns.rb +3 -1
  20. data/spec/rapns/daemon/certificate_spec.rb +6 -0
  21. data/spec/rapns/daemon/configuration_spec.rb +124 -40
  22. data/spec/rapns/daemon/connection_spec.rb +81 -129
  23. data/spec/rapns/daemon/delivery_handler_pool_spec.rb +1 -6
  24. data/spec/rapns/daemon/delivery_handler_spec.rb +117 -30
  25. data/spec/rapns/daemon/delivery_queue_spec.rb +29 -0
  26. data/spec/rapns/daemon/feedback_receiver_spec.rb +86 -0
  27. data/spec/rapns/daemon/feeder_spec.rb +25 -9
  28. data/spec/rapns/daemon/interruptible_sleep_spec.rb +32 -0
  29. data/spec/rapns/daemon/logger_spec.rb +34 -14
  30. data/spec/rapns/daemon_spec.rb +34 -31
  31. data/spec/rapns/feedback_spec.rb +12 -0
  32. data/spec/rapns/notification_spec.rb +5 -0
  33. data/spec/spec_helper.rb +5 -2
  34. metadata +16 -5
  35. data/lib/rapns/daemon/connection_pool.rb +0 -31
  36. 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
- * `host` the APNs host to connect to, either `gateway.sandbox.push.apple.com` or `gateway.sandbox.push.apple.com`.
47
- * `port` the APNs port. Currently `2195` for both hosts.
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 APN service provides two mechanism for delivery failure notification:
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
- Not implemented yet!
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
- migration_template "create_rapns_notifications.rb", "db/migrate/create_rapns_notifications.rb"
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
- airbrake_notify: true
7
- poll: 2
8
- connections: 3
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 :host, :port, :certificate, :certificate_password, :poll, :airbrake_notify, :connections, :pid_file
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(:certificate, config)
23
- set_variable(:airbrake_notify, config, :optional => true, :default => true)
24
- set_variable(:certificate_password, config, :optional => true, :default => "")
25
- set_variable(:poll, config, :optional => true, :default => 2)
26
- set_variable(:connections, config, :optional => true, :default => 3)
27
- set_variable(:pid_file, config, :optional => true, :default => "")
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 !config.key?(key.to_s) || config[key.to_s].to_s.strip == ""
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
- instance_variable_set("@#{key}", options[:default])
75
+ base.send("#{key}=", options[:default])
59
76
  else
60
- raise Rapns::ConfigurationError, "'#{key}' not defined for environment '#{@environment}' in #{@config_path}"
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
- instance_variable_set("@#{key}", config[key.to_s])
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
- SELECT_TIMEOUT = 0.5
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
- @ssl_socket.close if @ssl_socket
31
- @tcp_socket.close if @tcp_socket
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
- @ssl_socket.write(data)
39
- @ssl_socket.flush
37
+ write_data(data)
38
+ rescue Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError => e
39
+ retry_count += 1;
40
40
 
41
- check_for_error
42
- rescue Errno::EPIPE, OpenSSL::SSL::SSLError => e
43
- Rapns::Daemon.logger.error("[#{@name}] Lost connection to #{Rapns::Daemon.configuration.host}:#{Rapns::Daemon.configuration.port}, reconnecting...")
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 < 3
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: #{e.inspect}"
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
- protected
58
-
59
- def check_for_error
60
- if IO.select([@ssl_socket], nil, nil, SELECT_TIMEOUT)
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
- if cmd == 8 && status != 0
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
- begin
73
- Rapns::Daemon.logger.error("[#{@name}] Error received, reconnecting...")
74
- close
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(Rapns::Daemon.configuration.host, Rapns::Daemon.configuration.port)
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 #{Rapns::Daemon.configuration.host}:#{Rapns::Daemon.configuration.port}")
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
- Thread.new do
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
- Rapns::Daemon.connection_pool.claim_connection do |connection|
23
- begin
24
- connection.write(notification.to_binary)
47
+ begin
48
+ @connection.write(notification.to_binary)
49
+ check_for_error
25
50
 
26
- notification.delivered = true
27
- notification.delivered_at = Time.now
28
- notification.save!(:validate => false)
51
+ notification.delivered = true
52
+ notification.delivered_at = Time.now
53
+ notification.save!(:validate => false)
29
54
 
30
- Rapns::Daemon.logger.info("Notification #{notification.id} delivered to #{notification.device_token}")
31
- rescue Rapns::DeliveryError => error
32
- handle_delivery_error(notification, error)
33
- end
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.handler_available
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(num_handlers)
5
- @num_handlers = num_handlers
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(obj)
12
- @queue.push(obj)
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 handler_available
20
- @handler_mutex.synchronize do
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 handler_available?
26
- @queue.size < @num_handlers
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
@@ -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::Notification.ready_for_delivery.each do |notification|
28
- Rapns::Daemon.delivery_queue.push(notification)
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
- # Simple asking the adapter for the connection state is not sufficient.
67
+ # Simply asking the adapter for the connection state is not sufficient.
66
68
  Rapns::Notification.count
67
69
  end
68
70