rapns 1.0.1 → 1.0.2

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.
data/README.md CHANGED
@@ -123,6 +123,10 @@ It is your responsibility to avoid creating new notifications for devices that n
123
123
 
124
124
  After updating you should run `rails g rapns` to check for any new migrations or configuration changes.
125
125
 
126
+ ## Wiki
127
+
128
+ * [Why open multiple connections to the APNs?](https://github.com/ileitch/rapns/wiki/Why-open-multiple-connections-to-the-APNs%3F)
129
+
126
130
  ## Contributing to rapns
127
131
 
128
132
  Fork as usual and go crazy!
@@ -0,0 +1,52 @@
1
+ class PGError < StandardError; end if !defined?(PGError)
2
+ module Mysql; class Error < StandardError; end; end if !defined?(Mysql)
3
+ module Mysql2; class Error < StandardError; end; end if !defined?(Mysql2)
4
+
5
+ module Rapns
6
+ module Daemon
7
+ module DatabaseReconnectable
8
+ ADAPTER_ERRORS = [ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error]
9
+
10
+ def with_database_reconnect_and_retry
11
+ begin
12
+ yield
13
+ rescue *ADAPTER_ERRORS => e
14
+ Rapns::Daemon.logger.error(e)
15
+ database_connection_lost
16
+ retry
17
+ end
18
+ end
19
+
20
+ def database_connection_lost
21
+ Rapns::Daemon.logger.warn("[#{name}] Lost connection to database, reconnecting...")
22
+ attempts = 0
23
+ loop do
24
+ begin
25
+ Rapns::Daemon.logger.warn("[#{name}] Attempt #{attempts += 1}")
26
+ reconnect_database
27
+ check_database_is_connected
28
+ break
29
+ rescue *ADAPTER_ERRORS => e
30
+ Rapns::Daemon.logger.error(e, :airbrake_notify => false)
31
+ sleep_to_avoid_thrashing
32
+ end
33
+ end
34
+ Rapns::Daemon.logger.warn("[#{name}] Database reconnected")
35
+ end
36
+
37
+ def reconnect_database
38
+ ActiveRecord::Base.clear_all_connections!
39
+ ActiveRecord::Base.establish_connection
40
+ end
41
+
42
+ def check_database_is_connected
43
+ # Simply asking the adapter for the connection state is not sufficient.
44
+ Rapns::Notification.count
45
+ end
46
+
47
+ def sleep_to_avoid_thrashing
48
+ sleep 2
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,8 @@
1
1
  module Rapns
2
2
  module Daemon
3
3
  class DeliveryHandler
4
+ include DatabaseReconnectable
5
+
4
6
  STOP = 0x666
5
7
  SELECT_TIMEOUT = 0.5
6
8
  ERROR_TUPLE_BYTES = 6
@@ -16,6 +18,8 @@ module Rapns
16
18
  255 => "None (unknown error)"
17
19
  }
18
20
 
21
+ attr_reader :name
22
+
19
23
  def initialize(i)
20
24
  @name = "DeliveryHandler #{i}"
21
25
  host = Rapns::Daemon.configuration.push.host
@@ -46,9 +50,11 @@ module Rapns
46
50
  @connection.write(notification.to_binary)
47
51
  check_for_error
48
52
 
49
- notification.delivered = true
50
- notification.delivered_at = Time.now
51
- notification.save!(:validate => false)
53
+ with_database_reconnect_and_retry do
54
+ notification.delivered = true
55
+ notification.delivered_at = Time.now
56
+ notification.save!(:validate => false)
57
+ end
52
58
 
53
59
  Rapns::Daemon.logger.info("Notification #{notification.id} delivered to #{notification.device_token}")
54
60
  rescue Rapns::DeliveryError, Rapns::DisconnectionError => error
@@ -58,13 +64,15 @@ module Rapns
58
64
  end
59
65
 
60
66
  def handle_delivery_error(notification, error)
