rapns 3.0.0.beta.1 → 3.0.0.rc.1

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 +75 -106
  2. data/bin/rapns +14 -14
  3. data/config/database.yml +8 -0
  4. data/lib/generators/rapns_generator.rb +5 -1
  5. data/lib/generators/templates/add_gcm.rb +8 -0
  6. data/lib/generators/templates/rapns.rb +39 -0
  7. data/lib/rapns.rb +1 -0
  8. data/lib/rapns/apns/notification.rb +2 -0
  9. data/lib/rapns/apns/required_fields_validator.rb +14 -0
  10. data/lib/rapns/configuration.rb +24 -33
  11. data/lib/rapns/daemon.rb +18 -14
  12. data/lib/rapns/daemon/apns/app_runner.rb +1 -1
  13. data/lib/rapns/daemon/apns/delivery.rb +1 -1
  14. data/lib/rapns/daemon/apns/feedback_receiver.rb +1 -1
  15. data/lib/rapns/daemon/app_runner.rb +13 -7
  16. data/lib/rapns/daemon/delivery_handler.rb +0 -4
  17. data/lib/rapns/daemon/feeder.rb +1 -5
  18. data/lib/rapns/daemon/logger.rb +22 -9
  19. data/lib/rapns/version.rb +1 -1
  20. data/lib/tasks/cane.rake +2 -3
  21. data/lib/tasks/test.rake +1 -2
  22. data/spec/unit/apns/notification_spec.rb +12 -2
  23. data/spec/unit/configuration_spec.rb +38 -0
  24. data/spec/unit/daemon/apns/app_runner_spec.rb +2 -0
  25. data/spec/unit/daemon/apns/delivery_spec.rb +10 -4
  26. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  27. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +7 -7
  28. data/spec/unit/daemon/app_runner_spec.rb +51 -0
  29. data/spec/unit/daemon/database_reconnectable_spec.rb +2 -0
  30. data/spec/unit/daemon/delivery_error_spec.rb +2 -2
  31. data/spec/unit/daemon/delivery_handler_shared.rb +10 -1
  32. data/spec/unit/daemon/gcm/app_runner_spec.rb +3 -1
  33. data/spec/unit/daemon/logger_spec.rb +10 -2
  34. data/spec/unit/daemon_spec.rb +31 -14
  35. data/spec/unit_spec_helper.rb +3 -7
  36. metadata +10 -4
@@ -31,15 +31,15 @@ module Rapns
31
31
  extend DatabaseReconnectable
32
32
 
33
33
  class << self
34
- attr_accessor :logger, :config
34
+ attr_accessor :logger
35
35
  end
36
36
 
37
- def self.start(config)
38
- self.config = config
39
- self.logger = Logger.new(:foreground => config.foreground, :airbrake_notify => config.airbrake_notify)
37
+ def self.start
38
+ self.logger = Logger.new(:foreground => Rapns.config.foreground,
39
+ :airbrake_notify => Rapns.config.airbrake_notify)
40
40
  setup_signal_hooks
41
41
 
42
- unless config.foreground
42
+ unless Rapns.config.foreground
43
43
  daemonize
44
44
  reconnect_database
45
45
  end
@@ -47,7 +47,7 @@ module Rapns
47
47
  write_pid_file
48
48
  ensure_upgraded
49
49
  AppRunner.sync
50
- Feeder.start(config.push_poll)
50
+ Feeder.start(Rapns.config.push_poll)
51
51
  end
52
52
 
53
53
  protected
@@ -61,18 +61,22 @@ module Rapns
61
61
  puts "!!!! RAPNS NOT STARTED !!!!"
62
62
  puts
63
63
  puts "As of version v2.0.0 apps are configured in the database instead of rapns.yml."
64
- puts "Please run 'rails g rapns' to generate the new migrations and create your apps with Rapns::App."
64
+ puts "Please run 'rails g rapns' to generate the new migrations and create your app."
65
65
  puts "See https://github.com/ileitch/rapns for further instructions."
66
66
  puts
67
67
  exit 1
68
68
  end
69
69
 
70
70
  if count == 0
71
- logger.warn("You have not created an Rapns::App yet. See https://github.com/ileitch/rapns for instructions.")
71
+ logger.warn("You have not created an app yet. See https://github.com/ileitch/rapns for instructions.")
72
72
  end
73
73
 
74
74
  if File.exists?(File.join(Rails.root, 'config', 'rapns', 'rapns.yml'))
