push-core 0.0.1.pre2 → 0.0.1.pre4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Push
2
2
 
3
+ Please note this gem not yet used in production. If you want to help, please contact me.
4
+
3
5
  ## Installation
4
6
 
5
7
  Add to your `GemFile`
@@ -12,10 +14,14 @@ For __APNS__ (iOS: Apple Push Notification Services):
12
14
 
13
15
  gem push-apns
14
16
 
15
- For __C2DM__ (Android: Cloud to Device Messaging):
17
+ For __C2DM__ (Android: Cloud to Device Messaging, deprecated):
16
18
 
17
19
  gem push-c2dm
18
20
 
21
+ For __GCM__ (Android: Google Cloud Messaging):
22
+
23
+ gem push-gcm
24
+
19
25
  And run `bundle install` to install the gems.
20
26
 
21
27
  To generate the migration and the configuration files run:
@@ -24,29 +30,25 @@ To generate the migration and the configuration files run:
24
30
  bundle exec rake db:migrate
25
31
 
26
32
  ## Configuration
27
- A default configuration file looks like this:
33
+
34
+ The configuration is in the database and you add the configuration per push provider with the console (`rails c`):
35
+
36
+ APNS:
28
37
  ```ruby
29
- Push::Daemon::Builder.new do
30
- daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
31
-
32
- provider :apns,
33
- {
34
- :certificate => "production.pem",
35
- :certificate_password => "",
36
- :sandbox => false,
37
- :connections => 3,
38
- :feedback_poll => 60
39
- }
40
-
41
- provider :c2dm,
42
- {
43
- :connections => 2,
44
- :email => "",
45
- :password => ""
46
- }
47
- end
38
+ Push::ConfigurationApns.create(:app => 'app_name', :connections => 2, :certificate => File.read("certificate.pem"), :feedback_poll => 60, :enabled => true).save
48
39
  ```
49
- Remove the provider you're not using. Add your email and password to enable C2DM. For APNS follow the 'Generating Certificates' below.
40
+
41
+ C2DM
42
+ ```ruby
43
+ Push::ConfigurationC2dm.create(:app => 'app_name', :connections => 2, :email => "<email address here>", :password => "<password here>", :enabled => true).save
44
+ ```
45
+
46
+ GCM
47
+ ```ruby
48
+ Push::ConfigurationGcm.create(:app => 'app_name', :connections => 2, :key => '<api key here>').save
49
+ ```
50
+
51
+ You can have each provider per app_name and you can have more than one app_name. Use the instructions below to generate the certificate for the APNS provider.
50
52
 
51
53
 
52
54
  ### Generating Certificates
@@ -70,18 +72,49 @@ To start the daemon:
70
72
 
71
73
  bundle exec push <environment> <options>
72
74
 
73
- Where `<environment>` is your Rails environment and `<options>` can be `--foreground`, `--version` or `--help`.
75
+ Where `<environment>` is your Rails environment and `<options>` can be:
76
+
77
+ -f, --foreground Run in the foreground.
78
+ -p, --pid-file PATH Path to write PID file. Relative to Rails root unless absolute.
79
+ -P, --push-poll N Frequency in seconds to check for new notifications. Default: 2.
80
+ -n, --airbrake-notify Enables error notifications via Airbrake.
81
+ -F, --feedback-poll N Frequency in seconds to check for feedback for the feedback processor. Default: 60. Use 0 to disable.
82
+ -b, --feedback-processor PATH Path to the feedback processor. Default: lib/push/feedback_processor.
83
+ -v, --version Print this version of push.
84
+ -h, --help You're looking at it.
85
+
74
86
 
75
87
  ## Sending notifications
76
88
  APNS:
77
89
  ```ruby
78
- Push::MessageApns.new(device: "<APNS device_token here>", alert: 'Hello World', expiry: 1.day.to_i, attributes_for_device: {key: 'MSG'}).save
90
+ Push::MessageApns.new(:app => 'app_name', device: '<APNS device_token here>', alert: 'Hello World', expiry: 1.day.to_i, attributes_for_device: {key: 'MSG'}).save
79
91
  ```
80
92
  C2DM:
81
93
  ```ruby
82
- Push::MessageC2dm.new(device: "<C2DM registration_id here>", payload: { message: "Hello World" }, collapse_key: "MSG").save
94
+ Push::MessageC2dm.new(:app => 'app_name', device: '<C2DM registration_id here>', payload: { message: 'Hello World' }, collapse_key: 'MSG').save
83
95
  ```
