rapns 3.2.0 → 3.3.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/CHANGELOG.md +5 -0
  2. data/README.md +9 -18
  3. data/lib/generators/templates/rapns.rb +5 -0
  4. data/lib/rapns.rb +4 -0
  5. data/lib/rapns/apns_feedback.rb +1 -0
  6. data/lib/rapns/configuration.rb +4 -3
  7. data/lib/rapns/daemon.rb +20 -4
  8. data/lib/rapns/daemon/apns/feedback_receiver.rb +9 -11
  9. data/lib/rapns/daemon/delivery.rb +3 -20
  10. data/lib/rapns/daemon/delivery_queue.rb +2 -2
  11. data/lib/rapns/daemon/feeder.rb +5 -10
  12. data/lib/rapns/daemon/gcm/delivery.rb +19 -9
  13. data/lib/rapns/daemon/store/active_record.rb +74 -0
  14. data/lib/rapns/daemon/store/active_record/reconnectable.rb +61 -0
  15. data/lib/rapns/gcm/notification.rb +5 -4
  16. data/lib/rapns/gcm/registration_ids_count_validator.rb +1 -1
  17. data/lib/rapns/logger.rb +6 -2
  18. data/lib/rapns/push.rb +1 -0
  19. data/lib/rapns/reflection.rb +1 -1
  20. data/lib/rapns/version.rb +1 -1
  21. data/spec/unit/apns/notification_spec.rb +2 -0
  22. data/spec/unit/apns_feedback_spec.rb +5 -0
  23. data/spec/unit/configuration_spec.rb +1 -1
  24. data/spec/unit/daemon/apns/delivery_spec.rb +7 -64
  25. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +2 -2
  26. data/spec/unit/daemon/feeder_spec.rb +6 -58
  27. data/spec/unit/daemon/gcm/delivery_spec.rb +49 -57
  28. data/spec/unit/daemon/{database_reconnectable_spec.rb → store/active_record/reconnectable_spec.rb} +4 -3
  29. data/spec/unit/daemon/store/active_record_spec.rb +181 -0
  30. data/spec/unit/daemon_spec.rb +27 -7
  31. data/spec/unit/gcm/notification_spec.rb +2 -9
  32. data/spec/unit/push_spec.rb +5 -0
  33. data/spec/unit/reflection_spec.rb +0 -4
  34. data/spec/unit_spec_helper.rb +4 -1
  35. metadata +10 -7
  36. data/lib/rapns/daemon/database_reconnectable.rb +0 -57
@@ -1,3 +1,8 @@
1
+ ## 3.3.0 (April 21, 2013)
2
+ * GCM: collapse_key is no longer required to set expiry (time_to_live).
3
+ * Add reflection for GCM canonical IDs.
4
+ * Add Rapns::Daemon.store to decouple storage backend.
5
+
1
6
  ## 3.2.0 (Apr 1, 2013)
