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.
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