rapns 0.2.3 → 1.0.0

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/README.md +35 -10
  2. data/lib/generators/rapns_generator.rb +9 -1
  3. data/lib/generators/templates/create_rapns_feedback.rb +15 -0
  4. data/lib/generators/templates/rapns.yml +22 -9
  5. data/lib/rapns/daemon/configuration.rb +32 -14
  6. data/lib/rapns/daemon/connection.rb +35 -50
  7. data/lib/rapns/daemon/delivery_handler.rb +65 -15
  8. data/lib/rapns/daemon/delivery_handler_pool.rb +1 -5
  9. data/lib/rapns/daemon/delivery_queue.rb +10 -27
  10. data/lib/rapns/daemon/feedback_receiver.rb +57 -0
  11. data/lib/rapns/daemon/feeder.rb +9 -7
  12. data/lib/rapns/daemon/interruptible_sleep.rb +14 -0
  13. data/lib/rapns/daemon/pool.rb +0 -5
  14. data/lib/rapns/daemon.rb +9 -9
  15. data/lib/rapns/device_token_format_validator.rb +10 -0
  16. data/lib/rapns/feedback.rb +10 -0
  17. data/lib/rapns/notification.rb +15 -1
  18. data/lib/rapns/version.rb +1 -1
  19. data/lib/rapns.rb +3 -1
  20. data/spec/rapns/daemon/certificate_spec.rb +6 -0
  21. data/spec/rapns/daemon/configuration_spec.rb +124 -40
  22. data/spec/rapns/daemon/connection_spec.rb +81 -129
  23. data/spec/rapns/daemon/delivery_handler_pool_spec.rb +1 -6
  24. data/spec/rapns/daemon/delivery_handler_spec.rb +117 -30
  25. data/spec/rapns/daemon/delivery_queue_spec.rb +29 -0
  26. data/spec/rapns/daemon/feedback_receiver_spec.rb +86 -0
  27. data/spec/rapns/daemon/feeder_spec.rb +25 -9
  28. data/spec/rapns/daemon/interruptible_sleep_spec.rb +32 -0
  29. data/spec/rapns/daemon/logger_spec.rb +34 -14
  30. data/spec/rapns/daemon_spec.rb +34 -31
  31. data/spec/rapns/feedback_spec.rb +12 -0
  32. data/spec/rapns/notification_spec.rb +5 -0
  33. data/spec/spec_helper.rb +5 -2
  34. metadata +16 -5
  35. data/lib/rapns/daemon/connection_pool.rb +0 -31
  36. data/spec/rapns/daemon/connection_pool_spec.rb +0 -40
@@ -0,0 +1,14 @@
1
+ module Rapns
2
+ module Daemon
3
+ module InterruptibleSleep
4
+ def interruptible_sleep(seconds)
5
+ @_sleep_check, @_sleep_interrupt = IO.pipe
6
+ IO.select([@_sleep_check], nil, nil, seconds)
7
+ end
8
+
9
+ def interrupt_sleep
10
+ @_sleep_interrupt.close if @_sleep_interrupt
11
+ end
12
+ end
13
+ end
14
+ end
@@ -15,8 +15,6 @@ module Rapns
15
15
  end
16
16
 
17
17
  def drain
18
- drain_started
19
-
20
18
  while !@queue.empty?
21
19
  object = @queue.pop
22
20
  object_removed_from_pool(object)
@@ -33,9 +31,6 @@ module Rapns
33
31
 
34
32
  def object_removed_from_pool(object)
35
33
  end
36
-
37
- def drain_started
38
- end
39
34
  end
40
35
  end
41
36
  end
data/lib/rapns/daemon.rb CHANGED
@@ -2,23 +2,24 @@ require 'thread'
2
2
  require 'socket'
3
3
  require 'pathname'
4
4
 
5
+ require 'rapns/daemon/interruptible_sleep'
5
6
  require 'rapns/daemon/configuration'
6
7
  require 'rapns/daemon/certificate'
7
8
  require 'rapns/daemon/delivery_error'
8
9
  require 'rapns/daemon/pool'
9
- require 'rapns/daemon/connection_pool'
10
10
  require 'rapns/daemon/connection'
11
11
  require 'rapns/daemon/delivery_queue'
12
12
  require 'rapns/daemon/delivery_handler'