61
- notification.delivered = false
62
- notification.delivered_at = nil
63
- notification.failed = true
64
- notification.failed_at = Time.now
65
- notification.error_code = error.code
66
- notification.error_description = error.description
67
- notification.save!(:validate => false)
67
+ with_database_reconnect_and_retry do
68
+ notification.delivered = false
69
+ notification.delivered_at = nil
70
+ notification.failed = true
71
+ notification.failed_at = Time.now
72
+ notification.error_code = error.code
73
+ notification.error_description = error.description
74
+ notification.save!(:validate => false)
75
+ end
68
76
  end
69
77
 
70
78
  def check_for_error
@@ -1,16 +1,15 @@
1
- class PGError < StandardError; end if !defined?(PGError)
2
- module Mysql; class Error < StandardError; end; end if !defined?(Mysql)
3
- module Mysql2; class Error < StandardError; end; end if !defined?(Mysql2)
4
-
5
- ADAPTER_ERRORS = [PGError, Mysql::Error, Mysql2::Error]
6
-
7
1
  module Rapns
8
2
  module Daemon
9
3
  class Feeder
4
+ extend DatabaseReconnectable
10
5
  extend InterruptibleSleep
11
6
 
7
+ def self.name
8
+ "Feeder"
9
+ end
10
+
12
11
  def self.start(foreground)
13
- connect unless foreground
12
+ reconnect_database unless foreground
14
13
 
15
14
  loop do
16
15
  break if @stop
@@ -28,49 +27,17 @@ module Rapns
28
27
 
29
28
  def self.enqueue_notifications
30
29
  begin
31
- if Rapns::Daemon.delivery_queue.notifications_processed?
32
- Rapns::Notification.ready_for_delivery.each do |notification|
33
- Rapns::Daemon.delivery_queue.push(notification)
30
+ with_database_reconnect_and_retry do
31
+ if Rapns::Daemon.delivery_queue.notifications_processed?
32
+ Rapns::Notification.ready_for_delivery.each do |notification|
33
+ Rapns::Daemon.delivery_queue.push(notification)
34
+ end
34
35
  end
35
36
  end
36
- rescue ActiveRecord::StatementInvalid, *ADAPTER_ERRORS => e
37
- Rapns::Daemon.logger.error(e)
38
- reconnect
39
37
  rescue StandardError => e
40
38
  Rapns::Daemon.logger.error(e)
41
39
  end
42
40
  end
43
-
44
- def self.reconnect
45
- Rapns::Daemon.logger.warn('Lost connection to database, reconnecting...')
46
- attempts = 0
47
- loop do
48
- begin
49
- Rapns::Daemon.logger.warn("Attempt #{attempts += 1}")
50
- connect
51
- check_is_connected
52
- break
53
- rescue *ADAPTER_ERRORS => e
54
- Rapns::Daemon.logger.error(e, :airbrake_notify => false)
55
- sleep_to_avoid_thrashing
56
- end
57
- end
58
- Rapns::Daemon.logger.warn('Database reconnected')
59
- end
60
-
61
- def self.connect
62
- ActiveRecord::Base.clear_all_connections!
63
- ActiveRecord::Base.establish_connection
64
- end
65
-
66
- def self.check_is_connected
67
- # Simply asking the adapter for the connection state is not sufficient.
68
- Rapns::Notification.count
69
- end
70
-
71
- def self.sleep_to_avoid_thrashing
72
- sleep 2
73
- end
74
41
  end
75
42
  end
76
43
  end
@@ -39,9 +39,9 @@ module Rapns
39
39
  return unless @options[:airbrake_notify] == true
40
40
 
41
41
  if defined?(Airbrake)
42
- Airbrake.notify(e)
42
+ Airbrake.notify_or_ignore(e)
43
43
  elsif defined?(HoptoadNotifier)
44
- HoptoadNotifier.notify(e)
44
+ HoptoadNotifier.notify_or_ignore(e)
45
45
  end
46
46
  end
47
47
 
data/lib/rapns/daemon.rb CHANGED
@@ -9,6 +9,7 @@ require 'rapns/daemon/delivery_error'
9
9
  require 'rapns/daemon/disconnection_error'
