rapns 2.0.5 → 3.0.0.beta.1

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 (82) hide show
  1. data/lib/generators/rapns_generator.rb +1 -0
  2. data/lib/generators/templates/add_gcm.rb +86 -0
  3. data/lib/generators/templates/create_rapns_notifications.rb +1 -1
  4. data/lib/rapns/apns/app.rb +8 -0
  5. data/lib/rapns/apns/binary_notification_validator.rb +12 -0
  6. data/lib/rapns/apns/device_token_format_validator.rb +12 -0
  7. data/lib/rapns/apns/feedback.rb +14 -0
  8. data/lib/rapns/apns/notification.rb +84 -0
  9. data/lib/rapns/app.rb +5 -6
  10. data/lib/rapns/{config.rb → configuration.rb} +5 -5
  11. data/lib/rapns/daemon/apns/app_runner.rb +36 -0
  12. data/lib/rapns/daemon/apns/connection.rb +113 -0
  13. data/lib/rapns/daemon/apns/delivery.rb +63 -0
  14. data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
  15. data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
  16. data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
  17. data/lib/rapns/daemon/app_runner.rb +76 -77
  18. data/lib/rapns/daemon/database_reconnectable.rb +3 -3
  19. data/lib/rapns/daemon/delivery.rb +43 -0
  20. data/lib/rapns/daemon/delivery_error.rb +6 -2
  21. data/lib/rapns/daemon/delivery_handler.rb +13 -79
  22. data/lib/rapns/daemon/delivery_queue_18.rb +2 -2
  23. data/lib/rapns/daemon/delivery_queue_19.rb +3 -3
  24. data/lib/rapns/daemon/feeder.rb +5 -5
  25. data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
  26. data/lib/rapns/daemon/gcm/delivery.rb +206 -0
  27. data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
  28. data/lib/rapns/daemon.rb +31 -20
  29. data/lib/rapns/gcm/app.rb +7 -0
  30. data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
  31. data/lib/rapns/gcm/notification.rb +31 -0
  32. data/lib/rapns/gcm/payload_size_validator.rb +13 -0
  33. data/lib/rapns/multi_json_helper.rb +16 -0
  34. data/lib/rapns/notification.rb +28 -95
  35. data/lib/rapns/version.rb +1 -1
  36. data/lib/rapns.rb +14 -4
  37. data/lib/tasks/cane.rake +19 -0
  38. data/lib/tasks/test.rake +34 -0
  39. data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
  40. data/spec/acceptance_spec_helper.rb +85 -0
  41. data/spec/support/simplecov_helper.rb +13 -0
  42. data/spec/support/simplecov_quality_formatter.rb +8 -0
  43. data/spec/unit/apns/app_spec.rb +15 -0
  44. data/spec/unit/apns/feedback_spec.rb +12 -0
  45. data/spec/{rapns → unit/apns}/notification_spec.rb +44 -72
  46. data/spec/unit/app_spec.rb +18 -0
  47. data/spec/unit/daemon/apns/app_runner_spec.rb +37 -0
  48. data/spec/{rapns/daemon → unit/daemon/apns}/connection_spec.rb +9 -9
  49. data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
  50. data/spec/unit/daemon/apns/delivery_spec.rb +154 -0
  51. data/spec/{rapns/daemon → unit/daemon/apns}/feedback_receiver_spec.rb +14 -14
  52. data/spec/unit/daemon/app_runner_shared.rb +66 -0
  53. data/spec/unit/daemon/app_runner_spec.rb +78 -0
  54. data/spec/{rapns → unit}/daemon/database_reconnectable_spec.rb +4 -5
  55. data/spec/{rapns → unit}/daemon/delivery_error_spec.rb +2 -2
  56. data/spec/unit/daemon/delivery_handler_shared.rb +19 -0
  57. data/spec/{rapns → unit}/daemon/delivery_queue_spec.rb +1 -1
  58. data/spec/{rapns → unit}/daemon/feeder_spec.rb +33 -33
  59. data/spec/unit/daemon/gcm/app_runner_spec.rb +15 -0
  60. data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
  61. data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
  62. data/spec/{rapns → unit}/daemon/interruptible_sleep_spec.rb +1 -1
  63. data/spec/{rapns → unit}/daemon/logger_spec.rb +1 -1
  64. data/spec/{rapns → unit}/daemon_spec.rb +1 -1
  65. data/spec/unit/gcm/app_spec.rb +5 -0
  66. data/spec/unit/gcm/notification_spec.rb +55 -0
  67. data/spec/unit/notification_shared.rb +38 -0
  68. data/spec/unit/notification_spec.rb +6 -0
  69. data/spec/{rapns/app_spec.rb → unit_spec_helper.rb} +76 -16
  70. metadata +107 -45
  71. data/lib/rapns/binary_notification_validator.rb +0 -10
  72. data/lib/rapns/daemon/connection.rb +0 -114
  73. data/lib/rapns/daemon/delivery_handler_pool.rb +0 -18
  74. data/lib/rapns/daemon/disconnection_error.rb +0 -14
  75. data/lib/rapns/daemon/feedback_receiver.rb +0 -82
  76. data/lib/rapns/device_token_format_validator.rb +0 -10
  77. data/lib/rapns/feedback.rb +0 -12
  78. data/spec/rapns/daemon/app_runner_spec.rb +0 -193
  79. data/spec/rapns/daemon/delivery_handler_pool_spec.rb +0 -17
  80. data/spec/rapns/daemon/delivery_handler_spec.rb +0 -206
  81. data/spec/rapns/feedback_spec.rb +0 -12
  82. data/spec/spec_helper.rb +0 -78