75
- logger.warn("Since 2.0.0 rapns uses command-line options instead of a configuration file. Please remove config/rapns/rapns.yml.")
75
+ logger.warn(<<-EOS)
76
+ Since 2.0.0 rapns uses command-line options and a Ruby based configuration file.
77
+ Please run 'rails g rapns' to generate a new configuration file into config/initializers.
78
+ Remove config/rapns/rapns.yml to avoid this warning.
79
+ EOS
76
80
  end
77
81
  end
78
82
 
@@ -80,7 +84,7 @@ module Rapns
80
84
  @shutting_down = false
81
85
 
82
86
  Signal.trap('SIGHUP') { AppRunner.sync }
83
- Signal.trap('SIGUSR1') { AppRunner.debug }
87
+ Signal.trap('SIGUSR2') { AppRunner.debug }
84
88
 
85
89
  ['SIGINT', 'SIGTERM'].each do |signal|
86
90
  Signal.trap(signal) { handle_shutdown_signal }
@@ -101,17 +105,17 @@ module Rapns
101
105
  end
102
106
 
103
107
  def self.write_pid_file
104
- if !config.pid_file.blank?
108
+ if !Rapns.config.pid_file.blank?
105
109
  begin
106
- File.open(config.pid_file, 'w') { |f| f.puts Process.pid }
110
+ File.open(Rapns.config.pid_file, 'w') { |f| f.puts Process.pid }
107
111
  rescue SystemCallError => e
108
- logger.error("Failed to write PID to '#{config.pid_file}': #{e.inspect}")
112
+ logger.error("Failed to write PID to '#{Rapns.config.pid_file}': #{e.inspect}")
109
113
  end
110
114
  end
111
115
  end
112
116
 
113
117
  def self.delete_pid_file
114
- pid_file = config.pid_file
118
+ pid_file = Rapns.config.pid_file
115
119
  File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
116
120
  end
117
121
 
@@ -16,7 +16,7 @@ module Rapns
16
16
  protected
17
17
 
18
18
  def started
19
- poll = Rapns::Daemon.config[:feedback_poll]
19
+ poll = Rapns.config[:feedback_poll]
20
20
  host, port = ENVIRONMENTS[app.environment.to_sym][:feedback]
21
21
  @feedback_receiver = FeedbackReceiver.new(app, host, port, poll)
22
22
  @feedback_receiver.start
@@ -25,7 +25,7 @@ module Rapns
25
25
  def perform
26
26
  begin
27
27
  @connection.write(@notification.to_binary)
28
- check_for_error if Rapns::Daemon.config.check_for_errors
28
+ check_for_error if Rapns.config.check_for_errors
29
29
  mark_delivered
30
30
  Rapns::Daemon.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.device_token}")
31
31
  rescue Rapns::DeliveryError, Rapns::Apns::DisconnectionError => error
@@ -62,7 +62,7 @@ module Rapns
62
62
  Rapns::Daemon.logger.info("[FeedbackReceiver:#{@app.name}] Delivery failed at #{formatted_failed_at} for #{device_token}")
63
63
  feedback = Rapns::Apns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
64
64
  begin
65
- Rapns.configuration.feedback_callback.call(feedback) if Rapns.configuration.feedback_callback
65
+ Rapns.config.apns_feedback_callback.call(feedback) if Rapns.config.apns_feedback_callback
66
66
  rescue StandardError => e
67
67
  Rapns::Daemon.logger.error(e)
68
68
  end
@@ -8,7 +8,7 @@ module Rapns
8
8
  @runners = {}
9
9
 
10
10
  def self.enqueue(notification)
11
- if app = @runners[notification.app_id]
11
+ if app = runners[notification.app_id]
12
12
  app.enqueue(notification)
13
13
  else
14
14
  Rapns::Daemon.logger.error("No such app '#{notification.app_id}' for notification #{notification.id}.")
@@ -43,11 +43,11 @@ module Rapns
43
43
  end
44
44
 
45
45
  def self.stop
46
- @runners.values.map(&:stop)
46
+ runners.values.map(&:stop)
47
47
  end
48
48
 
49
49
  def self.debug
50
- @runners.values.map(&:debug)
50
+ runners.values.map(&:debug)
51
51
  end
52
52
 
53
53
  def self.idle
@@ -60,10 +60,6 @@ module Rapns
60
60
  @app = app
61
61
  end
62
62
 
63
- def new_delivery_handler
64
- raise NotImplementedError
65
- end
66
-
67
63
  def started
68
64
  end
69
65
 
@@ -73,6 +69,7 @@ module Rapns
73
69
  def start
