diff --git a/Gemfile b/Gemfile index 734ae30..de0eacd 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,8 @@ platform :ruby do gem 'mysql' gem 'yajl-ruby' gem 'sqlite3' + gem 'mongoid' + gem 'mongoid-autoinc' #gem 'perftools.rb' end diff --git a/lib/generators/rapns_generator.rb b/lib/generators/rapns_generator.rb index 2806ffb..6665b32 100644 --- a/lib/generators/rapns_generator.rb +++ b/lib/generators/rapns_generator.rb @@ -2,23 +2,27 @@ class RapnsGenerator < Rails::Generators::Base include Rails::Generators::Migration source_root File.expand_path('../templates', __FILE__) + class_option :mongoid, type: :boolean, default: false + def self.next_migration_number(path) @time ||= Time.now.utc @calls ||= -1 @calls += 1 (@time + @calls.seconds).strftime("%Y%m%d%H%M%S") end - + def copy_migration - add_rapns_migration('create_rapns_notifications') - add_rapns_migration('create_rapns_feedback') - add_rapns_migration('add_alert_is_json_to_rapns_notifications') - add_rapns_migration('add_app_to_rapns') - add_rapns_migration('create_rapns_apps') - add_rapns_migration('add_gcm') + if !options.mongoid? + add_rapns_migration('create_rapns_notifications') + add_rapns_migration('create_rapns_feedback') + add_rapns_migration('add_alert_is_json_to_rapns_notifications') + add_rapns_migration('add_app_to_rapns') + add_rapns_migration('create_rapns_apps') + add_rapns_migration('add_gcm') + end end - def copy_config + def copy_config copy_file 'rapns.rb', 'config/initializers/rapns.rb' end diff --git a/lib/generators/templates/rapns.rb b/lib/generators/templates/rapns.rb index cb3b103..bd52472 100644 --- a/lib/generators/templates/rapns.rb +++ b/lib/generators/templates/rapns.rb @@ -2,6 +2,9 @@ Rapns.configure do |config| + # Setup orm backend (default to active_record) + # require 'rapns/orm/{active_record|mongoid}' + # Run in the foreground? # config.foreground = false diff --git a/lib/rapns.rb b/lib/rapns.rb index 604465c..1ea93d6 100644 --- a/lib/rapns.rb +++ b/lib/rapns.rb @@ -1,43 +1,56 @@ -require 'active_record' require 'multi_json' +require 'rapns/version' +require 'rapns/deprecation' +require 'rapns/deprecatable' +require 'rapns/logger' +require 'rapns/multi_json_helper' +require 'rapns/configuration' + module Rapns + + def self.init_orm + if Rapns.config.store == :active_record + require 'rapns/orm/active_record' + elsif Rapns.config.store == :mongoid + require 'rapns/orm/mongoid' + else + raise "Unsupported store: #{Rapns.config.store.to_s}" + end + + require 'rapns/notification' + require 'rapns/app' + require 'rapns/apns/binary_notification_validator' + require 'rapns/apns/device_token_format_validator' + require 'rapns/apns/notification' + require 'rapns/apns/feedback' + require 'rapns/apns/app' + + require 'rapns/gcm/expiry_collapse_key_mutual_inclusion_validator' + require 'rapns/gcm/payload_data_size_validator' + require 'rapns/gcm/registration_ids_count_validator' + require 'rapns/gcm/notification' + require 'rapns/gcm/app' + + end + def self.attr_accessible_available? require 'rails' ::Rails::VERSION::STRING < '4' end + + def self.jruby? + defined? JRUBY_VERSION + end end -require 'rapns/version' -require 'rapns/deprecation' -require 'rapns/deprecatable' -require 'rapns/logger' -require 'rapns/multi_json_helper' -require 'rapns/notification' -require 'rapns/app' -require 'rapns/configuration' require 'rapns/reflection' require 'rapns/embed' require 'rapns/push' require 'rapns/apns_feedback' require 'rapns/upgraded' -require 'rapns/apns/binary_notification_validator' -require 'rapns/apns/device_token_format_validator' -require 'rapns/apns/notification' -require 'rapns/apns/feedback' -require 'rapns/apns/app' - -require 'rapns/gcm/expiry_collapse_key_mutual_inclusion_validator' -require 'rapns/gcm/payload_data_size_validator' -require 'rapns/gcm/registration_ids_count_validator' -require 'rapns/gcm/notification' -require 'rapns/gcm/app' - module Rapns - def self.jruby? - defined? JRUBY_VERSION - end def self.require_for_daemon require 'rapns/daemon' diff --git a/lib/rapns/apns/feedback.rb b/lib/rapns/apns/feedback.rb index 87c11cc..5b4941f 100644 --- a/lib/rapns/apns/feedback.rb +++ b/lib/rapns/apns/feedback.rb @@ -1,8 +1,18 @@ module Rapns module Apns - class Feedback < ActiveRecord::Base - self.table_name = 'rapns_feedback' - + class Feedback < Rapns::RecordBase + if Rapns.config.store == :active_record + self.table_name = 'rapns_feedback' + else + + field :device_token, type: String + field :failed_at, type: DateTime + + belongs_to :app, class_name: 'Rapns::App' + + index({device_token: 1}) + end + if Rapns.attr_accessible_available? attr_accessible :device_token, :failed_at, :app end diff --git a/lib/rapns/apns/notification.rb b/lib/rapns/apns/notification.rb index b8c56a9..1349607 100644 --- a/lib/rapns/apns/notification.rb +++ b/lib/rapns/apns/notification.rb @@ -76,9 +76,13 @@ def as_json end def to_binary(options = {}) - id_for_pack = options[:for_validation] ? 0 : id + if Rapns.config.store == :active_record + id_for_pack = options[:for_validation] ? 0 : id + else + id_for_pack = options[:for_validation] ? 0 : validation_id + end [1, id_for_pack, expiry, 0, 32, device_token, payload_size, payload].pack("cNNccH*na*") - end + end def data=(attrs) return unless attrs diff --git a/lib/rapns/app.rb b/lib/rapns/app.rb index 5c8aa80..1773ac2 100644 --- a/lib/rapns/app.rb +++ b/lib/rapns/app.rb @@ -1,7 +1,19 @@ module Rapns - class App < ActiveRecord::Base - self.table_name = 'rapns_apps' - + class App < Rapns::RecordBase + if Rapns.config.store == :active_record + self.table_name = 'rapns_apps' + else + + field :name, type: String + field :environment, type: String + field :certificate, type: String + field :password, type: String + field :connections, type: Integer, default: 1 + field :auth_key, type: String + + has_many :feedbacks, :class_name => 'Rapns::Apns::Feedback' + end + if Rapns.attr_accessible_available? attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key end diff --git a/lib/rapns/configuration.rb b/lib/rapns/configuration.rb index d30f222..2e7382f 100644 --- a/lib/rapns/configuration.rb +++ b/lib/rapns/configuration.rb @@ -5,6 +5,7 @@ def self.config def self.configure yield config if block_given? + Rapns.init_orm end CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll, :embedded, diff --git a/lib/rapns/daemon/apns/delivery.rb b/lib/rapns/daemon/apns/delivery.rb index afe51f1..763fa37 100644 --- a/lib/rapns/daemon/apns/delivery.rb +++ b/lib/rapns/daemon/apns/delivery.rb @@ -42,9 +42,17 @@ def check_for_error if tuple = @connection.read(ERROR_TUPLE_BYTES) cmd, code, notification_id = tuple.unpack("ccN") - + description = APN_ERRORS[code.to_i] || "Unknown error. Possible rapns bug?" - error = Rapns::DeliveryError.new(code, notification_id, description) + + if Rapns.config.store == :active_record + error = Rapns::DeliveryError.new(code, notification_id, description) + else + notification = Rapns::Apns::Notification.where(validation_id: notification_id).first + error = Rapns::DeliveryError.new(code, notification ? notification.id : 0, description) + end + + error else error = Rapns::Apns::DisconnectionError.new end diff --git a/lib/rapns/daemon/store/mongoid.rb b/lib/rapns/daemon/store/mongoid.rb new file mode 100644 index 0000000..173f415 --- /dev/null +++ b/lib/rapns/daemon/store/mongoid.rb @@ -0,0 +1,60 @@ +require 'mongoid' + +module Rapns + module Daemon + module Store + class Mongoid + + def deliverable_notifications(apps) + batch_size = Rapns.config.batch_size + relation = Rapns::Notification.ready_for_delivery.for_apps(apps) + relation = relation.limit(batch_size) unless Rapns.config.push + relation.to_a + end + + def retry_after(notification, deliver_after) + notification.retries += 1 + notification.deliver_after = deliver_after + notification.save!(:validate => false) + end + + def mark_delivered(notification) + notification.delivered = true + notification.delivered_at = Time.now + notification.save!(:validate => false) + end + + def mark_failed(notification, code, description) + notification.delivered = false + notification.delivered_at = nil + notification.failed = true + notification.failed_at = Time.now + notification.error_code = code + notification.error_description = description + notification.save!(:validate => false) + end + + def create_apns_feedback(failed_at, device_token, app) + Rapns::Apns::Feedback.create!(:failed_at => failed_at, + :device_token => device_token, :app => app) + end + + def create_gcm_notification(attrs, data, registration_ids, deliver_after, app) + notification = Rapns::Gcm::Notification.new + notification.assign_attributes(attrs) + notification.data = data + notification.registration_ids = registration_ids + notification.deliver_after = deliver_after + notification.app = app + notification.save! + notification + end + + def after_daemonize + + end + + end + end + end +end diff --git a/lib/rapns/notification.rb b/lib/rapns/notification.rb index 6929577..b15b236 100644 --- a/lib/rapns/notification.rb +++ b/lib/rapns/notification.rb @@ -1,11 +1,60 @@ module Rapns - class Notification < ActiveRecord::Base + class Notification < Rapns::RecordBase include Rapns::MultiJsonHelper - self.table_name = 'rapns_notifications' - - # TODO: Dump using multi json. - serialize :registration_ids + if Rapns.config.store == :active_record + self.table_name = 'rapns_notifications' + + scope :ready_for_delivery, lambda { + where('delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', + false, false, Time.now) + } + + scope :for_apps, lambda { |apps| + where('app_id IN (?)', apps.map(&:id)) + } + + # TODO: Dump using multi json. + serialize :registration_ids + + else + include Mongoid::Autoinc + + field :badge, type: Integer + field :device_token, type: String + field :sound, type: String, default: "default" + field :alert, type: String + field :data, type: String + field :expiry, type: Integer, default: 86400 + field :delivered, type: Boolean, default: false + field :delivered_at, type: DateTime + field :failed, type: Boolean, default: false + field :failed_at, type: DateTime + field :deliver_after, type: DateTime + field :alert_is_json, type: Boolean, default: false + field :collapse_key, type: String + field :delay_while_idle, type: Boolean, default: false + field :registration_ids, type: Array + field :app_id, type: Integer + field :retries, type: Integer, default: 0 + field :error_code, type: Integer + field :error_description, type: String + field :validation_id, type: Integer + + increments :validation_id + + index({app_id: 1, delivered: -1, failed: -1, deliver_after: -1}) + index({delivered: -1, failed: -1, deliver_after: -1}) + + scope :ready_for_delivery, lambda { + where({"$and" => [delivered: false, failed: false, "$or" => [{"$deliver_after.ne" => nil}, deliver_after: {"$lt" => Time.now}]]}) + } + + scope :for_apps, lambda { |apps| + where(app_id: {"$in" => apps.map(&:id)}) + } + + end belongs_to :app, :class_name => 'Rapns::App' @@ -17,16 +66,7 @@ class Notification < ActiveRecord::Base validates :expiry, :numericality => true, :allow_nil => true validates :app, :presence => true - - scope :ready_for_delivery, lambda { - where('delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', - false, false, Time.now) - } - - scope :for_apps, lambda { |apps| - where('app_id IN (?)', apps.map(&:id)) - } - + def initialize(*args) attributes = args.first if attributes.is_a?(Hash) && attributes.keys.include?(:attributes_for_device) diff --git a/lib/rapns/orm/active_record.rb b/lib/rapns/orm/active_record.rb new file mode 100644 index 0000000..508de21 --- /dev/null +++ b/lib/rapns/orm/active_record.rb @@ -0,0 +1,7 @@ +require 'active_record' + +module Rapns + class RecordBase < ActiveRecord::Base + self.abstract_class = true + end +end diff --git a/lib/rapns/orm/mongoid.rb b/lib/rapns/orm/mongoid.rb new file mode 100644 index 0000000..6514696 --- /dev/null +++ b/lib/rapns/orm/mongoid.rb @@ -0,0 +1,13 @@ +require 'mongoid' +require 'autoinc' + +module Rapns + class RecordBase + include Mongoid::Document + include Mongoid::Timestamps + + store_in collection: 'rapns' + end + + self.config.store = :mongoid +end \ No newline at end of file diff --git a/rapns.gemspec b/rapns.gemspec index 8fb6911..d3ab8f1 100644 --- a/rapns.gemspec +++ b/rapns.gemspec @@ -17,6 +17,7 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] s.add_dependency "multi_json", "~> 1.0" + s.add_dependency "mongoid-autoinc" s.add_dependency "net-http-persistent" if defined? JRUBY_VERSION diff --git a/spec/unit/configuration_spec.rb b/spec/unit/configuration_spec.rb index ea5a2e9..7365fb0 100644 --- a/spec/unit/configuration_spec.rb +++ b/spec/unit/configuration_spec.rb @@ -1,7 +1,7 @@ require 'unit_spec_helper' describe Rapns do - let(:config) { stub } + let(:config) { double(:store => :active_record ) } before { Rapns.stub(:config => config) } diff --git a/spec/unit/daemon/apns/delivery_spec.rb b/spec/unit/daemon/apns/delivery_spec.rb index 4106b0e..d4b05df 100644 --- a/spec/unit/daemon/apns/delivery_spec.rb +++ b/spec/unit/daemon/apns/delivery_spec.rb @@ -4,7 +4,7 @@ let(:app) { stub(:name => 'MyApp') } let(:notification) { stub.as_null_object } let(:logger) { stub(:error => nil, :info => nil) } - let(:config) { stub(:check_for_errors => true) } + let(:config) { stub(:check_for_errors => true, :store => :active_record) } let(:connection) { stub(:select => false, :write => nil, :reconnect => nil, :close => nil, :connect => nil) } let(:delivery) { Rapns::Daemon::Apns::Delivery.new(app, connection, notification) } let(:store) { stub(:mark_failed => nil, :mark_delivered => nil) } diff --git a/spec/unit/daemon/store/active_record_spec.rb b/spec/unit/daemon/store/active_record_spec.rb index 1bb6444..0585f5e 100644 --- a/spec/unit/daemon/store/active_record_spec.rb +++ b/spec/unit/daemon/store/active_record_spec.rb @@ -1,5 +1,4 @@ require 'unit_spec_helper' -require 'rapns/daemon/store/active_record' describe Rapns::Daemon::Store::ActiveRecord do let(:app) { Rapns::Apns::App.create!(:name => 'my_app', :environment => 'development', :certificate => TEST_CERT) } diff --git a/spec/unit_spec_helper.rb b/spec/unit_spec_helper.rb index 8ce3d94..dcc3b5a 100644 --- a/spec/unit_spec_helper.rb +++ b/spec/unit_spec_helper.rb @@ -62,6 +62,10 @@ require 'rapns' require 'rapns/daemon' +Rapns.configure do |config| + require 'rapns/orm/active_record' +end + Rapns::Notification.reset_column_information Rapns::App.reset_column_information Rapns::Apns::Feedback.reset_column_information