@@ -1,130 +1,129 @@
1
1
  module Rapns
2
- module Daemon
2
+ module Daemon
3
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
4
  class << self
16
- attr_reader :all
5
+ attr_reader :runners # TODO: Needed?
17
6
  end
18
7
 
19
- @all = {}
20
-
21
- def self.ready
22
- ready = []
23
- @all.each { |app, runner| ready << app if runner.ready? }
24
- ready
25
- end
8
+ @runners = {}
26
9
 
27
- def self.deliver(notification)
28
- if app = @all[notification.app]
29
- app.deliver(notification)
10
+ def self.enqueue(notification)
11
+ if app = @runners[notification.app_id]
12
+ app.enqueue(notification)
30
13
  else
31
- Rapns::Daemon.logger.error("No such app '#{notification.app}' for notification #{notification.id}.")
14
+ Rapns::Daemon.logger.error("No such app '#{notification.app_id}' for notification #{notification.id}.")
32
15
  end
33
16
  end
34
17
 
35
18
  def self.sync
36
19
  apps = Rapns::App.all
37
- apps.each do |app|
38
- if @all[app.key]
39
- @all[app.key].sync(app)
40
- else
41
- push_host, push_port = HOSTS[app.environment.to_sym][:push]
42
- feedback_host, feedback_port = HOSTS[app.environment.to_sym][:feedback]
43
- feedback_poll = Rapns::Daemon.config.feedback_poll
44
- runner = AppRunner.new(app, push_host, push_port, feedback_host, feedback_port, feedback_poll)
45
- begin
46
- runner.start
47
- @all[app.key] = runner
48
- rescue
49
- Rapns::Daemon.logger.info("[App:#{app.key}] failed to start. No notifications will be sent.")
50
- end
20
+ apps.each { |app| sync_app(app) }
21
+ removed = runners.keys - apps.map(&:id)
22
+ removed.each { |app_id| runners.delete(app_id).stop }
23
+ end
24
+
25
+ def self.sync_app(app)
26
+ if runners[app.id]
27
+ runners[app.id].sync(app)
28
+ else
29
+ runner = new_runner(app)
30
+ begin
31
+ runner.start
32
+ runners[app.id] = runner
33
+ rescue StandardError => e
34
+ Rapns::Daemon.logger.error("[#{app.name}] Exception raised during startup. Notifications will not be delivered for this app.")
35
+ Rapns::Daemon.logger.error(e)
51
36
  end
52
37
  end
38
+ end
53
39
 
54
- removed = @all.keys - apps.map(&:key)
55
- removed.each { |key| @all.delete(key).stop }
40
+ def self.new_runner(app)
41
+ type = app.class.parent.name.demodulize
42
+ "Rapns::Daemon::#{type}::AppRunner".constantize.new(app)
56
43
  end