74
70
  app.connections.times { handlers << start_handler }
75
71
  started
72
+ Rapns::Daemon.logger.info("[#{app.name}] Started, #{handlers_str}.")
76
73
  end
77
74
 
78
75
  def stop
@@ -87,10 +84,13 @@ module Rapns
87
84
  def sync(app)
88
85
  @app = app
89
86
  diff = handlers.size - app.connections
87
+ return if diff == 0
90
88
  if diff > 0
91
89
  diff.times { handlers.pop.stop }
90
+ Rapns::Daemon.logger.info("[#{app.name}] Terminated #{handlers_str(diff)}. #{handlers_str} remaining.")
92
91
  else
93
92
  diff.abs.times { handlers << start_handler }
93
+ Rapns::Daemon.logger.info("[#{app.name}] Added #{handlers_str(diff)}. #{handlers_str} remaining.")
94
94
  end
95
95
  end
96
96
 
@@ -124,6 +124,12 @@ module Rapns
124
124
  def handlers
125
125
  @handler ||= []
126
126
  end
127
+
128
+ def handlers_str(count = app.connections)
129
+ count = count.abs
130
+ str = count == 1 ? 'handler' : 'handlers'
131
+ "#{count} #{str}"
132
+ end
127
133
  end
128
134
  end
129
135
  end
@@ -3,10 +3,6 @@ module Rapns
3
3
  class DeliveryHandler
4
4
  attr_accessor :queue
5
5
 
6
- def deliver(notification)
7
- raise NotImplementedError
8
- end
9
-
10
6
  def start
11
7
  @thread = Thread.new do
12
8
  loop do
@@ -4,10 +4,6 @@ module Rapns
4
4
  extend InterruptibleSleep
5
5
  extend DatabaseReconnectable
6
6
 
7
- def self.name
8
- 'Feeder'
9
- end
10
-
11
7
  def self.start(poll)
12
8
  loop do
13
9
  enqueue_notifications
@@ -26,7 +22,7 @@ module Rapns
26
22
  def self.enqueue_notifications
27
23
  begin
28
24
  with_database_reconnect_and_retry do
29
- batch_size = Rapns::Daemon.config.batch_size
25
+ batch_size = Rapns.config.batch_size
30
26
  idle = Rapns::Daemon::AppRunner.idle.map(&:app)
31
27
  Rapns::Notification.ready_for_delivery.for_apps(idle).limit(batch_size).each do |notification|
32
28
  Rapns::Daemon::AppRunner.enqueue(notification)
@@ -3,10 +3,17 @@ module Rapns
3
3
  class Logger
4
4
  def initialize(options)
5
5
  @options = options
6
- log = File.open(File.join(Rails.root, 'log', 'rapns.log'), 'a')
7
- log.sync = true
8
- @logger = ActiveSupport::BufferedLogger.new(log, Rails.logger.level)
9
- @logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
6
+
7
+ begin
8
+ log = File.open(File.join(Rails.root, 'log', 'rapns.log'), 'a')
9
+ log.sync = true
10
+ @logger = ActiveSupport::BufferedLogger.new(log, Rails.logger.level)
11
+ @logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
12
+ rescue Errno::ENOENT, Errno::EPERM => e
13
+ @logger = nil
14
+ error(e)
15
+ error('Logging disabled.')
16
+ end
10
17
  end
11
18
 
12
19
  def info(msg)
@@ -15,16 +22,16 @@ module Rapns
15
22
 
16
23
  def error(msg, options = {})
17
24
  airbrake_notify(msg) if notify_via_airbrake?(msg, options)
18
- log(:error, msg, 'ERROR')
25
+ log(:error, msg, 'ERROR', STDERR)
19
26
  end
20
27
 
21
28
  def warn(msg)
22
- log(:warn, msg, 'WARNING')
29
+ log(:warn, msg, 'WARNING', STDERR)
23
30
  end
24
31
 
25
32
  private
26
33
 
27
- def log(where, msg, prefix = nil)
34
+ def log(where, msg, prefix = nil, io = STDOUT)
28
35
  if msg.is_a?(Exception)
29
36
  formatted_backtrace = msg.backtrace.join("\n")
30
37
  msg = "#{msg.class.name}, #{msg.message}\n#{formatted_backtrace}"
@@ -33,8 +40,14 @@ module Rapns
33
40
  formatted_msg = "[#{Time.now.to_s(:db)}] "
34
41
  formatted_msg << "[#{prefix}] " if prefix