10
10
  require 'rapns/daemon/pool'
11
11
  require 'rapns/daemon/connection'
12
+ require 'rapns/daemon/database_reconnectable'
12
13
  require 'rapns/daemon/delivery_queue'
13
14
  require 'rapns/daemon/delivery_handler'
14
15
  require 'rapns/daemon/delivery_handler_pool'
@@ -92,8 +93,12 @@ module Rapns
92
93
 
93
94
  def self.write_pid_file
94
95
  if !configuration.pid_file.blank?
95
- File.open(configuration.pid_file, 'w') do |f|
96
- f.puts $$
96
+ begin
97
+ File.open(configuration.pid_file, 'w') do |f|
98
+ f.puts $$
99
+ end
100
+ rescue SystemCallError => e
101
+ logger.error("Failed to write PID to '#{configuration.pid_file}': #{e.inspect}")
97
102
  end
98
103
  end
99
104
  end
data/lib/rapns/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rapns
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.2'
3
3
  end
@@ -0,0 +1,106 @@
1
+ require "spec_helper"
2
+
3
+ describe Rapns::Daemon::DatabaseReconnectable do
4
+ class TestDouble
5
+ include Rapns::Daemon::DatabaseReconnectable
6
+
7
+ attr_reader :name
8
+
9
+ def initialize(error, max_calls)
10
+ @name = 'TestDouble'
11
+ @error = error
12
+ @max_calls = max_calls
13
+ @calls = 0
14
+ end
15
+
16
+ def perform
17
+ with_database_reconnect_and_retry do
18
+ @calls += 1
19
+ raise @error if @calls <= @max_calls
20
+ end
21
+ end
22
+ end
23
+
24
+ let(:adapter_error_class) do
25
+ case $adapter
26
+ when 'postgresql'
27
+ PGError
28
+ when 'mysql'
29
+ Mysql::Error
30
+ when 'mysql2'
31
+ Mysql2::Error
32
+ else
33
+ raise "Please update #{__FILE__} for adapter #{$adapter}"
34
+ end
35
+ end
36
+ let(:error) { adapter_error_class.new("db down!") }
37
+ let(:test_double) { TestDouble.new(error, 1) }
38
+
39
+ before do
40
+ @logger = mock("Logger", :info => nil, :error => nil, :warn => nil)
41
+ Rapns::Daemon.stub(:logger).and_return(@logger)
42
+
43
+ ActiveRecord::Base.stub(:clear_all_connections!)
44
+ ActiveRecord::Base.stub(:establish_connection)
45
+ test_double.stub(:sleep)
46
+ end
47
+
48
+ it "should log the error raised" do
49
+ Rapns::Daemon.logger.should_receive(:error).with(error)
50
+ test_double.perform
51
+ end
52
+
53
+ it "should log that the database is being reconnected" do
54
+ Rapns::Daemon.logger.should_receive(:warn).with("[TestDouble] Lost connection to database, reconnecting...")
55
+ test_double.perform
56
+ end
57
+
58
+ it "should log the reconnection attempt" do
59
+ Rapns::Daemon.logger.should_receive(:warn).with("[TestDouble] Attempt 1")
60
+ test_double.perform
61
+ end
62
+
63
+ it "should clear all connections" do
64
+ ActiveRecord::Base.should_receive(:clear_all_connections!)
65
+ test_double.perform
66
+ end
67
+
68
+ it "should establish a new connection" do
69
+ ActiveRecord::Base.should_receive(:establish_connection)
70
+ test_double.perform
71
+ end
72
+
73
+ it "should test out the new connection by performing a count" do
74
+ Rapns::Notification.should_receive(:count)
75
+ test_double.perform
76
+ end
77
+
78
+ context "when the reconnection attempt is not successful" do
79
+ before do
80
+ class << Rapns::Notification
81
+ def count
82
+ @count_calls += 1
83
+ return if @count_calls == 2
84
+ raise @error
85
+ end
86
+ end
87
+ Rapns::Notification.instance_variable_set("@count_calls", 0)
88
+ Rapns::Notification.instance_variable_set("@error", error)
89
+ end
90
+
91
+ it "should log the 2nd attempt" do
92
+ Rapns::Daemon.logger.should_receive(:warn).with("[TestDouble] Attempt 2")
93
+ test_double.perform
94
+ end
95
+
96
+ it "should log errors raised when the reconnection is not successful without notifying airbrake" do
97
+ Rapns::Daemon.logger.should_receive(:error).with(error, :airbrake_notify => false)
98
+ test_double.perform
99
+ end
100
+
101
+ it "should sleep to avoid thrashing when the database is down" do
102
+ test_double.should_receive(:sleep).with(2)
103
+ test_double.perform
104
+ end
105
+ end
106
+ end
@@ -75,6 +75,11 @@ describe Rapns::Daemon::DeliveryHandler do
75
75
  delivery_handler.send(:handle_next_notification)
