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