13
13
  require 'rapns/daemon/delivery_handler_pool'
14
+ require 'rapns/daemon/feedback_receiver'
14
15
  require 'rapns/daemon/feeder'
15
16
  require 'rapns/daemon/logger'
16
17
 
17
18
  module Rapns
18
19
  module Daemon
19
20
  class << self
20
- attr_accessor :logger, :configuration, :certificate, :connection_pool, :delivery_queue,
21
- :delivery_handler_pool, :foreground
21
+ attr_accessor :logger, :configuration, :certificate,
22
+ :delivery_queue, :delivery_handler_pool, :foreground
22
23
  alias_method :foreground?, :foreground
23
24
  end
24
25
 
@@ -34,19 +35,18 @@ module Rapns
34
35
  self.certificate = Certificate.new(configuration.certificate)
35
36
  certificate.load
36
37
 
37
- self.delivery_queue = DeliveryQueue.new(configuration.connections)
38
+ self.delivery_queue = DeliveryQueue.new
38
39
 
39
40
  daemonize unless foreground?
40
41
 
41
42
  write_pid_file
42
43
 
43
- self.delivery_handler_pool = DeliveryHandlerPool.new(configuration.connections)
44
+ self.delivery_handler_pool = DeliveryHandlerPool.new(configuration.push.connections)
44
45
  delivery_handler_pool.populate
45
46
 
46
- self.connection_pool = ConnectionPool.new(configuration.connections)
47
- connection_pool.populate
48
-
49
47
  logger.info('Ready')
48
+
49
+ FeedbackReceiver.start
50
50
  Feeder.start(foreground?)
51
51
  end
52
52
 
@@ -70,9 +70,9 @@ module Rapns
70
70
 
71
71
  def self.shutdown
72
72
  puts "\nShutting down..."
73
+ Rapns::Daemon::FeedbackReceiver.stop
73
74
  Rapns::Daemon::Feeder.stop
74
75
  Rapns::Daemon.delivery_handler_pool.drain if Rapns::Daemon.delivery_handler_pool
75
- Rapns::Daemon.connection_pool.drain if Rapns::Daemon.connection_pool
76
76
  delete_pid_file
77
77
  end
78
78
 
@@ -0,0 +1,10 @@
1
+ module Rapns
2
+ class DeviceTokenFormatValidator < ActiveModel::Validator
3
+
4
+ def validate(record)
5
+ if record.device_token !~ /^[a-z0-9]{64}$/
6
+ record.errors[:device_token] << "is invalid"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Rapns
2
+ class Feedback < ActiveRecord::Base
3
+ set_table_name 'rapns_feedback'
4
+
5
+ validates :device_token, :presence => true
6
+ validates :failed_at, :presence => true
7
+
8
+ validates_with Rapns::DeviceTokenFormatValidator
9
+ end
10
+ end
@@ -2,10 +2,11 @@ module Rapns
2
2
  class Notification < ActiveRecord::Base
3
3
  set_table_name "rapns_notifications"
4
4
 
5
- validates :device_token, :presence => true, :format => { :with => /^[a-z0-9]{64}$/ }
5
+ validates :device_token, :presence => true
6
6
  validates :badge, :numericality => true, :allow_nil => true
7
7
  validates :expiry, :numericality => true, :presence => true
8
8
 
9
+ validates_with Rapns::DeviceTokenFormatValidator
9
10
  validates_with Rapns::BinaryNotificationValidator
10
11
 
11
12
  scope :ready_for_delivery, lambda { where(:delivered => false, :failed => false).merge(where("deliver_after IS NULL") | where("deliver_after < ?", Time.now)) }
@@ -14,6 +15,19 @@ module Rapns
14
15
  write_attribute(:device_token, token.delete(" <>")) if !token.nil?
15
16
  end
16
17
 
18
+ def alert=(alert)
19
+ if alert.is_a?(Hash)
20
+ write_attribute(:alert, ActiveSupport::JSON.encode(alert))
21
+ else
22
+ write_attribute(:alert, alert)
23
+ end
24
+ end
25
+
26
+ def alert
27
+ string_or_json = read_attribute(:alert)
28
+ ActiveSupport::JSON.decode(string_or_json) rescue string_or_json
29
+ end
30
+
17
31
  def attributes_for_device=(attrs)