76
76
  end
77
77
 
78
+ it "should update notification with the ability to reconnect the database" do
79
+ delivery_handler.should_receive(:with_database_reconnect_and_retry)
80
+ delivery_handler.send(:handle_next_notification)
81
+ end
82
+
78
83
  it "should log if an error is raised when updating the notification" do
79
84
  e = StandardError.new("bork!")
80
85
  @notification.stub(:save!).and_raise(e)
@@ -92,6 +97,11 @@ describe Rapns::Daemon::DeliveryHandler do
92
97
  @connection.stub(:select => true, :read => [8, 4, 69].pack("ccN"), :reconnect => nil)
93
98
  end
94
99
 
100
+ it "should update notification with the ability to reconnect the database" do
101
+ delivery_handler.should_receive(:with_database_reconnect_and_retry)
102
+ delivery_handler.send(:handle_next_notification)
103
+ end
104
+
95
105
  it "should set the notification as not delivered" do
96
106
  @notification.should_receive(:delivered=).with(false)
97
107
  delivery_handler.send(:handle_next_notification)
@@ -15,13 +15,18 @@ describe Rapns::Daemon::Feeder do
15
15
 
16
16
  it "should reconnect to the database when daemonized" do
17
17
  Rapns::Daemon::Feeder.stub(:loop)
18
- ActiveRecord::Base.should_receive(:establish_connection)
18
+ Rapns::Daemon::Feeder.should_receive(:reconnect_database)
19
19
  Rapns::Daemon::Feeder.start(false)
20
20
  end
21
21
 
22
+ it "should check for new notifications with the ability to reconnect the database" do
23
+ Rapns::Daemon::Feeder.should_receive(:with_database_reconnect_and_retry)
24
+ Rapns::Daemon::Feeder.enqueue_notifications
25
+ end
26
+
22
27
  it "should not reconnect to the database when running in the foreground" do
23
28
  Rapns::Daemon::Feeder.stub(:loop)
24
- ActiveRecord::Base.should_not_receive(:establish_connection)
29
+ Rapns::Daemon::Feeder.should_not_receive(:reconnect_database)
25
30
  Rapns::Daemon::Feeder.start(true)
26
31
  end
27
32
 
@@ -91,82 +96,4 @@ describe Rapns::Daemon::Feeder do
91
96
  Rapns::Daemon::Feeder.stub(:loop).and_yield
92
97
  Rapns::Daemon::Feeder.start(true)
93
98
  end
