rapns 3.2.0-java → 3.3.0-java

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/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 +28 -25
  36. data/lib/rapns/daemon/database_reconnectable.rb +0 -57
data/CHANGELOG.md CHANGED
@@ -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
data/lib/rapns.rb CHANGED
@@ -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
data/lib/rapns/daemon.rb CHANGED
@@ -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