35
42
  formatted_msg << msg
36
- puts formatted_msg if @options[:foreground]
37
- @logger.send(where, formatted_msg)
43
+
44
+ if io == STDERR
45
+ io.puts formatted_msg
46
+ elsif @options[:foreground]
47
+ io.puts formatted_msg
48
+ end
49
+
50
+ @logger.send(where, formatted_msg) if @logger
38
51
  end
39
52
 
40
53
  def airbrake_notify(e)
@@ -1,3 +1,3 @@
1
1
  module Rapns
2
- VERSION = '3.0.0.beta.1'
2
+ VERSION = '3.0.0.rc.1'
3
3
  end
@@ -3,12 +3,11 @@ begin
3
3
 
4
4
  desc "Run cane to check quality metrics"
5
5
  Cane::RakeTask.new(:quality) do |cane|
6
- cane.add_threshold 'coverage/covered_percent', :>=, 97
6
+ cane.add_threshold 'coverage/covered_percent', :>=, 99
7
7
  cane.no_style = false
8
8
  cane.style_measure = 1000
9
9
  cane.no_doc = true
10
- cane.abc_max = 15
11
- cane.abc_exclude = %w(Rapns::Daemon::Gcm::Delivery#handle_errors)
10
+ cane.abc_max = 20
12
11
  end
13
12
 
14
13
  namespace :spec do
@@ -16,12 +16,11 @@ namespace :test do
16
16
  pwd = Dir.pwd
17
17
 
18
18
  cmd("bundle exec rails new #{path} --skip-bundle")
19
- branch = cmd("git branch | grep '\*'").split(' ').last
20
19
 
21
20
  begin
22
21
  Dir.chdir(path)
23
22
  cmd('echo "gem \'rake\'" >> Gemfile')
24
- cmd("echo \"gem 'rapns', :git => '#{rapns_root}', :branch => '#{branch}'\" >> Gemfile")
23
+ cmd("echo \"gem 'rapns', :path => '#{rapns_root}'\" >> Gemfile")
25
24
  cmd('bundle install')
26
25
  cmd('bundle exec rails g rapns')
27
26
  cmd('bundle exec rake db:migrate')
@@ -25,13 +25,23 @@ describe Rapns::Apns::Notification do
25
25
  notification.errors[:base].include?("APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes.").should be_true
26
26
  end
27
27
 
28
- it "should default the sound to 1.aiff" do
29
- notification.sound.should == "1.aiff"
28
+ it "should default the sound to 'default'" do
29
+ notification.sound.should eq('default')
30
30
  end
31
31
 
32
32
  it "should default the expiry to 1 day" do
33
33
  notification.expiry.should == 1.day.to_i
34
34
  end
35
+
36
+ # The notification must contain one of alert, sound or badge.
37
+ # @see https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html
38
+ it "should not be valid if there is none of alert,sound,badge present" do
39
+ notification.alert = nil
40
+ notification.sound = nil
41
+ notification.badge = nil
42
+ notification.valid?.should be_false
43
+ notification.errors[:base].should include("APN Notification must contain one of alert, badge, or sound")
44
+ end
35
45
  end
36
46
 
37
47
  describe Rapns::Apns::Notification, "when assigning the device token" do
@@ -0,0 +1,38 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rapns do
4
+ let(:config) { stub }
5
+
6
+ before { Rapns.stub(:config => config) }
7
+
8
+ it 'can yields a config block' do
9
+ expect { |b| Rapns.configure(&b) }.to yield_with_args(config)
10
+ end
11
+ end
12
+
13
+ describe Rapns::Configuration do
14
+ let(:config) { Rapns::Configuration.new }
15
+
16
+ it 'configures a feedback callback' do
17
+ b = Proc.new {}
18
+ config.on_apns_feedback(&b)
19
+ config.apns_feedback_callback.should == b
20
+ end
21
+
22
+ it 'can be updated' do
23
+ new_config = Rapns::Configuration.new
24
+ new_config.batch_size = 100
25
+ expect { config.update(new_config) }.to change(config, :batch_size).to(100)
26
+ end
27
+
28
+ it 'sets the pid_file relative if not absolute' do
29
+ Rails.stub(:root => '/rails')
30
+ config.pid_file = 'tmp/rapns.pid'
31
+ config.pid_file.should == '/rails/tmp/rapns.pid'
32
+ end
33
+
34
+ it 'does not alter an absolute pid_file path' do
35
+ config.pid_file = '/tmp/rapns.pid'
36
+ config.pid_file.should == '/tmp/rapns.pid'
37
+ end
38
+ end
@@ -11,8 +11,10 @@ describe Rapns::Daemon::Apns::AppRunner do
11
11
  let(:handler) { stub(:start => nil, :stop => nil, :queue= => nil) }
12
12
  let(:receiver) { stub(:start => nil, :stop => nil) }
13
13
  let(:config) { {:feedback_poll => 60 } }
14
+ let(:logger) { stub(:info => nil) }
14
15
 
15
16
  before do
17
+ Rapns::Daemon.stub(:logger => logger)
16
18
  Rapns::Daemon::Apns::DeliveryHandler.stub(:new => handler)
17
19
  Rapns::Daemon::Apns::FeedbackReceiver.stub(:new => receiver)
18
20
  Rapns::Daemon.stub(:config => config)
@@ -16,7 +16,8 @@ describe Rapns::Daemon::Apns::Delivery do
16
16
  end
17
17
 
18
18
  before do
19
- Rapns::Daemon.stub(:logger => logger, :config => config)
19
+ Rapns.stub(:config => config)
20
+ Rapns::Daemon.stub(:logger => logger)
20
21
  end
21
22
 
22
23
  it "sends the binary version of the notification" do
@@ -95,9 +96,14 @@ describe Rapns::Daemon::Apns::Delivery do
95
96
  end
96
97
 
97
98
  it "logs the delivery error" do
98
- error = Rapns::DeliveryError.new(4, 12, "Missing payload")
99
- Rapns::DeliveryError.stub(:new => error)
100
- expect { delivery.perform }.to raise_error(error)
99
+ # checking for the stubbed error doesn't work in jruby, but checking
100
+ # for the exception by class does.
101
+
102
+ #error = Rapns::DeliveryError.new(4, 12, "Missing payload")
103
+ #Rapns::DeliveryError.stub(:new => error)
104
+ #expect { delivery.perform }.to raise_error(error)
105
+
106
+ expect { delivery.perform }.to raise_error(Rapns::DeliveryError)
101
107
  end
102
108
 
103
109
  it "sets the notification error description" do
@@ -0,0 +1,18 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Rapns::Apns::DisconnectionError do
4
+ let(:error) { Rapns::Apns::DisconnectionError.new }
5
+
6
+ it 'returns a nil error code' do
7
+ error.code.should be_nil
8
+ end
9
+
10
+ it 'contains an error description' do
11
+ error.description
12
+ end
13
+
14
+ it 'returns a message' do
15
+ error.message
16
+ error.to_s
17
+ end
18
+ end
@@ -90,29 +90,29 @@ describe Rapns::Daemon::Apns::FeedbackReceiver, 'check_for_feedback' do
90
90
  receiver.stop
91
91
  end
92
92
 
93
- it 'calls the configuration feedback_callback when feedback is received and the callback is set' do
93
+ it 'calls the apns_feedback_callback when feedback is received and the callback is set' do
94
94
  stub_connection_read_with_tuple
95
- Rapns.configuration.feedback_callback = Proc.new {}
95
+ Rapns.config.apns_feedback_callback = Proc.new {}
96
96
  feedback = Object.new
97
97
  Rapns::Apns::Feedback.stub(:create! => feedback)
98
- Rapns.configuration.feedback_callback.should_receive(:call).with(feedback)
98
+ Rapns.config.apns_feedback_callback.should_receive(:call).with(feedback)
99
99
  receiver.check_for_feedback
100
100
  end
101
101
 
102
- it 'catches exceptions in the feedback_callback' do
102
+ it 'catches exceptions in the apns_feedback_callback' do
103
103
  error = StandardError.new('bork!')
104
104
  stub_connection_read_with_tuple
105
105
  callback = Proc.new { raise error }
106
- Rapns::configuration.feedback_callback = callback
106
+ Rapns.config.on_apns_feedback &callback
107
107
  expect { receiver.check_for_feedback }.not_to raise_error
108
108
  end
109
109
 
110
- it 'logs an exception from the feedback_callback' do
110
+ it 'logs an exception from the apns_feedback_callback' do
111
111
  error = StandardError.new('bork!')
112
112
  stub_connection_read_with_tuple
113
113
  callback = Proc.new { raise error }
114
114
  Rapns::Daemon.logger.should_receive(:error).with(error)
115
- Rapns::configuration.feedback_callback = callback
115
+ Rapns.config.on_apns_feedback &callback
116
116
  receiver.check_for_feedback
117
117
  end
118
118
  end