rapns 0.2.3 → 1.0.0

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