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

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 +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