Permalink
Browse files

initial commit

  • Loading branch information...
tompesman committed Jun 15, 2012
0 parents commit 7fb79836e4fc616b1f23e9ab9bc394e9a40a465a
@@ -0,0 +1,4 @@
+.bundle/
+log/*.log
+pkg/
+Gemfile.lock
23 Gemfile
@@ -0,0 +1,23 @@
+source "http://rubygems.org"
+
+# Declare your gem's dependencies in push.gemspec.
+# Bundler will treat runtime dependencies like base dependencies, and
+# development dependencies will be added by default to the :development group.
+gemspec
+
+gem 'rake'
+gem 'rspec'
+gem 'shoulda'
+gem 'activerecord', :require => 'active_record'
+gem 'pg'
+gem 'mysql2'
+gem 'sqlite3'
+gem 'database_cleaner'
+gem 'simplecov'
+
+group :development do
+ gem 'guard'
+ gem 'guard-rspec'
+ gem 'growl'
+ gem 'webmock'
+end
@@ -0,0 +1,20 @@
+Copyright 2012 YOURNAME
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
+# Push
+
+This project rocks and uses MIT-LICENSE.
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+
+require 'optparse'
+require 'push'
+
+foreground = false
+environment = ARGV[0]
+banner = 'Usage: push <Rails environment> [options]'
+ARGV.options do |opts|
+ opts.banner = banner
+ opts.on('-f', '--foreground', 'Run in the foreground.') { foreground = true }
+ opts.on('-v', '--version', 'Print this version of push.') { puts "push #{Push::VERSION}"; exit }
+ opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
+ opts.parse!
+end
+
+if environment.nil?
+ puts banner
+ exit 1
+end
+
+ENV['RAILS_ENV'] = environment
+load 'config/environment.rb'
+
+require 'push/daemon'
+
+Push::Daemon.start(environment, foreground)
@@ -0,0 +1,22 @@
+class PushGenerator < Rails::Generators::Base
+ include Rails::Generators::Migration
+ source_root File.expand_path('../templates', __FILE__)
+
+ def self.next_migration_number(path)
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
+ end
+
+ def copy_migration
+ migration_dir = File.expand_path("db/migrate")
+
+ if !self.class.migration_exists?(migration_dir, 'create_push')
+ migration_template "create_push.rb", "db/migrate/create_push.rb"
+ end
+ end
+
+ def copy_config
+ copy_file "development.rb", "config/push/development.rb"
+ copy_file "staging.rb", "config/push/staging.rb"
+ copy_file "production.rb", "config/push/production.rb"
+ end
+end
@@ -0,0 +1,33 @@
+class CreatePush < ActiveRecord::Migration
+ def self.up
+ create_table :push_messages do |t|
+ t.string :device, :null => false
+ t.string :type, :null => false
+ t.text :properties, :null => true
+ t.boolean :delivered, :null => false, :default => false
+ t.timestamp :delivered_at, :null => true
+ t.boolean :failed, :null => false, :default => false
+ t.timestamp :failed_at, :null => true
+ t.integer :error_code, :null => true
+ t.string :error_description, :null => true
+ t.timestamp :deliver_after, :null => true
+ t.timestamps
+ end
+
+ add_index :push_messages, [:delivered, :failed, :deliver_after]
+
+ create_table :push_feedback do |t|
+ t.string :device, :null => false
+ t.string :type, :null => false
+ t.timestamp :failed_at, :null => false
+ t.timestamps
+ end
+
+ add_index :push_feedback, :device
+ end
+
+ def self.down
+ drop_table :push_feedback
+ drop_table :push_messages
+ end
+end
@@ -0,0 +1,19 @@
+Push::Daemon::Builder.new do
+ daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
+
+ provider :apns,
+ {
+ :certificate => "development.pem",
+ :certificate_password => "",
+ :sandbox => true,
+ :connections => 3,
+ :feedback_poll => 60
+ }
+
+ provider :c2dm,
+ {
+ :connections => 2,
+ :email => "",
+ :password => ""
+ }
+end
@@ -0,0 +1,19 @@
+Push::Daemon::Builder.new do
+ daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
+
+ provider :apns,
+ {
+ :certificate => "production.pem",
+ :certificate_password => "",
+ :sandbox => false,
+ :connections => 3,
+ :feedback_poll => 60
+ }
+
+ provider :c2dm,
+ {
+ :connections => 2,
+ :email => "",
+ :password => ""
+ }
+end
@@ -0,0 +1,19 @@
+Push::Daemon::Builder.new do
+ daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
+
+ provider :apns,
+ {
+ :certificate => "staging.pem",
+ :certificate_password => "",
+ :sandbox => true,
+ :connections => 3,
+ :feedback_poll => 60
+ }
+
+ provider :c2dm,
+ {
+ :connections => 2,
+ :email => "",
+ :password => ""
+ }
+end
@@ -0,0 +1,4 @@
+require 'active_record'
+require 'push/version'
+require 'push/message'
+require 'push/feedback'
@@ -0,0 +1,124 @@
+require 'thread'
+require 'push/daemon/builder'
+require 'push/daemon/interruptible_sleep'
+require 'push/daemon/delivery_error'
+require 'push/daemon/disconnection_error'
+require 'push/daemon/pool'
+require 'push/daemon/connection_pool'
+require 'push/daemon/database_reconnectable'
+require 'push/daemon/delivery_queue'
+require 'push/daemon/delivery_handler'
+require 'push/daemon/delivery_handler_pool'
+require 'push/daemon/feeder'
+require 'push/daemon/logger'
+
+module Push
+ module Daemon
+ class << self
+ attr_accessor :logger, :configuration, :delivery_queue,
+ :connection_pool, :delivery_handler_pool, :foreground, :providers
+ end
+
+ def self.start(environment, foreground)
+ self.providers = []
+ @foreground = foreground
+ setup_signal_hooks
+
+ require File.join(Rails.root, 'config', 'push', environment + '.rb')
+
+ self.logger = Logger.new(:foreground => foreground, :airbrake_notify => configuration[:airbrake_notify])
+
+ self.delivery_queue = DeliveryQueue.new
+
+ daemonize unless foreground
+
+ write_pid_file
+
+ dbconnections = 0
+ self.connection_pool = ConnectionPool.new
+ self.providers.each do |provider|
+ self.connection_pool.populate(provider)
+ dbconnections += provider.totalconnections
+ end
+
+ rescale_poolsize(dbconnections)
+
+ self.delivery_handler_pool = DeliveryHandlerPool.new(connection_pool.size)
+ delivery_handler_pool.populate
+
+ logger.info('[Daemon] Ready')
+
+ Push::Daemon::Feeder.start(foreground)
+ end
+
+ protected
+
+ def self.rescale_poolsize(size)
+ # 1 feeder + providers
+ size = 1 + size
+
+ h = ActiveRecord::Base.connection_config
+ h[:pool] = size
+ ActiveRecord::Base.establish_connection(h)
+ logger.info("[Daemon] Rescaled ActiveRecord ConnectionPool size to #{size}")
+ end
+
+ def self.setup_signal_hooks
+ @shutting_down = false
+
+ ['SIGINT', 'SIGTERM'].each do |signal|
+ Signal.trap(signal) do
+ handle_shutdown_signal
+ end
+ end
+ end
+
+ def self.handle_shutdown_signal
+ exit 1 if @shutting_down
+ @shutting_down = true
+ shutdown
+ end
+
+ def self.shutdown
+ puts "\nShutting down..."
+ Push::Daemon::Feeder.stop
+ Push::Daemon.delivery_handler_pool.drain if Push::Daemon.delivery_handler_pool
+
+ self.providers.each do |provider|
+ provider.stop
+ end
+
+ delete_pid_file
+ end
+
+ def self.daemonize
+ exit if pid = fork
+ Process.setsid
+ exit if pid = fork
+
+ Dir.chdir '/'
+ File.umask 0000
+
+ STDIN.reopen '/dev/null'
+ STDOUT.reopen '/dev/null', 'a'
+ STDERR.reopen STDOUT
+ end
+
+ def self.write_pid_file
+ if !configuration[:pid_file].blank?
+ begin
+ File.open(configuration[:pid_file], 'w') do |f|
+ f.puts $$
+ end
+ rescue SystemCallError => e
+ logger.error("Failed to write PID to '#{configuration[:pid_file]}': #{e.inspect}")
+ end
+ end
+ end
+
+ def self.delete_pid_file
+ pid_file = configuration[:pid_file]
+ File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
+ end
+ end
+end
@@ -0,0 +1,23 @@
+module Push
+ module Daemon
+ class Builder
+ def initialize(&block)
+ instance_eval(&block) if block_given?
+ end
+
+ def daemon(options)
+ Push::Daemon.configuration = options
+ end
+
+ def provider(klass, options)
+ begin
+ middleware = Push::Daemon.const_get("#{klass}".camelize)
+ rescue NameError
+ raise LoadError, "Could not find matching push provider for #{klass.inspect}. You may need to install an additional gem (such as push-#{klass})."
+ end
+
+ Push::Daemon.providers << middleware.new(options)
+ end
+ end
+ end
+end
@@ -0,0 +1,30 @@
+module Push
+ module Daemon
+ class ConnectionPool
+ def initialize()
+ @connections = Hash.new
+ end
+
+ def populate(provider)
+ @connections[provider.connectiontype.to_s] = Queue.new
+ provider.pushconnections.times do |i|
+ c = provider.connectiontype.new(provider, i+1)
+ c.connect
+ checkin(c)
+ end
+ end
+
+ def checkin(connection)
+ @connections[connection.class.to_s].push(connection)
+ end
+
+ def checkout(notification_type)
+ @connections[notification_type.to_s].pop
+ end
+
+ def size
+ @connections.size
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 7fb7983

Please sign in to comment.