push-core 0.0.1.pre2 → 0.0.1.pre4

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