18
32
  raise ArgumentError, "attributes_for_device must be a Hash" if !attrs.is_a?(Hash)
19
33
  write_attribute(:attributes_for_device, ActiveSupport::JSON.encode(attrs))
data/lib/rapns/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rapns
2
- VERSION = '0.2.3'
2
+ VERSION = '1.0.0'
3
3
  end
data/lib/rapns.rb CHANGED
@@ -2,4 +2,6 @@ require 'active_record'
2
2
 
3
3
  require 'rapns/version'
4
4
  require 'rapns/binary_notification_validator'
5
- require 'rapns/notification'
5
+ require 'rapns/device_token_format_validator'
6
+ require 'rapns/notification'
7
+ require 'rapns/feedback'
@@ -1,6 +1,12 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Rapns::Daemon::Certificate do
4
+ it 'reads the certificate from the given path' do
5
+ File.stub(:exists? => true)
6
+ File.should_receive(:read).with("/dir/development.pem")
7
+ cert = Rapns::Daemon::Certificate.new("/dir/development.pem")
8
+ cert.load
9
+ end
4
10
 
5
11
  it "should raise an error if the .pem file does not exist" do
6
12
  cert = Rapns::Daemon::Certificate.new("/tmp/rapns-missing.pem")
@@ -4,9 +4,46 @@ describe Rapns::Daemon::Configuration do
4
4
  module Rails
5
5
  end
6
6
 
7
+ let(:config) do
8
+ {
9
+ "airbrake_notify" => false,
10
+ "certificate" => "production.pem",
11
+ "certificate_password" => "abc123",
12
+ "pid_file" => "rapns.pid",
13
+ "push" => {
14
+ "port" => 123,
15
+ "host" => "localhost",
16
+ "poll" => 4,
17
+ "connections" => 6
18
+ },
19
+ "feedback" => {
20
+ "port" => 123,
21
+ "host" => "localhost",
22
+ "poll" => 30,
23
+ }
24
+ }
25
+ end
26
+
7
27
  before do
8
28
  Rails.stub(:root).and_return("/rails_root")
9
- @config = {"port" => 123, "host" => "localhost", "certificate" => "production.pem", "certificate_password" => "abc123", "airbrake_notify" => false, "poll" => 4, "connections" => 6, "pid_file" => "rapns.pid"}
29
+ end
30
+
31
+ it 'opens the config from the given path' do
32
+ YAML.stub(:load => {"production" => config})
33
+ fd = stub(:read => nil)
34
+ File.should_receive(:open).with("/tmp/rapns-non-existant-file").and_yield(fd)
35
+ config = Rapns::Daemon::Configuration.new("production", "/tmp/rapns-non-existant-file")
36
+ config.stub(:ensure_config_exists)
37
+ config.load
38
+ end
39
+
40
+ it 'reads the config as YAML' do
41
+ YAML.should_receive(:load).and_return({"production" => config})
42
+ fd = stub(:read => nil)
43
+ File.stub(:open).and_yield(fd)
44
+ config = Rapns::Daemon::Configuration.new("production", "/tmp/rapns-non-existant-file")
45
+ config.stub(:ensure_config_exists)
46
+ config.load
10
47
  end
11
48
 
12
49
  it "should raise an error if the configuration file does not exist" do
@@ -16,131 +53,178 @@ describe Rapns::Daemon::Configuration do
16
53
  it "should raise an error if the environment is not configured" do
17
54
  configuration = Rapns::Daemon::Configuration.new("development", "/some/config.yml")
18
55
  configuration.stub(:read_config).and_return({"production" => {}})
19
- expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "Configuration for environment 'development' not defined in /some/config.yml")
56
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "Configuration for environment 'development' not defined in /some/config.yml")
57
+ end
58
+
59
+ it "should raise an error if the push host is not configured" do
60
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
61
+ config["push"]["host"] = nil
62
+ configuration.stub(:read_config).and_return({"production" => config})
63
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'push.host' not defined for environment 'production' in /some/config.yml. You may need to run 'rails g rapns' after updating.")
20
64
  end
21
65
 
22
- it "should raise an error if the host is not configured" do
66
+ it "should raise an error if the push port is not configured" do
23
67
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
24
- configuration.stub(:read_config).and_return({"production" => @config.except("host")})
25
- expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'host' not defined for environment 'production' in /some/config.yml")
68
+ config["push"]["port"] = nil
69
+ configuration.stub(:read_config).and_return({"production" => config})
70
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'push.port' not defined for environment 'production' in /some/config.yml. You may need to run 'rails g rapns' after updating.")
26
71
  end
