module APN
# Subclass of Resque::Worker which initializes a single TCP socket on creation to communicate with Apple's Push Notification servers.
# Shares this socket with each child process forked off by Resque to complete a job. Socket is closed in the before_unregister_worker
# callback, which gets called on normal or exceptional exits.
#
# End result: single persistent TCP connection to Apple, so they don't ban you for frequently opening and closing connections,
# which they apparently view as a DOS attack.
#
# Accepts :environment
(production vs anything else) and :cert_path
options on initialization. If called in a
# Rails context, will default to RAILS_ENV and RAILS_ROOT/config/certs. :environment will default to development.
# APN::Sender expects two files to exist in the specified :cert_path
directory:
# apn_production.pem
and apn_development.pem
.
#
# If a socket error is encountered, will teardown the connection and retry again twice before admitting defeat.
class Sender < ::Resque::Worker
include APN::Connection::Base
TIMES_TO_RETRY_SOCKET_ERROR = 2
# Send a raw string over the socket to Apple's servers (presumably already formatted by APN::Notification)
def send_to_apple( notification, attempt = 0 )
if attempt > TIMES_TO_RETRY_SOCKET_ERROR
log_and_die("Error with connection to #{apn_host} (retried #{TIMES_TO_RETRY_SOCKET_ERROR} times): #{error}")
end
self.socket.write( notification.to_s )
rescue SocketError => error
log(:error, "Error with connection to #{apn_host} (attempt #{attempt}): #{error}")
# Try reestablishing the connection
teardown_connection
setup_connection
send_to_apple(notification, attempt + 1)
end
protected
def apn_host
@apn_host ||= apn_production? ? "gateway.push.apple.com" : "gateway.sandbox.push.apple.com"
end
def apn_port
2195
end
end
end
__END__
# irb -r 'lib/apple_push_notification'
## To enqueue test job
k = 'ceecdc18 ef17b2d0 745475e0 0a6cd5bf 54534184 ac2649eb 40873c81 ae76dbe8'
c = '0f58e3e2 77237b8f f8213851 c835dee0 376b7a31 9e0484f7 06fe3035 7c5dda2f'
APN.notify k, 'Resque Test'
# If you need to really force quit some screwed up workers
Resque.workers.map{|w| Resque.redis.srem(:workers, w)}
# To run worker from rake task
CERT_PATH=/Users/kali/Code/insurrection/certs/ ENVIRONMENT=production rake apn:work
# To run worker from IRB
Resque.workers.map(&:unregister_worker)
require 'ruby-debug'
worker = APN::Sender.new(:cert_path => '/Users/kali/Code/insurrection/certs/', :environment => :production)
worker.very_verbose = true
worker.work(5)
# To run worker as daemon
args = ['--environment=production', '--cert-path=/Users/kali/Code/insurrection/certs/']
APN::SenderDaemon.new(args).daemonize
# To run daemonized version in Rails app
./script/apn_sender --environment=production start