57
44
 
58
45
  def self.stop
59
- @all.values.map(&:stop)
46
+ @runners.values.map(&:stop)
60
47
  end
61
48
 
62
49
  def self.debug
63
- @all.values.map(&:debug)
50
+ @runners.values.map(&:debug)
51
+ end
52
+
53
+ def self.idle
54
+ runners.values.select { |runner| runner.idle? }
64
55
  end
65
56
 
66
- def initialize(app, push_host, push_port, feedback_host, feedback_port, feedback_poll)
57
+ attr_reader :app
58
+
59
+ def initialize(app)
67
60
  @app = app
68
- @push_host = push_host
69
- @push_port = push_port
70
- @feedback_host = feedback_host
71
- @feedback_port = feedback_port
72
- @feedback_poll = feedback_poll
61
+ end
62
+
63
+ def new_delivery_handler
64
+ raise NotImplementedError
65
+ end
73
66
 
74
- @queue = DeliveryQueue.new
75
- @feedback_receiver = nil
76
- @handlers = []
67
+ def started
77
68
  end
78
69
 
70
+ def stopped
71
+ end
79
72
 
80
73
  def start
81
- begin
82
- @feedback_receiver = FeedbackReceiver.new(@app.key, @feedback_host, @feedback_port, @feedback_poll, @app.certificate, @app.password)
83
- @feedback_receiver.start
84
-
85
- @app.connections.times { @handlers << start_handler }
86
- rescue OpenSSL::SSL::SSLError
87
- # these errors mean that a connection is not possible, raise back to caller
88
- raise
89
- rescue StandardError => e
90
- Rapns::Daemon.logger.error(e)
91
- end
74
+ app.connections.times { handlers << start_handler }
75
+ started
92
76
  end
93
77
 
94
- def deliver(notification)
95
- @queue.push(notification)
78
+ def stop
79
+ handlers.map(&:stop)
80
+ stopped
96
81
  end
97
82
 
98
- def stop
99
- @handlers.map(&:stop)
100
- @feedback_receiver.stop if @feedback_receiver
83
+ def enqueue(notification)
84
+ queue.push(notification)
101
85
  end
102
86
 
103
87
  def sync(app)
104
88
  @app = app
105
- diff = @handlers.size - app.connections
89
+ diff = handlers.size - app.connections
106
90
  if diff > 0
107
- diff.times { @handlers.pop.stop }
91
+ diff.times { handlers.pop.stop }
108
92
  else
109
- diff.abs.times { @handlers << start_handler }
93
+ diff.abs.times { handlers << start_handler }
110
94
  end
111
95
  end
112
96
 
113
- def ready?
114
- @queue.notifications_processed?
97
+ def debug
98
+ Rapns::Daemon.logger.info <<-EOS
99
+
100
+ #{@app.name}:
101
+ handlers: #{handlers.size}
102
+ queued: #{queue.size}
103
+ idle: #{idle?}
104
+ EOS
115
105
  end
116
106
 
117
- def debug
118
- Rapns::Daemon.logger.info("\nAppRunner State:\n#{@app.key}:\n handlers: #{@handlers.size}\n backlog: #{@queue.size}\n ready: #{ready?}")
107
+ def idle?
108
+ queue.notifications_processed?
119
109
  end
120
110
 
121
111
  protected
122
112
 
123
113
  def start_handler
124
- handler = DeliveryHandler.new(@queue, @app.key, @push_host, @push_port, @app.certificate, @app.password)
114
+ handler = new_delivery_handler
115
+ handler.queue = queue
125
116
  handler.start
126
117
  handler
127
118
  end
119
+
120
+ def queue
121
+ @queue ||= Rapns::Daemon::DeliveryQueue.new
122
+ end
123
+
124
+ def handlers
125
+ @handler ||= []
126
+ end
128
127
  end
129
- end
130
- end
128
+ end
129
+ end
@@ -23,11 +23,11 @@ module Rapns
23
23
  end
24
24
 
25
25
  def database_connection_lost
26
- Rapns::Daemon.logger.warn("[#{name}] Lost connection to database, reconnecting...")
26
+ Rapns::Daemon.logger.warn("Lost connection to database, reconnecting...")
27
27
  attempts = 0