27
72
 
28
- it "should raise an error if the port is not configured" do
73
+ it "should raise an error if the feedback host is not configured" do
29
74
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
30
- configuration.stub(:read_config).and_return({"production" => @config.except("port")})
31
- expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'port' not defined for environment 'production' in /some/config.yml")
75
+ config["feedback"]["host"] = nil
76
+ configuration.stub(:read_config).and_return({"production" => config})
77
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'feedback.host' not defined for environment 'production' in /some/config.yml. You may need to run 'rails g rapns' after updating.")
78
+ end
79
+
80
+ it "should raise an error if the feedback port is not configured" do
81
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
82
+ config["feedback"]["port"] = nil
83
+ configuration.stub(:read_config).and_return({"production" => config})
84
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'feedback.port' not defined for environment 'production' in /some/config.yml. You may need to run 'rails g rapns' after updating.")
32
85
  end
33
86
 
34
87
  it "should raise an error if the certificate is not configured" do
35
88
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
36
- configuration.stub(:read_config).and_return({"production" => @config.except("certificate")})
37
- expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'certificate' not defined for environment 'production' in /some/config.yml")
89
+ configuration.stub(:read_config).and_return({"production" => config.except("certificate")})
90
+ expect { configuration.load }.to raise_error(Rapns::ConfigurationError, "'certificate' not defined for environment 'production' in /some/config.yml. You may need to run 'rails g rapns' after updating.")
91
+ end
92
+
93
+ it "should set the push host" do
94
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
95
+ configuration.stub(:read_config).and_return({"production" => config})
96
+ configuration.load
97
+ configuration.push.host.should == "localhost"
38
98
  end
39
99
 
40
- it "should set the host" do
100
+ it "should set the push port" do
41
101
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
42
- configuration.stub(:read_config).and_return({"production" => @config})
102
+ configuration.stub(:read_config).and_return({"production" => config})
43
103
  configuration.load
44
- configuration.host.should == "localhost"
104
+ configuration.push.port.should == 123
45
105
  end
46
106
 
47
- it "should set the port" do
107
+ it "should set the feedback port" do
48
108
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
49
- configuration.stub(:read_config).and_return({"production" => @config})
109
+ configuration.stub(:read_config).and_return({"production" => config})
50
110
  configuration.load
51
- configuration.port.should == 123
111
+ configuration.feedback.port.should == 123
112
+ end
113
+
114
+ it "should set the feedback host" do
115
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
116
+ configuration.stub(:read_config).and_return({"production" => config})
117
+ configuration.load
118
+ configuration.feedback.host.should == "localhost"
52
119
  end
53
120
 
54
121
  it "should set the airbrake notify flag" do
55
122
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
56
- configuration.stub(:read_config).and_return({"production" => @config})
123
+ configuration.stub(:read_config).and_return({"production" => config})
57
124
  configuration.load
58
125
  configuration.airbrake_notify?.should == false
59
126
  end
60
127
 
61
128
  it "should default the airbrake notify flag to true if not set" do
62
129
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
63
- configuration.stub(:read_config).and_return({"production" => @config.except("airbrake_notify")})
130
+ configuration.stub(:read_config).and_return({"production" => config.except("airbrake_notify")})
64
131
  configuration.load
65
132
  configuration.airbrake_notify?.should == true
66
133
  end
67
134
 
68
- it "should set the poll frequency" do
135
+ it "should set the push poll frequency" do
136
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
137
+ configuration.stub(:read_config).and_return({"production" => config})
138
+ configuration.load
139
+ configuration.push.poll.should == 4
140
+ end
141
+
142
+ it "should set the feedback poll frequency" do
143
+ configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
144
+ configuration.stub(:read_config).and_return({"production" => config})
145
+ configuration.load
146
+ configuration.feedback.poll.should == 30
147
+ end
148
+
149
+ it "should default the push poll frequency to 2 if not set" do
69
150
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
70
- configuration.stub(:read_config).and_return({"production" => @config})
151
+ config["push"]["poll"] = nil
152
+ configuration.stub(:read_config).and_return({"production" => config})
71
153
  configuration.load
