rapns 1.0.1 → 1.0.2

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