2
7
  * Rapns.apns_feedback for one time feedback retrieval. Rapns.push no longer checks for feedback (#117, #105).
3
8
  * Lazily connect to the APNs only when a notification is to be delivered (#111).
data/README.md CHANGED
@@ -34,26 +34,12 @@ Generate the migrations, rapns.yml and migrate:
34
34
  rails g rapns
35
35
  rake db:migrate
36
36
 
37
- ## Generating Certificates (APNs only)
38
-
39
- 1. Open up Keychain Access and select the `Certificates` category in the sidebar.
40
- 2. Expand the disclosure arrow next to the iOS Push Services certificate you want to export.
41
- 3. Select both the certificate and private key.
42
- 4. Right click and select `Export 2 items...`.
43
- 5. Save the file as `cert.p12`, make sure the File Format is `Personal Information Exchange (p12)`.
44
- 6. Convert the certificate to a .pem, where `<environment>` should be `sandbox` or `production`, depending on the certificate you exported.
45
-
46
- Without a password:
47
-
48
- `openssl pkcs12 -nodes -clcerts -in cert.p12 -out <environment>.pem`
49
-
50
- With a password:
51
-
52
- `openssl pkcs12 -clcerts -in cert.p12 -out <environment>.pem`
53
-
54
37
  ## Create an App
55
38
 
56
39
  #### APNs
40
+
41
+ If this is your first time using the APNs, you will need to generate SSL certificates. See [Generating Certificates](https://github.com/ileitch/rapns/wiki/Generating-Certificates) for instructions.
42
+
57
43
  ```ruby
58
44
  app = Rapns::Apns::App.new
59
45
  app.name = "ios_app"
@@ -107,9 +93,10 @@ Inside an existing process (see [Embedding API](https://github.com/ileitch/rapns
107
93
 
108
94
  *Please note that only ever a single instance of Rapns should be running.*
109
95
 
110
- In a scheduler:
96
+ In a scheduler (see [Push API](https://github.com/ileitch/rapns/wiki/Push-API)):
111
97
 
112
98
  Rapns.push
99
+ Rapns.apns_feedback
113
100
 
114
101
  See [Configuration](https://github.com/ileitch/rapns/wiki/Configuration) for a list of options, or run `rapns --help`.
115
102
 
@@ -130,12 +117,16 @@ After updating you should run `rails g rapns` to check for any new migrations.
130
117
  * [Embedding API](https://github.com/ileitch/rapns/wiki/Embedding-API)
131
118
 
132
119
  ### APNs
120
+ * [Generating Certificates](https://github.com/ileitch/rapns/wiki/Generating-Certificates)
133
121
  * [Advanced APNs Features](https://github.com/ileitch/rapns/wiki/Advanced-APNs-Features)
134
122
  * [APNs Delivery Failure Handling](https://github.com/ileitch/rapns/wiki/APNs-Delivery-Failure-Handling)
135
123
  * [Why open multiple connections to the APNs?](https://github.com/ileitch/rapns/wiki/Why-open-multiple-connections-to-the-APNs%3F)
136
124
  * [Silent failures might be dropped connections](https://github.com/ileitch/rapns/wiki/Dropped-connections)
137
125
 
138
126
  ### GCM
127
+ * [Notification Options](https://github.com/ileitch/rapns/wiki//GCM-Notification-Options)
128
+ * [Canonical IDs](https://github.com/ileitch/rapns/wiki/Canonical-IDs)
129
+ * [Delivery Failures & Retries](https://github.com/ileitch/rapns/wiki/Delivery-Failures-&-Retries)
139
130
 
140
131
  ## Contributing
141
132
 
@@ -63,6 +63,11 @@ Rapns.reflect do |on|
63
63
  # on.apns_connection_lost do |app, error|
64
64
  # end
65
65
 
66
+ # Called when the GCM returns a canonical registration ID.
67
+ # You will need to replace old_id with canonical_id in your records.
68
+ # on.gcm_canonical_id do |old_id, canonical_id|
69
+ # end
70
+
66
71
  # Called when an exception is raised.
67
72
  # on.error do |error|
68
73
  # end
@@ -29,6 +29,10 @@ require 'rapns/gcm/notification'
29
29
  require 'rapns/gcm/app'
30
30
 
31
31
  module Rapns
32
+ def self.jruby?
33
+ defined? JRUBY_VERSION
34
+ end
35
+
32
36
  def self.require_for_daemon
33
37
  require 'rapns/daemon'
34
38
  require 'rapns/patches'
@@ -1,6 +1,7 @@
1
1
  module Rapns
2
2
  def self.apns_feedback
3
3
  Rapns.require_for_daemon
4
+ Rapns::Daemon.initialize_store
4
5
 
5
6
  Rapns::Apns::App.all.each do |app|
6
7
  receiver = Rapns::Daemon::Apns::FeedbackReceiver.new(app, 0)
@@ -9,7 +9,7 @@ module Rapns
9
9
 
10
10
  CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll, :embedded,
11
11
  :airbrake_notify, :check_for_errors, :pid_file, :batch_size,
12
- :push]
12
+ :push, :store]
13
13
 
14
14
  class ConfigurationWithoutDefaults < Struct.new(*CONFIG_ATTRS)
15
15
  end
@@ -40,7 +40,7 @@ module Rapns
40
40
  end
41
41
 
42
42
  def foreground=(bool)
43
- if defined? JRUBY_VERSION
43
+ if Rapns.jruby?
44
44
  # The JVM does not support fork().
45
45
  super(true)
46
46
  else
@@ -56,7 +56,7 @@ module Rapns
56
56
  private
57
57
 
58
58
  def set_defaults
59
- if defined? JRUBY_VERSION
59
+ if Rapns.jruby?
60
60
  # The JVM does not support fork().
61
61
  self.foreground = true
62
62
  else
@@ -70,6 +70,7 @@ module Rapns
70
70
  self.batch_size = 5000
71
71
  self.pid_file = nil
72
72
  self.apns_feedback_callback = nil
73
+ self.store = :active_record
73
74
 
74
75
  # Internal options.
75
76
  self.embedded = false
@@ -8,7 +8,6 @@ require 'net/http/persistent'
8
8
  require 'rapns/daemon/reflectable'
9
9
  require 'rapns/daemon/interruptible_sleep'
10
10
  require 'rapns/daemon/delivery_error'
11
- require 'rapns/daemon/database_reconnectable'
12
11
  require 'rapns/daemon/delivery'
13
12
  require 'rapns/daemon/delivery_queue'
14
13
  require 'rapns/daemon/feeder'
@@ -28,14 +27,19 @@ require 'rapns/daemon/gcm/delivery_handler'
28
27
 
29
28
  module Rapns
30
29
  module Daemon
31
- extend DatabaseReconnectable
30
+ class << self
31
+ attr_accessor :store
32
+ end
32
33
 
33
34
  def self.start
34
35
  setup_signal_traps if trap_signals?
35
36
 
37
+ initialize_store
38
+ return unless store
39
+
36
40
  if daemonize?
37
41
  daemonize
38
- reconnect_database
42
+ store.after_daemonize
39
43
  end
40
44
 
41
45
  write_pid_file
@@ -51,10 +55,22 @@ module Rapns
51
55
  delete_pid_file
52
56
  end
53
57
 
58
+ def self.initialize_store
59
+ return if store
60
+ begin
61
+ require "rapns/daemon/store/#{Rapns.config.store}"
62
+ klass = "Rapns::Daemon::Store::#{Rapns.config.store.to_s.camelcase}".constantize
63
+ self.store = klass.new
64
+ rescue StandardError, LoadError => e
65
+ Rapns.logger.error("Failed to load '#{Rapns.config.store}' storage backend.")
66
+ Rapns.logger.error(e)
67
+ end
68
+ end
69
+
54
70
  protected
55
71
 
56
72
  def self.daemonize?
57
- !(Rapns.config.foreground || Rapns.config.embedded || defined?(JRUBY_VERSION))
73
+ !(Rapns.config.foreground || Rapns.config.embedded || Rapns.jruby?)
58
74
  end
59
75
 
60
76
  def self.trap_signals?
@@ -4,7 +4,6 @@ module Rapns
4
4
  class FeedbackReceiver
5
5
  include Reflectable
6
6
  include InterruptibleSleep
7
- include DatabaseReconnectable
8
7
 
9
8
  FEEDBACK_TUPLE_BYTES = 38
10
9
  HOSTS = {
@@ -63,17 +62,16 @@ module Rapns
63
62
 
64
63
  def create_feedback(failed_at, device_token)
65
64
  formatted_failed_at = failed_at.strftime("%Y-%m-%d %H:%M:%S UTC")
66
- with_database_reconnect_and_retry do
67
- Rapns.logger.info("[#{@app.name}] [FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}.")
68
- feedback = Rapns::Apns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
69
- reflect(:apns_feedback, feedback)
65
+ Rapns.logger.info("[#{@app.name}] [FeedbackReceiver] Delivery failed at #{formatted_failed_at} for #{device_token}.")
70
66
 
71
- # Deprecated.
72
- begin
73
- Rapns.config.apns_feedback_callback.call(feedback) if Rapns.config.apns_feedback_callback
74
- rescue StandardError => e
75
- Rapns.logger.error(e)
76
- end
67
+ feedback = Rapns::Daemon.store.create_apns_feedback(failed_at, device_token, @app)
68
+ reflect(:apns_feedback, feedback)
69
+
70
+ # Deprecated.
71
+ begin
72
+ Rapns.config.apns_feedback_callback.call(feedback) if Rapns.config.apns_feedback_callback
73
+ rescue StandardError => e
74
+ Rapns.logger.error(e)
77
75
  end
78
76
  end
79
77
  end
@@ -1,7 +1,6 @@
1
1
  module Rapns
2
2
  module Daemon
3
3
  class Delivery
4
- include DatabaseReconnectable
5
4
  include Reflectable
6
5
 
7
6
  def self.perform(*args)
@@ -9,11 +8,7 @@ module Rapns
9
8
  end
10
9
 
11
10
  def retry_after(notification, deliver_after)
12
- with_database_reconnect_and_retry do
13
- notification.retries += 1
14
- notification.deliver_after = deliver_after
15
- notification.save!(:validate => false)
16
- end
11
+ Rapns::Daemon.store.retry_after(notification, deliver_after)
17
12
  reflect(:notification_will_retry, notification)
18
13
  end
19
14
 
@@ -22,24 +17,12 @@ module Rapns
22
17
  end
23
18
 
24
19
  def mark_delivered
25
- with_database_reconnect_and_retry do
26
- @notification.delivered = true
27
- @notification.delivered_at = Time.now
28
- @notification.save!(:validate => false)
29
- end
20
+ Rapns::Daemon.store.mark_delivered(@notification)
30
21
  reflect(:notification_delivered, @notification)
31
22
  end
32
23
 
33
24
  def mark_failed(code, description)
34
- with_database_reconnect_and_retry do
35
- @notification.delivered = false
36
- @notification.delivered_at = nil
37
- @notification.failed = true
38
- @notification.failed_at = Time.now
39
- @notification.error_code = code
40
- @notification.error_description = description
41
- @notification.save!(:validate => false)
42
- end
25
+ Rapns::Daemon.store.mark_failed(@notification, code, description)
43
26
  reflect(:notification_failed, @notification)
44
27
  end
45
28
  end
@@ -35,8 +35,8 @@ module Rapns
35
35
  end
36
36
 
37
37
  def notifications_processed?
38
- synchronize { @num_notifications == 0 }
38
+ synchronize { @num_notifications <= 0 }
39
39
  end
40
40
  end
41
41
  end
42
- end
42
+ end
@@ -2,7 +2,6 @@ module Rapns
2
2
  module Daemon
3
3
  class Feeder
4
4
  extend InterruptibleSleep
5
- extend DatabaseReconnectable
6
5
  extend Reflectable
7
6
 
8
7
  def self.start
@@ -38,15 +37,11 @@ module Rapns
38
37
 
39
38
  def self.enqueue_notifications
40
39
  begin
41
- with_database_reconnect_and_retry do
42
- batch_size = Rapns.config.batch_size
43
- idle = Rapns::Daemon::AppRunner.idle.map(&:app)
44
- relation = Rapns::Notification.ready_for_delivery.for_apps(idle)
45
- relation = relation.limit(batch_size) unless Rapns.config.push
46
- relation.each do |notification|
47
- Rapns::Daemon::AppRunner.enqueue(notification)
48
- reflect(:notification_enqueued, notification)
49
- end
40
+ idle = Rapns::Daemon::AppRunner.idle.map(&:app)
41
+
42
+ Rapns::Daemon.store.deliverable_notifications(idle).each do |notification|
43
+ Rapns::Daemon::AppRunner.enqueue(notification)
44
+ reflect(:notification_enqueued, notification)
50
45
  end
51
46
  rescue StandardError => e
52
47
  Rapns.logger.error(e)
@@ -51,6 +51,8 @@ module Rapns
51
51
  else
52
52
  handle_errors(response, body)
53
53
  end
54
+
55
+ handle_canonical_ids(response, body)
54
56
  end
55
57
 
56
58
  def handle_errors(response, body)
@@ -69,6 +71,17 @@ module Rapns
69
71
  end
70
72
  end
71
73
 
74
+ def handle_canonical_ids(response, body)
75
+ if body['canonical_ids'] && body['canonical_ids'].to_i > 0
76
+ body['results'].each_with_index do |result, i|
77
+ if result['message_id'] && result['registration_id']
78
+ old_id = @notification.registration_ids[i]
79
+ reflect(:gcm_canonical_id, old_id, result['registration_id'])
80
+ end
81
+ end
82
+ end
83
+ end
84
+
72
85
  def bad_request(response)
73
86
  raise Rapns::DeliveryError.new(400, @notification.id, 'GCM failed to parse the JSON request. Possibly an rapns bug, please open an issue.')
74
87
  end
@@ -94,19 +107,16 @@ module Rapns
94
107
 
95
108
  def some_devices_unavailable(response, errors)
96
109
  unavailable_idxs = errors.find_all { |i, error| error.in?(UNAVAILABLE_STATES) }.map(&:first)
97
- new_notification = build_new_notification(response, unavailable_idxs)
98
- with_database_reconnect_and_retry { new_notification.save! }
110
+ new_notification = create_new_notification(response, unavailable_idxs)
99
111
  raise Rapns::DeliveryError.new(nil, @notification.id,
100
112
  describe_errors(errors) + " #{unavailable_idxs.join(', ')} will be retried as notification #{new_notification.id}.")
101
113
  end
102
114
 
103
- def build_new_notification(response, idxs)
104
- notification = Rapns::Gcm::Notification.new
105
- notification.assign_attributes(@notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle'))
106
- notification.data = @notification.data
107
- notification.registration_ids = idxs.map { |i| @notification.registration_ids[i] }
108
- notification.deliver_after = deliver_after_header(response)
109
- notification
115
+ def create_new_notification(response, unavailable_idxs)
116
+ attrs = @notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle')
117
+ registration_ids = unavailable_idxs.map { |i| @notification.registration_ids[i] }
118
+ Rapns::Daemon.store.create_gcm_notification(attrs, @notification.data,
119
+ registration_ids, deliver_after_header(response), @notification.app)
110
120
  end
111
121
 
112
122
  def deliver_after_header(response)
@@ -0,0 +1,74 @@
1
+ require 'active_record'
2
+
3
+ require 'rapns/daemon/store/active_record/reconnectable'
4
+
5
+ module Rapns
6
+ module Daemon
7
+ module Store
8
+ class ActiveRecord
9
+ include Reconnectable
10
+
11
+ def deliverable_notifications(apps)
12
+ with_database_reconnect_and_retry do
13
+ batch_size = Rapns.config.batch_size
14
+ relation = Rapns::Notification.ready_for_delivery.for_apps(apps)
15
+ relation = relation.limit(batch_size) unless Rapns.config.push
16
+ relation.all
17
+ end
18
+ end
19
+
20
+ def retry_after(notification, deliver_after)
21
+ with_database_reconnect_and_retry do
22
+ notification.retries += 1
23
+ notification.deliver_after = deliver_after
24
+ notification.save!(:validate => false)
25
+ end
26
+ end
27
+
28
+ def mark_delivered(notification)
29
+ with_database_reconnect_and_retry do
30
+ notification.delivered = true
31
+ notification.delivered_at = Time.now
32
+ notification.save!(:validate => false)
33
+ end
34
+ end
35
+
36
+ def mark_failed(notification, code, description)
37
+ with_database_reconnect_and_retry do
38
+ notification.delivered = false
39
+ notification.delivered_at = nil
40
+ notification.failed = true
41
+ notification.failed_at = Time.now
42
+ notification.error_code = code
43
+ notification.error_description = description
44
+ notification.save!(:validate => false)
45
+ end
46
+ end
47
+
48
+ def create_apns_feedback(failed_at, device_token, app)
49
+ with_database_reconnect_and_retry do
50
+ Rapns::Apns::Feedback.create!(:failed_at => failed_at,
51
+ :device_token => device_token, :app => app)
52
+ end
53
+ end
54
+
55
+ def create_gcm_notification(attrs, data, registration_ids, deliver_after, app)
56
+ with_database_reconnect_and_retry do
57
+ notification = Rapns::Gcm::Notification.new
58
+ notification.assign_attributes(attrs)
59
+ notification.data = data
60
+ notification.registration_ids = registration_ids
61
+ notification.deliver_after = deliver_after
62
+ notification.app = app
63
+ notification.save!
64
+ notification
65
+ end
66
+ end
67
+
68
+ def after_daemonize
69
+ reconnect_database
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,61 @@
1
+ class PGError < StandardError; end if !defined?(PGError)
2
+ class Mysql; class Error < StandardError; end; end if !defined?(Mysql)
3
+ module Mysql2; class Error < StandardError; end; end if !defined?(Mysql2)
4
+ module ActiveRecord; end
5
+ class ActiveRecord::JDBCError < StandardError; end if !defined?(::ActiveRecord::JDBCError)
6
+
7
+ module Rapns
8
+ module Daemon
9
+ module Store
10
+ class ActiveRecord
11
+ module Reconnectable
12
+ ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error,
13
+ Mysql2::Error, ::ActiveRecord::JDBCError]
14
+
15
+ def with_database_reconnect_and_retry
16
+ begin
17
+ ::ActiveRecord::Base.connection_pool.with_connection do
18
+ yield
19
+ end
20
+ rescue *ADAPTER_ERRORS => e
21
+ Rapns.logger.error(e)
22
+ database_connection_lost
23
+ retry
24
+ end
25
+ end
26
+
27
+ def database_connection_lost
28
+ Rapns.logger.warn("Lost connection to database, reconnecting...")
29
+ attempts = 0
30
+ loop do
31
+ begin
32
+ Rapns.logger.warn("Attempt #{attempts += 1}")
33
+ reconnect_database
34
+ check_database_is_connected
35
+ break
36
+ rescue *ADAPTER_ERRORS => e
37
+ Rapns.logger.error(e, :airbrake_notify => false)
38
+ sleep_to_avoid_thrashing
39
+ end
40
+ end
41
+ Rapns.logger.warn("Database reconnected")
42
+ end
43
+
44
+ def reconnect_database
45
+ ::ActiveRecord::Base.clear_all_connections!
46
+ ::ActiveRecord::Base.establish_connection
47
+ end
48
+
49
+ def check_database_is_connected
50
+ # Simply asking the adapter for the connection state is not sufficient.
51
+ Rapns::Notification.count
52
+ end
53
+
54
+ def sleep_to_avoid_thrashing
55
+ sleep 2
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end