28
28
  loop do
29
29
  begin
30
- Rapns::Daemon.logger.warn("[#{name}] Attempt #{attempts += 1}")
30
+ Rapns::Daemon.logger.warn("Attempt #{attempts += 1}")
31
31
  reconnect_database
32
32
  check_database_is_connected
33
33
  break
@@ -36,7 +36,7 @@ module Rapns
36
36
  sleep_to_avoid_thrashing
37
37
  end
38
38
  end
39
- Rapns::Daemon.logger.warn("[#{name}] Database reconnected")
39
+ Rapns::Daemon.logger.warn("Database reconnected")
40
40
  end
41
41
 
42
42
  def reconnect_database
@@ -0,0 +1,43 @@
1
+ module Rapns
2
+ module Daemon
3
+ class Delivery
4
+ include DatabaseReconnectable
5
+
6
+ def self.perform(*args)
7
+ new(*args).perform
8
+ end
9
+
10
+ def retry_after(notification, deliver_after)
11
+ with_database_reconnect_and_retry do
12
+ notification.retries += 1
13
+ notification.deliver_after = deliver_after
14
+ notification.save!(:validate => false)
15
+ end
16
+ end
17
+
18
+ def retry_exponentially(notification)
19
+ retry_after(notification, Time.now + 2 ** (notification.retries + 1))
20
+ end
21
+
22
+ def mark_delivered
23
+ with_database_reconnect_and_retry do
24
+ @notification.delivered = true
25
+ @notification.delivered_at = Time.now
26
+ @notification.save!(:validate => false)
27
+ end
28
+ end
29
+
30
+ def mark_failed(code, description)
31
+ with_database_reconnect_and_retry do
32
+ @notification.delivered = false
33
+ @notification.delivered_at = nil
34
+ @notification.failed = true
35
+ @notification.failed_at = Time.now
36
+ @notification.error_code = code
37
+ @notification.error_description = description
38
+ @notification.save!(:validate => false)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -8,8 +8,12 @@ module Rapns
8
8
  @description = description
9
9
  end
10
10
 
11
+ def to_s
12
+ message
13
+ end
14
+
11
15
  def message
12
- "Unable to deliver notification #{@notification_id}, received APN error #{@code} (#{@description})"
16
+ "Unable to deliver notification #{@notification_id}, received error #{@code} (#{@description})"
13
17
  end
14
18
  end
15
- end
19
+ end
@@ -1,105 +1,39 @@
1
1
  module Rapns
2
2
  module Daemon
3
3
  class DeliveryHandler
4
- include DatabaseReconnectable
4
+ attr_accessor :queue
5
5
 
6
- SELECT_TIMEOUT = 0.2
7
- ERROR_TUPLE_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
- attr_reader :name
21
-
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)
6
+ def deliver(notification)
7
+ raise NotImplementedError
26
8
  end
27
9
 
28
10
  def start
29
- @connection.connect
30
-
31
11
  @thread = Thread.new do
32
12
  loop do
33
- break if @stop
34
13
  handle_next_notification
14
+ break if @stop
35
15
  end
36
16
  end
37
17
  end
38
18
 
39
19
  def stop
40
20
  @stop = true
41
- @queue.wakeup(@thread)
42
- end
43
-
44
- protected
45
-
46
- def deliver(notification)
47
- begin
48
- @connection.write(notification.to_binary)
49
- check_for_error if Rapns::Daemon.config.check_for_errors
50
-
51
- with_database_reconnect_and_retry do
52
- notification.delivered = true
53
- notification.delivered_at = Time.now
54
- notification.save!(:validate => false)
55
- end
56
-
57
- Rapns::Daemon.logger.info("[#{@name}] #{notification.id} sent to #{notification.device_token}")
58
- rescue Rapns::DeliveryError, Rapns::DisconnectionError => error
59
- handle_delivery_error(notification, error)
60
- raise
61
- end
62
- end
63
-
64
- def handle_delivery_error(notification, error)
65
- with_database_reconnect_and_retry do
66
- notification.delivered = false
67
- notification.delivered_at = nil
68
- notification.failed = true
69
- notification.failed_at = Time.now
70
- notification.error_code = error.code
71
- notification.error_description = error.description
72
- notification.save!(:validate => false)
21
+ if @thread
22
+ queue.wakeup(@thread)
23
+ @thread.join
73
24
  end
