rapns 3.2.0 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -0
- data/README.md +9 -18
- data/lib/generators/templates/rapns.rb +5 -0
- data/lib/rapns.rb +4 -0
- data/lib/rapns/apns_feedback.rb +1 -0
- data/lib/rapns/configuration.rb +4 -3
- data/lib/rapns/daemon.rb +20 -4
- data/lib/rapns/daemon/apns/feedback_receiver.rb +9 -11
- data/lib/rapns/daemon/delivery.rb +3 -20
- data/lib/rapns/daemon/delivery_queue.rb +2 -2
- data/lib/rapns/daemon/feeder.rb +5 -10
- data/lib/rapns/daemon/gcm/delivery.rb +19 -9
- data/lib/rapns/daemon/store/active_record.rb +74 -0
- data/lib/rapns/daemon/store/active_record/reconnectable.rb +61 -0
- data/lib/rapns/gcm/notification.rb +5 -4
- data/lib/rapns/gcm/registration_ids_count_validator.rb +1 -1
- data/lib/rapns/logger.rb +6 -2
- data/lib/rapns/push.rb +1 -0
- data/lib/rapns/reflection.rb +1 -1
- data/lib/rapns/version.rb +1 -1
- data/spec/unit/apns/notification_spec.rb +2 -0
- data/spec/unit/apns_feedback_spec.rb +5 -0
- data/spec/unit/configuration_spec.rb +1 -1
- data/spec/unit/daemon/apns/delivery_spec.rb +7 -64
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +2 -2
- data/spec/unit/daemon/feeder_spec.rb +6 -58
- data/spec/unit/daemon/gcm/delivery_spec.rb +49 -57
- data/spec/unit/daemon/{database_reconnectable_spec.rb → store/active_record/reconnectable_spec.rb} +4 -3
- data/spec/unit/daemon/store/active_record_spec.rb +181 -0
- data/spec/unit/daemon_spec.rb +27 -7
- data/spec/unit/gcm/notification_spec.rb +2 -9
- data/spec/unit/push_spec.rb +5 -0
- data/spec/unit/reflection_spec.rb +0 -4
- data/spec/unit_spec_helper.rb +4 -1
- metadata +10 -7
- 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
data/lib/rapns/apns_feedback.rb
CHANGED
data/lib/rapns/configuration.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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
|
-
|
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 ||
|
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
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/rapns/daemon/feeder.rb
CHANGED
@@ -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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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 =
|
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
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|