72
- configuration.poll.should == 4
154
+ configuration.push.poll.should == 2
73
155
  end
74
156
 
75
- it "should default the poll frequency to 2 if not set" do
157
+ it "should default the feedback poll frequency to 60 if not set" do
76
158
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
77
- configuration.stub(:read_config).and_return({"production" => @config.except("poll")})
159
+ config["feedback"]["poll"] = nil
160
+ configuration.stub(:read_config).and_return({"production" => config})
78
161
  configuration.load
79
- configuration.poll.should == 2
162
+ configuration.feedback.poll.should == 60
80
163
  end
81
164
 
82
- it "should set the number of connections" do
165
+ it "should set the number of push connections" do
83
166
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
84
- configuration.stub(:read_config).and_return({"production" => @config})
167
+ configuration.stub(:read_config).and_return({"production" => config})
85
168
  configuration.load
86
- configuration.connections.should == 6
169
+ configuration.push.connections.should == 6
87
170
  end
88
171
 
89
- it "should default the number of connections to 3 if not set" do
172
+ it "should default the number of push connections to 3 if not set" do
90
173
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
91
- configuration.stub(:read_config).and_return({"production" => @config.except("connections")})
174
+ config["push"]["connections"] = nil
175
+ configuration.stub(:read_config).and_return({"production" => config})
92
176
  configuration.load
93
- configuration.connections.should == 3
177
+ configuration.push.connections.should == 3
94
178
  end
95
179
 
96
180
  it "should set the certificate password" do
97
181
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
98
- configuration.stub(:read_config).and_return({"production" => @config})
182
+ configuration.stub(:read_config).and_return({"production" => config})
99
183
  configuration.load
100
184
  configuration.certificate_password.should == "abc123"
101
185
  end
102
186
 
103
187
  it "should set the certificate password to a blank string if it is not configured" do
104
188
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
105
- configuration.stub(:read_config).and_return({"production" => @config.except("certificate_password")})
189
+ configuration.stub(:read_config).and_return({"production" => config.except("certificate_password")})
106
190
  configuration.load
107
191
  configuration.certificate_password.should == ""
108
192
  end
109
193
 
110
194
  it "should set the certificate, with absolute path" do
111
195
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
112
- configuration.stub(:read_config).and_return({"production" => @config})
196
+ configuration.stub(:read_config).and_return({"production" => config})
113
197
  configuration.load
114
198
  configuration.certificate.should == "/rails_root/config/rapns/production.pem"
115
199
  end
116
200
 
117
201
  it "should keep the absolute path of the certificate if it has one" do
118
- @config["certificate"] = "/different_path/to/production.pem"
202
+ config["certificate"] = "/different_path/to/production.pem"
119
203
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
120
- configuration.stub(:read_config).and_return({"production" => @config})
204
+ configuration.stub(:read_config).and_return({"production" => config})
121
205
  configuration.load
122
206
  configuration.certificate.should == "/different_path/to/production.pem"
123
207
  end
124
208
 
125
209
  it "should set the PID file path" do
126
210
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
127
- configuration.stub(:read_config).and_return({"production" => @config})
211
+ configuration.stub(:read_config).and_return({"production" => config})
128
212
  configuration.load
129
213
  configuration.pid_file.should == "/rails_root/rapns.pid"
130
214
  end
131
215
 
132
216
  it "should keep the absolute path of the PID file if it has one" do
133
- @config["pid_file"] = "/some/absolue/path/rapns.pid"
217
+ config["pid_file"] = "/some/absolue/path/rapns.pid"
134
218
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
135
- configuration.stub(:read_config).and_return({"production" => @config})
219
+ configuration.stub(:read_config).and_return({"production" => config})
136
220
  configuration.load
137
221
  configuration.pid_file.should == "/some/absolue/path/rapns.pid"
138
222
  end
139
223
 
140
224
  it "should return nil if no PID file was set" do
141
- @config["pid_file"] = ""
225
+ config["pid_file"] = ""
142
226
  configuration = Rapns::Daemon::Configuration.new("production", "/some/config.yml")
143
- configuration.stub(:read_config).and_return({"production" => @config})
227
+ configuration.stub(:read_config).and_return({"production" => config})
144
228
  configuration.load
145
229
  configuration.pid_file.should be_nil
146
230
  end