94
-
95
- context "when the database connection is lost" do
96
- let(:error) { adapter_error.new("db down!") }
97
- before do
98
- ActiveRecord::Base.stub(:clear_all_connections!)
99
- ActiveRecord::Base.stub(:establish_connection)
100
- Rapns::Notification.stub(:ready_for_delivery).and_raise(error)
101
- end
102
-
103
- def adapter_error
104
- case $adapter
105
- when 'postgresql'
106
- PGError
107
- when 'mysql'
108
- Mysql::Error
109
- when 'mysql2'
110
- Mysql2::Error
111
- else
112
- raise "Please update #{__FILE__} for adapter #{$adapter}"
113
- end
114
- end
115
-
116
- it "should log the error raised" do
117
- Rapns::Daemon.logger.should_receive(:error).with(error)
118
- Rapns::Daemon::Feeder.enqueue_notifications
119
- end
120
-
121
- it "should log that the database is being reconnected" do
122
- Rapns::Daemon.logger.should_receive(:warn).with("Lost connection to database, reconnecting...")
123
- Rapns::Daemon::Feeder.enqueue_notifications
124
- end
125
-
126
- it "should log the reconnection attempt" do
127
- Rapns::Daemon.logger.should_receive(:warn).with("Attempt 1")
128
- Rapns::Daemon::Feeder.enqueue_notifications
129
- end
130
-
131
- it "should clear all connections" do
132
- ActiveRecord::Base.should_receive(:clear_all_connections!)
133
- Rapns::Daemon::Feeder.enqueue_notifications
134
- end
135
-
136
- it "should establish a new connection" do
137
- ActiveRecord::Base.should_receive(:establish_connection)
138
- Rapns::Daemon::Feeder.enqueue_notifications
139
- end
140
-
141
- it "should test out the new connection by performing a count" do
142
- Rapns::Notification.should_receive(:count)
143
- Rapns::Daemon::Feeder.enqueue_notifications
144
- end
145
-
146
- context "when the reconnection attempt is not successful" do
147
- let(:error) { adapter_error.new("shit got real") }
148
-
149
- before do
150
- class << Rapns::Notification
151
- def count
152
- @count_calls += 1
153
- return if @count_calls == 2
154
- raise @error
155
- end
156
- end
157
- Rapns::Notification.instance_variable_set("@count_calls", 0)
158
- Rapns::Notification.instance_variable_set("@error", error)
159
- end
160
-
161
- it "should log errors raised when the reconnection is not successful without notifying airbrake" do
162
- Rapns::Daemon.logger.should_receive(:error).with(error, :airbrake_notify => false)
163
- Rapns::Daemon::Feeder.enqueue_notifications
164
- end
165
-
166
- it "should sleep to avoid thrashing when the database is down" do
167
- Rapns::Daemon::Feeder.should_receive(:sleep).with(2)
168
- Rapns::Daemon::Feeder.enqueue_notifications
169
- end
170
- end
171
- end
172
99
  end
@@ -77,7 +77,7 @@ describe Rapns::Daemon::Logger do
77
77
  it "should notify Airbrake of the exception" do
78
78
  e = RuntimeError.new("hi mom")
79
79
  logger = Rapns::Daemon::Logger.new(:foreground => false, :airbrake_notify => true)
80
- Airbrake.should_receive(:notify).with(e)
80
+ Airbrake.should_receive(:notify_or_ignore).with(e)
81
81
  logger.error(e)
82
82
  end
83
83
 
@@ -96,7 +96,7 @@ describe Rapns::Daemon::Logger do
96
96
  it "should notify using HoptoadNotifier" do
97
97
  e = RuntimeError.new("hi mom")
98
98
  logger = Rapns::Daemon::Logger.new(:foreground => false, :airbrake_notify => true)
99
- HoptoadNotifier.should_receive(:notify).with(e)
99
+ HoptoadNotifier.should_receive(:notify_or_ignore).with(e)
100
100
  logger.error(e)
101
101
  end
102
102
  end
@@ -104,20 +104,20 @@ describe Rapns::Daemon::Logger do
104
104
  it "should not notify Airbrake of the exception if the airbrake_notify option is false" do
105
105
  e = RuntimeError.new("hi mom")
106
106
  logger = Rapns::Daemon::Logger.new(:foreground => false, :airbrake_notify => false)
107
- Airbrake.should_not_receive(:notify).with(e)
107
+ Airbrake.should_not_receive(:notify_or_ignore).with(e)
108
108
  logger.error(e)
109
109
  end
