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 +58 -25
- data/bin/push +19 -3
- data/lib/generators/push_generator.rb +1 -3
- data/lib/generators/templates/create_push.rb +17 -1
- data/lib/generators/templates/feedback_processor.rb +28 -0
- data/lib/push.rb +1 -0
- data/lib/push/configuration.rb +10 -0
- data/lib/push/daemon.rb +27 -40
- data/lib/push/daemon/app.rb +103 -0
- data/lib/push/daemon/connection_pool.rb +1 -1
- data/lib/push/daemon/delivery_handler.rb +12 -13
- data/lib/push/daemon/delivery_queue.rb +38 -7
- data/lib/push/daemon/feedback.rb +33 -0
- data/lib/push/daemon/feedback/feedback_feeder.rb +49 -0
- data/lib/push/daemon/feedback/feedback_handler.rb +48 -0
- data/lib/push/daemon/feeder.rb +6 -7
- data/lib/push/feedback.rb +12 -0
- data/lib/push/message.rb +2 -1
- data/lib/push/version.rb +1 -1
- metadata +15 -14
- data/lib/generators/templates/development.rb +0 -19
- data/lib/generators/templates/production.rb +0 -19
- data/lib/generators/templates/staging.rb +0 -19
- data/lib/push/daemon/builder.rb +0 -23
- data/lib/push/daemon/delivery_handler_pool.rb +0 -20
- data/lib/push/daemon/pool.rb +0 -36
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
|
-
|
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::
|
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
|
-
|
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
|
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:
|
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:
|
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
|
-
|
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 "
|
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, :
|
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
@@ -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/
|
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, :
|
19
|
-
:connection_pool, :delivery_handler_pool, :foreground, :providers
|
19
|
+
attr_accessor :logger, :config
|
20
20
|
end
|
21
21
|
|
22
|
-
def self.start(environment,
|
23
|
-
self.
|
24
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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 !
|
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 '#{
|
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 =
|
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
|
@@ -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(
|
10
|
-
@
|
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
|
-
|
23
|
+
@queue.wakeup(@thread)
|
25
24
|
end
|
26
25
|
|
27
26
|
protected
|
28
27
|
|
29
28
|
def handle_next_notification
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
begin
|
30
|
+
notification = @queue.pop
|
31
|
+
rescue DeliveryQueue::WakeupError
|
33
32
|
return
|
34
33
|
end
|
35
34
|
|
36
35
|
begin
|
37
|
-
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
|
-
|
43
|
-
|
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 =
|
7
|
+
@queue = []
|
8
|
+
@waiting = []
|
9
|
+
@mutex = Mutex.new
|
8
10
|
end
|
9
11
|
|
10
|
-
def
|
11
|
-
@mutex.synchronize
|
12
|
-
|
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
|
16
|
-
@queue.
|
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
|
data/lib/push/daemon/feeder.rb
CHANGED
@@ -8,13 +8,13 @@ module Push
|
|
8
8
|
"Feeder"
|
9
9
|
end
|
10
10
|
|
11
|
-
def self.start(
|
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
|
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
|
-
|
32
|
-
Push::
|
33
|
-
|
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
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.
|
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-
|
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: &
|
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: *
|
24
|
+
version_requirements: *70213224802660
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: sqlite3
|
27
|
-
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: *
|
36
|
-
description: Push daemon for push notification services like APNS and
|
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/
|
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/
|
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
|
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
|
data/lib/push/daemon/builder.rb
DELETED
@@ -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
|
data/lib/push/daemon/pool.rb
DELETED
@@ -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
|