push-core 0.0.1.pre
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/MIT-LICENSE +20 -0
- data/README.md +3 -0
- data/bin/push +27 -0
- data/lib/generators/push_generator.rb +22 -0
- data/lib/generators/templates/create_push.rb +33 -0
- data/lib/generators/templates/development.rb +19 -0
- data/lib/generators/templates/production.rb +19 -0
- data/lib/generators/templates/staging.rb +19 -0
- data/lib/push.rb +4 -0
- data/lib/push/daemon.rb +124 -0
- data/lib/push/daemon/builder.rb +23 -0
- data/lib/push/daemon/connection_pool.rb +30 -0
- data/lib/push/daemon/database_reconnectable.rb +51 -0
- data/lib/push/daemon/delivery_error.rb +16 -0
- data/lib/push/daemon/delivery_handler.rb +48 -0
- data/lib/push/daemon/delivery_handler_pool.rb +20 -0
- data/lib/push/daemon/delivery_queue.rb +28 -0
- data/lib/push/daemon/disconnection_error.rb +14 -0
- data/lib/push/daemon/feeder.rb +43 -0
- data/lib/push/daemon/interruptible_sleep.rb +18 -0
- data/lib/push/daemon/logger.rb +53 -0
- data/lib/push/daemon/pool.rb +36 -0
- data/lib/push/feedback.rb +8 -0
- data/lib/push/message.rb +47 -0
- data/lib/push/version.rb +3 -0
- metadata +93 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/bin/push
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'push'
|
5
|
+
|
6
|
+
foreground = false
|
7
|
+
environment = ARGV[0]
|
8
|
+
banner = 'Usage: push <Rails environment> [options]'
|
9
|
+
ARGV.options do |opts|
|
10
|
+
opts.banner = banner
|
11
|
+
opts.on('-f', '--foreground', 'Run in the foreground.') { foreground = true }
|
12
|
+
opts.on('-v', '--version', 'Print this version of push.') { puts "push #{Push::VERSION}"; exit }
|
13
|
+
opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
|
14
|
+
opts.parse!
|
15
|
+
end
|
16
|
+
|
17
|
+
if environment.nil?
|
18
|
+
puts banner
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
ENV['RAILS_ENV'] = environment
|
23
|
+
load 'config/environment.rb'
|
24
|
+
|
25
|
+
require 'push/daemon'
|
26
|
+
|
27
|
+
Push::Daemon.start(environment, foreground)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class PushGenerator < Rails::Generators::Base
|
2
|
+
include Rails::Generators::Migration
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def self.next_migration_number(path)
|
6
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
7
|
+
end
|
8
|
+
|
9
|
+
def copy_migration
|
10
|
+
migration_dir = File.expand_path("db/migrate")
|
11
|
+
|
12
|
+
if !self.class.migration_exists?(migration_dir, 'create_push')
|
13
|
+
migration_template "create_push.rb", "db/migrate/create_push.rb"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
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"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class CreatePush < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :push_messages do |t|
|
4
|
+
t.string :device, :null => false
|
5
|
+
t.string :type, :null => false
|
6
|
+
t.text :properties, :null => true
|
7
|
+
t.boolean :delivered, :null => false, :default => false
|
8
|
+
t.timestamp :delivered_at, :null => true
|
9
|
+
t.boolean :failed, :null => false, :default => false
|
10
|
+
t.timestamp :failed_at, :null => true
|
11
|
+
t.integer :error_code, :null => true
|
12
|
+
t.string :error_description, :null => true
|
13
|
+
t.timestamp :deliver_after, :null => true
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
add_index :push_messages, [:delivered, :failed, :deliver_after]
|
18
|
+
|
19
|
+
create_table :push_feedback do |t|
|
20
|
+
t.string :device, :null => false
|
21
|
+
t.string :type, :null => false
|
22
|
+
t.timestamp :failed_at, :null => false
|
23
|
+
t.timestamps
|
24
|
+
end
|
25
|
+
|
26
|
+
add_index :push_feedback, :device
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.down
|
30
|
+
drop_table :push_feedback
|
31
|
+
drop_table :push_messages
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
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.rb
ADDED
data/lib/push/daemon.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'push/daemon/builder'
|
3
|
+
require 'push/daemon/interruptible_sleep'
|
4
|
+
require 'push/daemon/delivery_error'
|
5
|
+
require 'push/daemon/disconnection_error'
|
6
|
+
require 'push/daemon/pool'
|
7
|
+
require 'push/daemon/connection_pool'
|
8
|
+
require 'push/daemon/database_reconnectable'
|
9
|
+
require 'push/daemon/delivery_queue'
|
10
|
+
require 'push/daemon/delivery_handler'
|
11
|
+
require 'push/daemon/delivery_handler_pool'
|
12
|
+
require 'push/daemon/feeder'
|
13
|
+
require 'push/daemon/logger'
|
14
|
+
|
15
|
+
module Push
|
16
|
+
module Daemon
|
17
|
+
class << self
|
18
|
+
attr_accessor :logger, :configuration, :delivery_queue,
|
19
|
+
:connection_pool, :delivery_handler_pool, :foreground, :providers
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.start(environment, foreground)
|
23
|
+
self.providers = []
|
24
|
+
@foreground = foreground
|
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
|
+
|
35
|
+
write_pid_file
|
36
|
+
|
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
|
48
|
+
|
49
|
+
logger.info('[Daemon] Ready')
|
50
|
+
|
51
|
+
Push::Daemon::Feeder.start(foreground)
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def self.rescale_poolsize(size)
|
57
|
+
# 1 feeder + providers
|
58
|
+
size = 1 + size
|
59
|
+
|
60
|
+
h = ActiveRecord::Base.connection_config
|
61
|
+
h[:pool] = size
|
62
|
+
ActiveRecord::Base.establish_connection(h)
|
63
|
+
logger.info("[Daemon] Rescaled ActiveRecord ConnectionPool size to #{size}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.setup_signal_hooks
|
67
|
+
@shutting_down = false
|
68
|
+
|
69
|
+
['SIGINT', 'SIGTERM'].each do |signal|
|
70
|
+
Signal.trap(signal) do
|
71
|
+
handle_shutdown_signal
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.handle_shutdown_signal
|
77
|
+
exit 1 if @shutting_down
|
78
|
+
@shutting_down = true
|
79
|
+
shutdown
|
80
|
+
end
|
81
|
+
|
82
|
+
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
|
89
|
+
end
|
90
|
+
|
91
|
+
delete_pid_file
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.daemonize
|
95
|
+
exit if pid = fork
|
96
|
+
Process.setsid
|
97
|
+
exit if pid = fork
|
98
|
+
|
99
|
+
Dir.chdir '/'
|
100
|
+
File.umask 0000
|
101
|
+
|
102
|
+
STDIN.reopen '/dev/null'
|
103
|
+
STDOUT.reopen '/dev/null', 'a'
|
104
|
+
STDERR.reopen STDOUT
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.write_pid_file
|
108
|
+
if !configuration[:pid_file].blank?
|
109
|
+
begin
|
110
|
+
File.open(configuration[:pid_file], 'w') do |f|
|
111
|
+
f.puts $$
|
112
|
+
end
|
113
|
+
rescue SystemCallError => e
|
114
|
+
logger.error("Failed to write PID to '#{configuration[:pid_file]}': #{e.inspect}")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.delete_pid_file
|
120
|
+
pid_file = configuration[:pid_file]
|
121
|
+
File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,23 @@
|
|
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
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Push
|
2
|
+
module Daemon
|
3
|
+
class ConnectionPool
|
4
|
+
def initialize()
|
5
|
+
@connections = Hash.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def populate(provider)
|
9
|
+
@connections[provider.connectiontype.to_s] = Queue.new
|
10
|
+
provider.pushconnections.times do |i|
|
11
|
+
c = provider.connectiontype.new(provider, i+1)
|
12
|
+
c.connect
|
13
|
+
checkin(c)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def checkin(connection)
|
18
|
+
@connections[connection.class.to_s].push(connection)
|
19
|
+
end
|
20
|
+
|
21
|
+
def checkout(notification_type)
|
22
|
+
@connections[notification_type.to_s].pop
|
23
|
+
end
|
24
|
+
|
25
|
+
def size
|
26
|
+
@connections.size
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class PGError < StandardError; end if !defined?(PGError)
|
2
|
+
module Mysql2; class Error < StandardError; end; end if !defined?(Mysql2)
|
3
|
+
|
4
|
+
module Push
|
5
|
+
module Daemon
|
6
|
+
module DatabaseReconnectable
|
7
|
+
ADAPTER_ERRORS = [ActiveRecord::StatementInvalid, PGError, Mysql2::Error]
|
8
|
+
|
9
|
+
def with_database_reconnect_and_retry(name)
|
10
|
+
begin
|
11
|
+
yield
|
12
|
+
rescue *ADAPTER_ERRORS => e
|
13
|
+
Push::Daemon.logger.error(e)
|
14
|
+
database_connection_lost(name)
|
15
|
+
retry
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def database_connection_lost(name)
|
20
|
+
Push::Daemon.logger.warn("[#{name}] Lost connection to database, reconnecting...")
|
21
|
+
attempts = 0
|
22
|
+
loop do
|
23
|
+
begin
|
24
|
+
Push::Daemon.logger.warn("[#{name}] Attempt #{attempts += 1}")
|
25
|
+
reconnect_database
|
26
|
+
check_database_is_connected
|
27
|
+
break
|
28
|
+
rescue *ADAPTER_ERRORS => e
|
29
|
+
Push::Daemon.logger.error(e, :airbrake_notify => false)
|
30
|
+
sleep_to_avoid_thrashing
|
31
|
+
end
|
32
|
+
end
|
33
|
+
Push::Daemon.logger.warn("[#{name}] Database reconnected")
|
34
|
+
end
|
35
|
+
|
36
|
+
def reconnect_database
|
37
|
+
ActiveRecord::Base.clear_all_connections!
|
38
|
+
ActiveRecord::Base.establish_connection
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_database_is_connected
|
42
|
+
# Simply asking the adapter for the connection state is not sufficient.
|
43
|
+
Push::Message.count
|
44
|
+
end
|
45
|
+
|
46
|
+
def sleep_to_avoid_thrashing
|
47
|
+
sleep 2
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Push
|
2
|
+
class DeliveryError < StandardError
|
3
|
+
attr_reader :code, :description
|
4
|
+
|
5
|
+
def initialize(code, message_id, description, source)
|
6
|
+
@code = code
|
7
|
+
@message_id = message_id
|
8
|
+
@description = description
|
9
|
+
@source = source
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
"Unable to deliver message #{@message_id}, received #{@source} error #{@code} (#{@description})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Push
|
2
|
+
module Daemon
|
3
|
+
class DeliveryHandler
|
4
|
+
include DatabaseReconnectable
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
STOP = 0x666
|
8
|
+
|
9
|
+
def initialize(i)
|
10
|
+
@name = "DeliveryHandler #{i}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
Thread.new do
|
15
|
+
loop do
|
16
|
+
break if @stop
|
17
|
+
handle_next_notification
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
@stop = true
|
24
|
+
Push::Daemon.delivery_queue.push(STOP)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def handle_next_notification
|
30
|
+
notification = Push::Daemon.delivery_queue.pop
|
31
|
+
|
32
|
+
if notification == STOP
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
connection = Push::Daemon.connection_pool.checkout(notification.use_connection)
|
38
|
+
notification.deliver(connection)
|
39
|
+
rescue StandardError => e
|
40
|
+
Push::Daemon.logger.error(e)
|
41
|
+
ensure
|
42
|
+
Push::Daemon.connection_pool.checkin(connection)
|
43
|
+
Push::Daemon.delivery_queue.notification_processed
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,20 @@
|
|
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
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Push
|
2
|
+
module Daemon
|
3
|
+
class DeliveryQueue
|
4
|
+
def initialize
|
5
|
+
@mutex = Mutex.new
|
6
|
+
@num_notifications = 0
|
7
|
+
@queue = Queue.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def push(notification)
|
11
|
+
@mutex.synchronize { @num_notifications += 1 }
|
12
|
+
@queue.push(notification)
|
13
|
+
end
|
14
|
+
|
15
|
+
def pop
|
16
|
+
@queue.pop
|
17
|
+
end
|
18
|
+
|
19
|
+
def notification_processed
|
20
|
+
@mutex.synchronize { @num_notifications -= 1 }
|
21
|
+
end
|
22
|
+
|
23
|
+
def notifications_processed?
|
24
|
+
@mutex.synchronize { @num_notifications == 0 }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Push
|
2
|
+
class DisconnectionError < StandardError
|
3
|
+
attr_reader :code, :description
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@code = nil
|
7
|
+
@description = "APNs disconnected without returning an error."
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"The APNs disconnected without returning an error. This may indicate you are using an invalid certificate for the host."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Push
|
2
|
+
module Daemon
|
3
|
+
class Feeder
|
4
|
+
extend DatabaseReconnectable
|
5
|
+
extend InterruptibleSleep
|
6
|
+
|
7
|
+
def self.name
|
8
|
+
"Feeder"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.start(foreground)
|
12
|
+
reconnect_database unless foreground
|
13
|
+
|
14
|
+
loop do
|
15
|
+
break if @stop
|
16
|
+
enqueue_notifications
|
17
|
+
interruptible_sleep Push::Daemon.configuration[:poll]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.stop
|
22
|
+
@stop = true
|
23
|
+
interrupt_sleep
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def self.enqueue_notifications
|
29
|
+
begin
|
30
|
+
with_database_reconnect_and_retry(name) do
|
31
|
+
if Push::Daemon.delivery_queue.notifications_processed?
|
32
|
+
Push::Message.ready_for_delivery.each do |notification|
|
33
|
+
Push::Daemon.delivery_queue.push(notification)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
rescue StandardError => e
|
38
|
+
Push::Daemon.logger.error(e)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Push
|
2
|
+
module Daemon
|
3
|
+
module InterruptibleSleep
|
4
|
+
def interruptible_sleep(seconds)
|
5
|
+
@_sleep_check, @_sleep_interrupt = IO.pipe
|
6
|
+
IO.select([@_sleep_check], nil, nil, seconds)
|
7
|
+
@_sleep_check.close rescue IOError
|
8
|
+
@_sleep_interrupt.close rescue IOError
|
9
|
+
end
|
10
|
+
|
11
|
+
def interrupt_sleep
|
12
|
+
if @_sleep_interrupt
|
13
|
+
@_sleep_interrupt.close rescue IOError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Push
|
2
|
+
module Daemon
|
3
|
+
class Logger
|
4
|
+
def initialize(options)
|
5
|
+
@options = options
|
6
|
+
log_path = File.join(Rails.root, 'log', 'push.log')
|
7
|
+
@logger = ActiveSupport::BufferedLogger.new(log_path, Rails.logger.level)
|
8
|
+
@logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
|
9
|
+
end
|
10
|
+
|
11
|
+
def info(msg)
|
12
|
+
log(:info, msg)
|
13
|
+
end
|
14
|
+
|
15
|
+
def error(msg, options = {})
|
16
|
+
airbrake_notify(msg) if notify_via_airbrake?(msg, options)
|
17
|
+
log(:error, msg, 'ERROR')
|
18
|
+
end
|
19
|
+
|
20
|
+
def warn(msg)
|
21
|
+
log(:warn, msg, 'WARNING')
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def log(where, msg, prefix = nil)
|
27
|
+
if msg.is_a?(Exception)
|
28
|
+
msg = "#{msg.class.name}, #{msg.message}: #{msg.backtrace.join("\n")}"
|
29
|
+
end
|
30
|
+
|
31
|
+
formatted_msg = "[#{Time.now.to_s(:db)}] "
|
32
|
+
formatted_msg << "[#{prefix}] " if prefix
|
33
|
+
formatted_msg << msg
|
34
|
+
puts formatted_msg if @options[:foreground]
|
35
|
+
@logger.send(where, formatted_msg)
|
36
|
+
end
|
37
|
+
|
38
|
+
def airbrake_notify(e)
|
39
|
+
return unless @options[:airbrake_notify] == true
|
40
|
+
|
41
|
+
if defined?(Airbrake)
|
42
|
+
Airbrake.notify_or_ignore(e)
|
43
|
+
elsif defined?(HoptoadNotifier)
|
44
|
+
HoptoadNotifier.notify_or_ignore(e)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def notify_via_airbrake?(msg, options)
|
49
|
+
msg.is_a?(Exception) && options[:airbrake_notify] != false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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
|
data/lib/push/message.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/errors'
|
3
|
+
require 'push/daemon/database_reconnectable'
|
4
|
+
module Push
|
5
|
+
class Message < ActiveRecord::Base
|
6
|
+
include Push::Daemon::DatabaseReconnectable
|
7
|
+
self.table_name = "push_messages"
|
8
|
+
|
9
|
+
validates :device, :presence => true
|
10
|
+
|
11
|
+
scope :ready_for_delivery, lambda { where('delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', false, false, Time.now) }
|
12
|
+
|
13
|
+
def deliver(connection)
|
14
|
+
begin
|
15
|
+
connection.write(self.to_message)
|
16
|
+
check_for_error(connection)
|
17
|
+
|
18
|
+
# this makes no sense in the rails environment, but it does in the daemon
|
19
|
+
with_database_reconnect_and_retry(connection.name) do
|
20
|
+
self.delivered = true
|
21
|
+
self.delivered_at = Time.now
|
22
|
+
self.save!(:validate => false)
|
23
|
+
end
|
24
|
+
|
25
|
+
Push::Daemon.logger.info("Message #{id} delivered to #{device}")
|
26
|
+
rescue Push::DeliveryError, Push::DisconnectionError => error
|
27
|
+
handle_delivery_error(error, connection)
|
28
|
+
raise
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def handle_delivery_error(error, connection)
|
35
|
+
# this code makes no sense in the rails environment, but it does in the daemon
|
36
|
+
with_database_reconnect_and_retry(connection.name) do
|
37
|
+
self.delivered = false
|
38
|
+
self.delivered_at = nil
|
39
|
+
self.failed = true
|
40
|
+
self.failed_at = Time.now
|
41
|
+
self.error_code = error.code
|
42
|
+
self.error_description = error.description
|
43
|
+
self.save!(:validate => false)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/push/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: push-core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.pre
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tom Pesman
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: &70154092596240 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.1
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70154092596240
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sqlite3
|
27
|
+
requirement: &70154092595780 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70154092595780
|
36
|
+
description: Push daemon for push notification services like APNS and C2DM.
|
37
|
+
email:
|
38
|
+
- tom@tnux.net
|
39
|
+
executables:
|
40
|
+
- push
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- lib/generators/push_generator.rb
|
45
|
+
- lib/generators/templates/create_push.rb
|
46
|
+
- lib/generators/templates/development.rb
|
47
|
+
- lib/generators/templates/production.rb
|
48
|
+
- lib/generators/templates/staging.rb
|
49
|
+
- lib/push.rb
|
50
|
+
- lib/push/daemon.rb
|
51
|
+
- lib/push/daemon/builder.rb
|
52
|
+
- lib/push/daemon/connection_pool.rb
|
53
|
+
- lib/push/daemon/database_reconnectable.rb
|
54
|
+
- lib/push/daemon/delivery_error.rb
|
55
|
+
- lib/push/daemon/delivery_handler.rb
|
56
|
+
- lib/push/daemon/delivery_handler_pool.rb
|
57
|
+
- lib/push/daemon/delivery_queue.rb
|
58
|
+
- lib/push/daemon/disconnection_error.rb
|
59
|
+
- lib/push/daemon/feeder.rb
|
60
|
+
- lib/push/daemon/interruptible_sleep.rb
|
61
|
+
- lib/push/daemon/logger.rb
|
62
|
+
- lib/push/daemon/pool.rb
|
63
|
+
- lib/push/feedback.rb
|
64
|
+
- lib/push/message.rb
|
65
|
+
- lib/push/version.rb
|
66
|
+
- README.md
|
67
|
+
- MIT-LICENSE
|
68
|
+
- bin/push
|
69
|
+
homepage: https://github.com/tompesman/push-core
|
70
|
+
licenses: []
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>'
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 1.3.1
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.5
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Core of the modular push daemon.
|
93
|
+
test_files: []
|