110
110
 
111
111
  it "should not notify Airbrake if explicitly disabled in the call to error" do
112
112
  e = RuntimeError.new("hi mom")
113
113
  logger = Rapns::Daemon::Logger.new(:foreground => false, :airbrake_notify => true)
114
- Airbrake.should_not_receive(:notify).with(e)
114
+ Airbrake.should_not_receive(:notify_or_ignore).with(e)
115
115
  logger.error(e, :airbrake_notify => false)
116
116
  end
117
117
 
118
118
  it "should not attempt to notify Airbrake of the error is not an Exception" do
119
119
  logger = Rapns::Daemon::Logger.new(:foreground => false)
120
- Airbrake.should_not_receive(:notify)
120
+ Airbrake.should_not_receive(:notify_or_ignore)
121
121
  logger.error("string error message")
122
122
  end
123
123
 
@@ -42,8 +42,8 @@ describe Rapns::Daemon, "when starting" do
42
42
  Rapns::Daemon::FeedbackReceiver.stub(:start)
43
43
  Rapns::Daemon::Feeder.stub(:start)
44
44
  Rapns::Daemon.stub(:daemonize)
45
- Rapns::Daemon.stub(:write_pid_file)
46
- @logger = mock("Logger", :info => nil)
45
+ File.stub(:open)
46
+ @logger = mock("Logger", :info => nil, :error => nil)
47
47
  Rapns::Daemon::Logger.stub(:new).and_return(@logger)
48
48
  end
49
49
 
@@ -102,6 +102,12 @@ describe Rapns::Daemon, "when starting" do
102
102
  Rapns::Daemon.start("development", {})
103
103
  end
104
104
 
105
+ it "should log an error if the PID file could not be written" do
106
+ File.stub(:open).and_raise(Errno::ENOENT)
107
+ @logger.should_receive(:error).with("Failed to write PID to '/rails_root/rapns.pid': #<Errno::ENOENT: No such file or directory>")
108
+ Rapns::Daemon.start("development", {})
109
+ end
110
+
105
111
  it "should start the feedback receiver" do
106
112
  Rapns::Daemon::FeedbackReceiver.should_receive(:start)
107
113
  Rapns::Daemon.start("development", true)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rapns
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-12 00:00:00.000000000 Z
12
+ date: 2011-12-23 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Easy to use library for Apple's Push Notification Service with Rails
15
15
  3
@@ -30,6 +30,7 @@ files:
30
30
  - lib/rapns/daemon/certificate.rb
31
31
  - lib/rapns/daemon/configuration.rb
32
32
  - lib/rapns/daemon/connection.rb
33
+ - lib/rapns/daemon/database_reconnectable.rb
33
34
  - lib/rapns/daemon/delivery_error.rb
34
35
  - lib/rapns/daemon/delivery_handler.rb
35
36
  - lib/rapns/daemon/delivery_handler_pool.rb
@@ -51,6 +52,7 @@ files:
51
52
  - spec/rapns/daemon/certificate_spec.rb
52
53
  - spec/rapns/daemon/configuration_spec.rb
53
54
  - spec/rapns/daemon/connection_spec.rb
55
+ - spec/rapns/daemon/database_reconnectable_spec.rb
54
56
  - spec/rapns/daemon/delivery_error_spec.rb
55
57
  - spec/rapns/daemon/delivery_handler_pool_spec.rb
56
58
  - spec/rapns/daemon/delivery_handler_spec.rb
@@ -92,6 +94,7 @@ test_files:
92
94
  - spec/rapns/daemon/certificate_spec.rb
93
95
  - spec/rapns/daemon/configuration_spec.rb
94
96
  - spec/rapns/daemon/connection_spec.rb
97
+ - spec/rapns/daemon/database_reconnectable_spec.rb
95
98
  - spec/rapns/daemon/delivery_error_spec.rb
96
99
  - spec/rapns/daemon/delivery_handler_pool_spec.rb
97
100
  - spec/rapns/daemon/delivery_handler_spec.rb