84
96
 
97
+ GCM:
98
+ ```ruby
99
+ Push::MessageGcm.new(:app => 'app_name', device: '<GCM registration_id here>', payload: { message: 'Hello World' }, collapse_key: 'MSG').save
100
+ ```
101
+
102
+ ## Feedback processing
103
+
104
+ The push providers return feedback in various ways and these are captured and stored in the `push_feedback` table. The installer installs the `lib/push/feedback_processor.rb` file which is by default called every 60 seconds. In this file you can process the feedback which is different for every application.
105
+
106
+ ## Rake Task
107
+
108
+ The push-core also comes with a rake task to delete all the messages and feedback of the last 7 days or by the DAYS parameter.
109
+
110
+ bundle exec rake push:clean DAYS=2
111
+
112
+ ## Prerequisites
113
+
114
+ * Rails 3.2 +
115
+ * Ruby 1.9
116
+
117
+
85
118
  ## Thanks
86
119
 
87
120
  This project started as a fork of Ian Leitch [RAPNS](https://github.com/ileitch/rapns) project. The differences between this project and RAPNS is the support for C2DM and the modularity of the push providers.
data/bin/push CHANGED
@@ -3,12 +3,24 @@
3
3
  require 'optparse'
4
4
  require 'push'
5
5
 
6
- foreground = false
7
6
  environment = ARGV[0]
7
+
8
+ config = Struct.new(:foreground, :pid_file, :push_poll, :airbrake_notify, :feedback_poll, :feedback_processor).new
9
+ config.foreground = false
10
+ config.push_poll = 2
11
+ config.airbrake_notify = false
12
+ config.feedback_poll = 60
13
+ config.feedback_processor = 'lib/push/feedback_processor'
14
+
8
15
  banner = 'Usage: push <Rails environment> [options]'
9
16
  ARGV.options do |opts|
10
17
  opts.banner = banner
11
- opts.on('-f', '--foreground', 'Run in the foreground.') { foreground = true }
18
+ opts.on('-f', '--foreground', 'Run in the foreground.') { config.foreground = true }
19
+ opts.on('-p PATH', '--pid-file PATH', String, 'Path to write PID file. Relative to Rails root unless absolute.') { |path| config.pid_file = path }
20
+ opts.on('-P N', '--push-poll N', Integer, "Frequency in seconds to check for new notifications. Default: #{config.push_poll}.") { |n| config.push_poll = n }
21
+ opts.on('-n', '--airbrake-notify', 'Enables error notifications via Airbrake.') { config.check_for_errors = true }
22
+ opts.on('-F N', '--feedback-poll N', Integer, "Frequency in seconds to check for feedback for the feedback processor. Default: #{config.feedback_poll}. Use 0 to disable.") { |n| config.feedback_poll = n }
23
+ opts.on('-b PATH', '--feedback-processor PATH', String, "Path to the feedback processor. Default: #{config.feedback_processor}.") { |n| config.feedback_processor = n }
12
24
  opts.on('-v', '--version', 'Print this version of push.') { puts "push #{Push::VERSION}"; exit }
13
25
  opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
14
26
  opts.parse!
@@ -24,4 +36,8 @@ load 'config/environment.rb'
24
36
 
25
37
  require 'push/daemon'
26
38
 
27
- Push::Daemon.start(environment, foreground)
39
+ if config.pid_file && !Pathname.new(config.pid_file).absolute?
40
+ config.pid_file = File.join(Rails.root, config.pid_file)
41
+ end
42
+
43
+ Push::Daemon.start(environment, config)
@@ -15,8 +15,6 @@ class PushGenerator < Rails::Generators::Base
15
15
  end
16
16
 
17
17
  def copy_config
18
- copy_file "development.rb", "config/push/development.rb"
19
- copy_file "staging.rb", "config/push/staging.rb"
20
- copy_file "production.rb", "config/push/production.rb"
18
+ copy_file "feedback_processor.rb", "lib/push/feedback_processor.rb"
21
19
  end
22
20
  end
@@ -1,6 +1,16 @@
1
1
  class CreatePush < ActiveRecord::Migration
2
2
  def self.up
3
+ create_table :push_configurations do |t|
4
+ t.string :type, :null => false
5
+ t.string :app, :null => false
6
+ t.text :properties, :null => true
7
+ t.boolean :enabled, :null => false, :default => false
8
+ t.integer :connections, :null => false, :default => 1
9
+ t.timestamps
10
+ end
11
+
3
12
  create_table :push_messages do |t|
13
+ t.string :app, :null => false
4
14
  t.string :device, :null => false
5
15
  t.string :type, :null => false
6
16
  t.text :properties, :null => true
@@ -17,17 +27,23 @@ class CreatePush < ActiveRecord::Migration
17
27
  add_index :push_messages, [:delivered, :failed, :deliver_after]
18
28
 
19
29
  create_table :push_feedback do |t|
30
+ t.string :app, :null => false
20
31
  t.string :device, :null => false
21
32
  t.string :type, :null => false
33
+ t.string :follow_up, :null => false
22
34
  t.timestamp :failed_at, :null => false
35
+ t.boolean :processed, :null => false, :default => false
36
+ t.timestamp :processed_at, :null => true
37
+ t.text :properties, :null => true
23
38
  t.timestamps
24
39
  end
25
40
 
26
- add_index :push_feedback, :device
41
+ add_index :push_feedback, :processed
27
42
  end
28
43
 
29
44
  def self.down
30
45
  drop_table :push_feedback
31
46
  drop_table :push_messages
47
+ drop_table :push_configurations
32
48
  end
33
49
  end
@@ -0,0 +1,28 @@
1
+ module Push
2
+ class FeedbackProcessor
3
+ def self.process(feedback)
4
+ if feedback.instance_of? Push::FeedbackGcm
5
+ if feedback.follow_up == 'delete'
6
+ # TODO: delete gcm device
7
+
8
+ elsif feedback.follow_up == 'update'
9
+ # TODO: update gcm device
10
+ # device = feedback.update_to
11
+
12
+ end
13
+ elsif feedback.instance_of? Push::FeedbackC2dm
14
+ if feedback.follow_up == 'delete'
15
+ # TODO: delete c2dm device
16
+
17
+ end
18
+ elsif feedback.instance_of? Push::FeedbackApns
19
+ if feedback.follow_up == 'delete'
20
+ # TODO: delete apns device
21
+
22
+ end
23
+ else
24
+ Push::Daemon.logger.info("[FeedbackProcessor] Unknown feedback type")
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/push.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
2
  require 'push/version'
3
+ require 'push/configuration'
3
4
  require 'push/message'
4
5
  require 'push/feedback'
@@ -0,0 +1,10 @@
1
+ module Push
2
+ class Configuration < ActiveRecord::Base
3
+ self.table_name = 'push_configurations'
4
+ scope :enabled, where(:enabled => true)
5
+ validates :app, :presence => true
6
+ validates :connections, :presence => true
7
+ validates :connections, :numericality => { :greater_than => 0, :only_integer => true }
8
+ validates :type, :uniqueness => { :scope => :app, :message => "Only one push provider type per configuration name" }
9
+ end
10
+ end
data/lib/push/daemon.rb CHANGED
@@ -1,54 +1,39 @@
1
1
  require 'thread'
2
- require 'push/daemon/builder'
3
2
  require 'push/daemon/interruptible_sleep'
4
3
  require 'push/daemon/delivery_error'
5
4
  require 'push/daemon/disconnection_error'
6
- require 'push/daemon/pool'
7
5
  require 'push/daemon/connection_pool'
8
6
  require 'push/daemon/database_reconnectable'
9
7
  require 'push/daemon/delivery_queue'
10
8
  require 'push/daemon/delivery_handler'
11
- require 'push/daemon/delivery_handler_pool'
9
+ require 'push/daemon/feedback'
10
+ require 'push/daemon/feedback/feedback_feeder'
11
+ require 'push/daemon/feedback/feedback_handler'
12
12
  require 'push/daemon/feeder'
13
13
  require 'push/daemon/logger'
14
+ require 'push/daemon/app'
14
15
 
15
16
  module Push
16
17
  module Daemon
17
18
  class << self
18
- attr_accessor :logger, :configuration, :delivery_queue,
19
- :connection_pool, :delivery_handler_pool, :foreground, :providers
19
+ attr_accessor :logger, :config
20
20
  end
21
21
 
22
- def self.start(environment, foreground)
23
- self.providers = []
24
- @foreground = foreground
22
+ def self.start(environment, config)
23
+ self.config = config
24
+ self.logger = Logger.new(:foreground => config.foreground, :airbrake_notify => config.airbrake_notify)
25
25
  setup_signal_hooks
26
-
27
- require File.join(Rails.root, 'config', 'push', environment + '.rb')
28
-
29
- self.logger = Logger.new(:foreground => foreground, :airbrake_notify => configuration[:airbrake_notify])
30
-
31
- self.delivery_queue = DeliveryQueue.new
32
-
33
- daemonize unless foreground
34
-
26
+ daemonize unless config.foreground
35
27
  write_pid_file
36
28
 
37
- dbconnections = 0
38
- self.connection_pool = ConnectionPool.new
39
- self.providers.each do |provider|
40
- self.connection_pool.populate(provider)
41
- dbconnections += provider.totalconnections
42
- end
43
-
44
- rescale_poolsize(dbconnections)
45
-
46
- self.delivery_handler_pool = DeliveryHandlerPool.new(connection_pool.size)
47
- delivery_handler_pool.populate
29
+ App.load
30
+ App.start
31
+ Feedback.load(config)
32
+ Feedback.start
33
+ rescale_poolsize(App.database_connections + Feedback.database_connections)
48
34
 
49
35
  logger.info('[Daemon] Ready')
50
-
51
- Push::Daemon::Feeder.start(foreground)
36
+ Feeder.start(config)
52
37
  end
53
38
 
54
39
  protected
@@ -80,14 +65,16 @@ module Push
80
65
  end
81
66
 
82
67
  def self.shutdown
83
- puts "\nShutting down..."
84
- Push::Daemon::Feeder.stop
85
- Push::Daemon.delivery_handler_pool.drain if Push::Daemon.delivery_handler_pool
86
-
87
- self.providers.each do |provider|
88
- provider.stop
68
+ print "\nShutting down..."
69
+ Feeder.stop
70
+ Feedback.stop
71
+ App.stop
72
+
73
+ while Thread.list.count > 1
74
+ sleep 0.1
75
+ print "."
89
76
  end
90
-
77
+ print "\n"
91
78
  delete_pid_file
92
79
  end
93
80
 
@@ -105,19 +92,19 @@ module Push
105
92
  end
106
93
 
107
94
  def self.write_pid_file
108
- if !configuration[:pid_file].blank?
95
+ if !config[:pid_file].blank?
109
96
  begin
110
97
  File.open(configuration[:pid_file], 'w') do |f|
111
98
  f.puts $$
112
99
  end
113
100
  rescue SystemCallError => e
114
- logger.error("Failed to write PID to '#{configuration[:pid_file]}': #{e.inspect}")
101
+ logger.error("Failed to write PID to '#{config[:pid_file]}': #{e.inspect}")
115
102
  end
116
103
  end
117
104
  end
118
105
 
119
106
  def self.delete_pid_file
120
- pid_file = configuration[:pid_file]
107
+ pid_file = config[:pid_file]
121
108
  File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
122
109
  end
123
110
  end
@@ -0,0 +1,103 @@
1
+ module Push
2
+ module Daemon
3
+ class App
4
+ class << self
5
+ attr_reader :apps
6
+ end
7
+
8
+ @apps = {}
9
+
10
+ def self.load
11
+ configurations = Push::Configuration.enabled
12
+ configurations.each do |config|
13
+ if @apps[config.app] == nil
14
+ @apps[config.app] = App.new(config.app)
15
+ end
16
+ @apps[config.app].configs << config
17
+ end
18
+ end
19
+
20
+ def self.ready
21
+ ready = []
22
+ @apps.each { |app, runner| ready << app if runner.ready? }
23
+ ready
24
+ end
25
+
26
+ def self.deliver(notification)
27
+ if app = @apps[notification.app]
28
+ app.deliver(notification)
29
+ else
30
+ Rapns::Daemon.logger.error("No such app '#{notification.app}' for notification #{notification.id}.")
31
+ end
32
+ end
33
+
34
+ def self.start
35
+ @apps.values.map(&:start)
36
+ end
37
+
38
+ def self.stop
39
+ @apps.values.map(&:stop)
40
+ end
41
+
42
+ def self.database_connections
43
+ @apps.empty? ? 0 : @apps.values.collect{|x| x.database_connections }.inject(:+)
44
+ end
45
+
46
+ def initialize(name)
47
+ @name = name
48
+ @configs = []
49
+ @handlers = []
50
+ @providers = []
51
+ @queue = DeliveryQueue.new
52
+ @database_connections = 0
53
+ end
54
+
55
+ attr_accessor :configs
56
+ attr_reader :database_connections
57
+
58
+ def deliver(notification)
59
+ @queue.push(notification)
60
+ end
61
+
62
+ def start
63
+ @connection_pool = ConnectionPool.new
64
+ @configs.each do |config|
65
+ provider = load_provider(config.name, config.properties.merge({:connections => config.connections, :name => config.app}))
66
+ @providers << provider
67
+ @database_connections += provider.totalconnections
68
+ @connection_pool.populate(provider)
69
+ end
70
+ @connection_pool.size.times do |i|
71
+ @handlers << start_handler(i)
72
+ end
73
+ end
74
+
75
+ def stop
76
+ @handlers.map(&:stop)
77
+ @providers.map(&:stop)
78
+ end
79
+
80
+ def ready?
81
+ @queue.notifications_processed?
82
+ end
83
+
84
+ protected
85
+
86
+ def start_handler(i)
87
+ handler = DeliveryHandler.new(@queue, @connection_pool, "#{@name} #{i}")
88
+ handler.start
89
+ handler
90
+ end
91
+
92
+ def load_provider(klass, options)
93
+ begin
94
+ middleware = Push::Daemon.const_get("#{klass}".camelize)
95
+ rescue NameError
96
+ raise LoadError, "Could not find matching push provider for #{klass.inspect}. You may need to install an additional gem (such as push-#{klass})."
97
+ end
98
+
99
+ middleware.new(options)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -23,7 +23,7 @@ module Push
23
23
  end
24
24
 
25
25
  def size
26
- @connections.size
26
+ @connections.values.collect{|x| x.length }.inject(:+)
27
27
  end
28
28
  end
29
29
  end
@@ -1,17 +1,16 @@
1
1
  module Push
2
2
  module Daemon
3
3
  class DeliveryHandler
4
- include DatabaseReconnectable
5
-
6
4
  attr_reader :name
7
- STOP = 0x666
8
5
 
9
- def initialize(i)
10
- @name = "DeliveryHandler #{i}"
6
+ def initialize(queue, connection_pool, name)
7
+ @queue = queue
8
+ @connection_pool = connection_pool
9
+ @name = "DeliveryHandler #{name}"
11
10
  end
12
11
 
13
12
  def start
14
- Thread.new do
13
+ @thread = Thread.new do
15
14
  loop do
16
15
  break if @stop
17
16
  handle_next_notification
@@ -21,26 +20,26 @@ module Push
21
20
 
22
21
  def stop
23
22
  @stop = true
24
- Push::Daemon.delivery_queue.push(STOP)
23
+ @queue.wakeup(@thread)
25
24
  end
26
25
 
27
26
  protected
28
27
 
29
28
  def handle_next_notification
30
- notification = Push::Daemon.delivery_queue.pop
31
-
32
- if notification == STOP
29
+ begin
30
+ notification = @queue.pop
31
+ rescue DeliveryQueue::WakeupError
33
32
  return
34
33
  end
35
34
 
36
35
  begin
37
- connection = Push::Daemon.connection_pool.checkout(notification.use_connection)
36
+ connection = @connection_pool.checkout(notification.use_connection)
38
37
  notification.deliver(connection)
39
38
  rescue StandardError => e
40
39
  Push::Daemon.logger.error(e)
41
40
  ensure
42
- Push::Daemon.connection_pool.checkin(connection)
43
- Push::Daemon.delivery_queue.notification_processed
41
+ @connection_pool.checkin(connection)
42
+ @queue.notification_processed
44
43
  end
45
44
  end
46
45
  end
@@ -1,19 +1,23 @@
1
1
  module Push
2
2
  module Daemon
3
3
  class DeliveryQueue
4
+ class WakeupError < StandardError; end
4
5
  def initialize
5
- @mutex = Mutex.new
6
6
  @num_notifications = 0
7
- @queue = Queue.new
7
+ @queue = []
8
+ @waiting = []
9
+ @mutex = Mutex.new
8
10
  end
9
11
 
10
- def push(notification)
11
- @mutex.synchronize { @num_notifications += 1 }
12
- @queue.push(notification)
12
+ def wakeup(thread)
13
+ @mutex.synchronize do
14
+ t = @waiting.delete(thread)
15
+ t.raise WakeupError if t
16
+ end
13
17
  end
14
18
 
15
- def pop
16
- @queue.pop
19
+ def size
20
+ @mutex.synchronize { @queue.size }
17
21
  end
18
22
 
19
23
  def notification_processed
@@ -23,6 +27,33 @@ module Push
23
27
  def notifications_processed?
24
28
  @mutex.synchronize { @num_notifications == 0 }
25
29
  end
30
+
31
+ def push(notification)
32
+ @mutex.synchronize do
33
+ @num_notifications += 1
34
+ @queue.push(notification)
35
+
36
+ begin
37
+ t = @waiting.shift
38
+ t.wakeup if t
39
+ rescue ThreadError
40
+ retry
41
+ end
42
+ end
43
+ end
44
+
45
+ def pop
46
+ @mutex.synchronize do
47
+ while true
48
+ if @queue.empty?
49
+ @waiting.push Thread.current
50
+ @mutex.sleep
51
+ else
52
+ return @queue.shift
53
+ end
54
+ end
55
+ end
56
+ end
26
57
  end
27
58
  end
28
59
  end
@@ -0,0 +1,33 @@
1
+ module Push
2
+ module Daemon
3
+ module Feedback
4
+ class << self
5
+ attr_accessor :queue, :handler, :feeder
6
+ end
7
+
8
+ def self.load(config)
9
+ return if config.feedback_poll == 0
10
+ self.queue = DeliveryQueue.new
11
+ self.handler = Feedback::FeedbackHandler.new(Rails.root + config.feedback_processor)
12
+ self.feeder = Feedback::FeedbackFeeder.new(config.feedback_poll)
13
+ end
14
+
15
+ def self.start
16
+ return if self.handler.nil? or self.feeder.nil?
17
+ self.handler.start
18
+ self.feeder.start
19
+ @started = true
20
+ end
21
+
22
+ def self.stop
23
+ return unless @started
24
+ self.feeder.stop
25
+ self.handler.stop
26
+ end
27
+
28
+ def self.database_connections
29
+ @started ? 2 : 0
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ module Push
2
+ module Daemon
3
+ module Feedback
4
+ class FeedbackFeeder
5
+ include ::Push::Daemon::DatabaseReconnectable
6
+ include ::Push::Daemon::InterruptibleSleep
7
+
8
+ def initialize(poll)
9
+ @poll = poll
10
+ end
11
+
12
+ def name
13
+ "FeedbackFeeder"
14
+ end
15
+
16
+ def start
17
+ Thread.new do
18
+ loop do
19
+ break if @stop
20
+ enqueue_feedback
21
+ interruptible_sleep @poll
22
+ end
23
+ end
24
+ end
25
+
26
+ def stop
27
+ @stop = true
28
+ interrupt_sleep
29
+ end
30
+
31
+ protected
32
+
33
+ def enqueue_feedback
34
+ begin
35
+ with_database_reconnect_and_retry(name) do
36
+ if Push::Daemon::Feedback.queue.notifications_processed?
37
+ Push::Feedback.ready_for_followup.find_each do |feedback|
38
+ Push::Daemon::Feedback.queue.push(feedback)
39
+ end
40
+ end
41
+ end
42
+ rescue StandardError => e
43
+ Push::Daemon.logger.error(e)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ module Push
2
+ module Daemon
3
+ module Feedback
4
+ class FeedbackHandler
5
+ attr_reader :name
6
+
7
+ def initialize(processor)
8
+ @name = "FeedbackHandler"
9
+ @queue = Push::Daemon::Feedback.queue
10
+ require processor
11
+ end
12
+
13
+ def start
14
+ @thread = Thread.new do
15
+ loop do
16
+ break if @stop
17
+ handle_next_feedback
18
+ end
19
+ end
20
+ end
21
+
22
+ def stop
23
+ @stop = true
24
+ @queue.wakeup(@thread)
25
+ end
26
+
27
+ protected
28
+
29
+ def handle_next_feedback
30
+ begin
31
+ feedback = @queue.pop
32
+ rescue DeliveryQueue::WakeupError
33
+ return
34
+ end
35
+
36
+ begin
37
+ Push::FeedbackProcessor.process(feedback)
38
+ rescue StandardError => e
39
+ Push::Daemon.logger.error(e)
40
+ ensure
41
+ feedback.is_processed(@name)
42
+ @queue.notification_processed
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -8,13 +8,13 @@ module Push
8
8
  "Feeder"
9
9
  end
10
10
 
11
- def self.start(foreground)
12
- reconnect_database unless foreground
11
+ def self.start(config)
12
+ reconnect_database unless config.foreground
13
13
 
14
14
  loop do
15
15
  break if @stop
16
16
  enqueue_notifications
17
- interruptible_sleep Push::Daemon.configuration[:poll]
17
+ interruptible_sleep config.push_poll
18
18
  end
19
19
  end
20
20
 
@@ -28,10 +28,9 @@ module Push
28
28
  def self.enqueue_notifications
29
29
  begin
30
30
  with_database_reconnect_and_retry(name) do
31
- if Push::Daemon.delivery_queue.notifications_processed?
32
- Push::Message.ready_for_delivery.find_each do |notification|
33
- Push::Daemon.delivery_queue.push(notification)
34
- end
31
+ Push::Message.ready_for_delivery.find_each do |notification|
32
+ ready_apps = Push::Daemon::App.ready
33
+ Push::Daemon::App.deliver(notification) if ready_apps.include?(notification.app)
35
34
  end
36
35
  end
37
36
  rescue StandardError => e
data/lib/push/feedback.rb CHANGED
@@ -1,8 +1,20 @@
1
1
  module Push
2
2
  class Feedback < ActiveRecord::Base
3
+ include Push::Daemon::DatabaseReconnectable
3
4
  self.table_name = 'push_feedback'
4
5
 
6
+ scope :ready_for_followup, where(:processed => false)
7
+ validates :app, :presence => true
5
8
  validates :device, :presence => true
9
+ validates :follow_up, :presence => true
6
10
  validates :failed_at, :presence => true
11
+
12
+ def is_processed(name)
13
+ with_database_reconnect_and_retry(name) do
14
+ self.processed = true
15
+ self.processed_at = Time.now
16
+ self.save
17
+ end
18
+ end
7
19
  end
8
20
  end
data/lib/push/message.rb CHANGED
@@ -6,6 +6,7 @@ module Push
6
6
  include Push::Daemon::DatabaseReconnectable
7
7
  self.table_name = "push_messages"
8
8
 
9
+ validates :app, :presence => true
9
10
  validates :device, :presence => true
10
11
 
11
12
  scope :ready_for_delivery, lambda { where('delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', false, false, Time.now) }
@@ -22,7 +23,7 @@ module Push
22
23
  self.save!(:validate => false)
23
24
  end
24
25
 
25
- Push::Daemon.logger.info("Message #{id} delivered to #{device}")
26
+ Push::Daemon.logger.info("[#{connection.name}] Message #{id} delivered to #{device}")
26
27
  rescue Push::DeliveryError, Push::DisconnectionError => error
27
28
  handle_delivery_error(error, connection)
28
29
  raise
data/lib/push/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Push
2
- VERSION = "0.0.1.pre2"
2
+ VERSION = "0.0.1.pre4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: push-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.pre2
4
+ version: 0.0.1.pre4
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-03 00:00:00.000000000 Z
12
+ date: 2012-07-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &70357910508440 !ruby/object:Gem::Requirement
16
+ requirement: &70213224802660 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 3.2.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70357910508440
24
+ version_requirements: *70213224802660
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: sqlite3
27
- requirement: &70357910506440 !ruby/object:Gem::Requirement
27
+ requirement: &70213224801920 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,8 +32,9 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70357910506440
36
- description: Push daemon for push notification services like APNS and C2DM.
35
+ version_requirements: *70213224801920
36
+ description: Push daemon for push notification services like APNS (iOS/Apple) and
37
+ GCM/C2DM (Android).
37
38
  email:
38
39
  - tom@tnux.net
39
40
  executables:
@@ -43,24 +44,24 @@ extra_rdoc_files: []
43
44
  files:
44
45
  - lib/generators/push_generator.rb
45
46
  - lib/generators/templates/create_push.rb
46
- - lib/generators/templates/development.rb
47
- - lib/generators/templates/production.rb
48
- - lib/generators/templates/staging.rb
47
+ - lib/generators/templates/feedback_processor.rb
49
48
  - lib/push-core.rb
50
49
  - lib/push.rb
50
+ - lib/push/configuration.rb
51
51
  - lib/push/daemon.rb
52
- - lib/push/daemon/builder.rb
52
+ - lib/push/daemon/app.rb
53
53
  - lib/push/daemon/connection_pool.rb
54
54
  - lib/push/daemon/database_reconnectable.rb
55
55
  - lib/push/daemon/delivery_error.rb
56
56
  - lib/push/daemon/delivery_handler.rb
57
- - lib/push/daemon/delivery_handler_pool.rb
58
57
  - lib/push/daemon/delivery_queue.rb
59
58
  - lib/push/daemon/disconnection_error.rb
59
+ - lib/push/daemon/feedback.rb
60
+ - lib/push/daemon/feedback/feedback_feeder.rb
61
+ - lib/push/daemon/feedback/feedback_handler.rb
60
62
  - lib/push/daemon/feeder.rb
61
63
  - lib/push/daemon/interruptible_sleep.rb
62
64
  - lib/push/daemon/logger.rb
63
- - lib/push/daemon/pool.rb
64
65
  - lib/push/feedback.rb
65
66
  - lib/push/message.rb
66
67
  - lib/push/railtie.rb
@@ -92,5 +93,5 @@ rubyforge_project:
92
93
  rubygems_version: 1.8.5
93
94
  signing_key:
94
95
  specification_version: 3
95
- summary: Core of the modular push daemon.
96
+ summary: Core of the push daemon.
96
97
  test_files: []
@@ -1,19 +0,0 @@
1
- Push::Daemon::Builder.new do
2
- daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
3
-
4
- provider :apns,
5
- {
6
- :certificate => "development.pem",
7
- :certificate_password => "",
8
- :sandbox => true,
9
- :connections => 3,
10
- :feedback_poll => 60
11
- }
12
-
13
- provider :c2dm,
14
- {
15
- :connections => 2,
16
- :email => "",
17
- :password => ""
18
- }
19
- end
@@ -1,19 +0,0 @@
1
- Push::Daemon::Builder.new do
2
- daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
3
-
4
- provider :apns,
5
- {
6
- :certificate => "production.pem",
7
- :certificate_password => "",
8
- :sandbox => false,
9
- :connections => 3,
10
- :feedback_poll => 60
11
- }
12
-
13
- provider :c2dm,
14
- {
15
- :connections => 2,
16
- :email => "",
17
- :password => ""
18
- }
19
- end
@@ -1,19 +0,0 @@
1
- Push::Daemon::Builder.new do
2
- daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
3
-
4
- provider :apns,
5
- {
6
- :certificate => "staging.pem",
7
- :certificate_password => "",
8
- :sandbox => true,
9
- :connections => 3,
10
- :feedback_poll => 60
11
- }
12
-
13
- provider :c2dm,
14
- {
15
- :connections => 2,
16
- :email => "",
17
- :password => ""
18
- }
19
- end
@@ -1,23 +0,0 @@
1
- module Push
2
- module Daemon
3
- class Builder
4
- def initialize(&block)
5
- instance_eval(&block) if block_given?
6
- end
7
-
8
- def daemon(options)
9
- Push::Daemon.configuration = options
10
- end
11
-
12
- def provider(klass, options)
13
- begin
14
- middleware = Push::Daemon.const_get("#{klass}".camelize)
15
- rescue NameError
16
- raise LoadError, "Could not find matching push provider for #{klass.inspect}. You may need to install an additional gem (such as push-#{klass})."
17
- end
18
-
19
- Push::Daemon.providers << middleware.new(options)
20
- end
21
- end
22
- end
23
- end
@@ -1,20 +0,0 @@
1
- module Push
2
- module Daemon
3
- class DeliveryHandlerPool < Pool
4
-
5
- protected
6
-
7
- def new_object_for_pool(i)
8
- DeliveryHandler.new(i)
9
- end
10
-
11
- def object_added_to_pool(object)
12
- object.start
13
- end
14
-
15
- def object_removed_from_pool(object)
16
- object.stop
17
- end
18
- end
19
- end
20
- end
@@ -1,36 +0,0 @@
1
- module Push
2
- module Daemon
3
- class Pool
4
- def initialize(num_objects)
5
- @num_objects = num_objects
6
- @queue = Queue.new
7
- end
8
-
9
- def populate
10
- @num_objects.times do |i|
11
- object = new_object_for_pool(i)
12
- @queue.push(object)
13
- object_added_to_pool(object)
14
- end
15
- end
16
-
17
- def drain
18
- while !@queue.empty?
19
- object = @queue.pop
20
- object_removed_from_pool(object)
21
- end
22
- end
23
-
24
- protected
25
-
26
- def new_object_for_pool(i)
27
- end
28
-
29
- def object_added_to_pool(object)
30
- end
31
-
32
- def object_removed_from_pool(object)
33
- end
34
- end
35
- end
36
- end