rapns 2.0.5 → 3.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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