25
+ stopped
74
26
  end
75
27
 
76
- def check_for_error
77
- if @connection.select(SELECT_TIMEOUT)
78
- error = nil
79
-
80
- if tuple = @connection.read(ERROR_TUPLE_BYTES)
81
- cmd, code, notification_id = tuple.unpack("ccN")
82
-
83
- description = APN_ERRORS[code.to_i] || "Unknown error. Possible rapns bug?"
84
- error = Rapns::DeliveryError.new(code, notification_id, description)
85
- else
86
- error = Rapns::DisconnectionError.new
87
- end
28
+ protected
88
29
 
89
- begin
90
- Rapns::Daemon.logger.error("[#{@name}] Error received, reconnecting...")
91
- @connection.reconnect
92
- ensure
93
- raise error if error
94
- end
95
- end
30
+ def stopped
96
31
  end
97
32
 
98
33
  def handle_next_notification
99
34
  begin
100
- notification = @queue.pop
35
+ notification = queue.pop
101
36
  rescue DeliveryQueue::WakeupError
102
- @connection.close
103
37
  return
104
38
  end
105
39
 
@@ -108,9 +42,9 @@ module Rapns
108
42
  rescue StandardError => e
109
43
  Rapns::Daemon.logger.error(e)
110
44
  ensure
111
- @queue.notification_processed
45
+ queue.notification_processed
112
46
  end
113
47
  end
114
48
  end
115
49
  end
116
- end
50
+ end
@@ -1,6 +1,6 @@
1
1
  module Rapns
2
2
  module Daemon
3
- class DeliveryQueue18
3
+ class DeliveryQueue18
4
4
  def push(obj)
5
5
  Thread.critical = true
6
6
  @queue.push obj
@@ -39,6 +39,6 @@ module Rapns
39
39
  Thread.critical = false
40
40
  end
41
41
  end
42
- end
42
+ end
43
43
  end
44
44
  end
@@ -1,6 +1,6 @@
1
1
  module Rapns
2
2
  module Daemon
3
- class DeliveryQueue19
3
+ class DeliveryQueue19
4
4
  def initialize
5
5
  @mutex = Mutex.new
6
6
  end
@@ -35,8 +35,8 @@ module Rapns
35
35
  protected
36
36
 
37
37
  def synchronize(&blk)
38
- @mutex.synchronize(&blk)
38
+ @mutex.synchronize(&blk)
39
39
  end
40
- end
40
+ end
41
41
  end
42
42
  end
@@ -10,9 +10,9 @@ module Rapns
10
10
 
11
11
  def self.start(poll)
12
12
  loop do
13
- break if @stop
14
13
  enqueue_notifications
15
14
  interruptible_sleep poll
15
+ break if @stop
16
16
  end
17
17
  end
18
18
 
@@ -26,10 +26,10 @@ module Rapns
26
26
  def self.enqueue_notifications
27
27
  begin
28
28
  with_database_reconnect_and_retry do
29
- ready_apps = Rapns::Daemon::AppRunner.ready
30
29
  batch_size = Rapns::Daemon.config.batch_size
31
- Rapns::Notification.ready_for_delivery.find_each(:batch_size => batch_size) do |notification|
32
- Rapns::Daemon::AppRunner.deliver(notification) if ready_apps.include?(notification.app)
30
+ idle = Rapns::Daemon::AppRunner.idle.map(&:app)
31
+ Rapns::Notification.ready_for_delivery.for_apps(idle).limit(batch_size).each do |notification|
32
+ Rapns::Daemon::AppRunner.enqueue(notification)
33
33
  end
34
34
  end
35
35
  rescue StandardError => e
@@ -38,4 +38,4 @@ module Rapns
38
38
  end
39
39
  end
40
40
  end
41
- end
41
+ end
@@ -0,0 +1,13 @@
1
+ module Rapns
2
+ module Daemon
3
+ module Gcm
4
+ class AppRunner < Rapns::Daemon::AppRunner
5
+ protected
6
+
7
+ def new_delivery_handler
8
+ DeliveryHandler.new(app)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end