pushr-core 1.0.0.pre.1

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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +156 -0
  4. data/bin/pushr +43 -0
  5. data/lib/generators/templates/feedback_processor.rb +33 -0
  6. data/lib/pushr/configuration.rb +49 -0
  7. data/lib/pushr/core.rb +54 -0
  8. data/lib/pushr/daemon.rb +116 -0
  9. data/lib/pushr/daemon/app.rb +71 -0
  10. data/lib/pushr/daemon/delivery_error.rb +19 -0
  11. data/lib/pushr/daemon/delivery_handler.rb +41 -0
  12. data/lib/pushr/daemon/feedback_handler.rb +38 -0
  13. data/lib/pushr/daemon/logger.rb +57 -0
  14. data/lib/pushr/feedback.rb +37 -0
  15. data/lib/pushr/message.rb +36 -0
  16. data/lib/pushr/redis_connection.rb +38 -0
  17. data/lib/pushr/version.rb +3 -0
  18. data/spec/lib/pushr/configuration_spec.rb +58 -0
  19. data/spec/lib/pushr/daemon/app_spec.rb +55 -0
  20. data/spec/lib/pushr/daemon/delivery_error_spec.rb +15 -0
  21. data/spec/lib/pushr/daemon/delivery_handler_spec.rb +48 -0
  22. data/spec/lib/pushr/daemon/feedback_handler_spec.rb +42 -0
  23. data/spec/lib/pushr/daemon/logger_spec.rb +25 -0
  24. data/spec/lib/pushr/daemon_spec.rb +15 -0
  25. data/spec/lib/pushr/feedback_spec.rb +27 -0
  26. data/spec/lib/pushr/message_spec.rb +33 -0
  27. data/spec/lib/pushr/redis_connection_spec.rb +14 -0
  28. data/spec/spec_helper.rb +30 -0
  29. data/spec/support/logger.rb +11 -0
  30. data/spec/support/pushr_configuration_dummy.rb +22 -0
  31. data/spec/support/pushr_connection_dummy.rb +22 -0
  32. data/spec/support/pushr_dummy.rb +16 -0
  33. data/spec/support/pushr_feedback_dummy.rb +11 -0
  34. data/spec/support/pushr_feedback_processor_dummy.rb +9 -0
  35. data/spec/support/pushr_invalid_configuration_dummy.rb +8 -0
  36. data/spec/support/pushr_message_dummy.rb +13 -0
  37. metadata +281 -0
@@ -0,0 +1,71 @@
1
+ module Pushr
2
+ module Daemon
3
+ class App
4
+ @apps = {}
5
+
6
+ class << self
7
+ attr_reader :apps
8
+
9
+ def load
10
+ Configuration.all.each do |config|
11
+ @apps["#{config.app}:#{config.name}"] = App.new(config) if config.enabled == true
12
+ end
13
+ end
14
+
15
+ def total_connections
16
+ @apps.values.map(&:connections).inject(0, :+)
17
+ end
18
+
19
+ def start
20
+ @apps.values.map(&:start)
21
+ end
22
+
23
+ def stop
24
+ @apps.values.map(&:stop)
25
+ end
26
+ end
27
+
28
+ def initialize(config)
29
+ @config = config
30
+ @handlers = []
31
+ @provider = nil
32
+ end
33
+
34
+ def connections
35
+ @config.connections
36
+ end
37
+
38
+ def start
39
+ @provider = load_provider(@config.name, @config)
40
+
41
+ @config.connections.times do |i|
42
+ connection = @provider.connectiontype.new(@config, i + 1)
43
+ connection.connect
44
+
45
+ handler = DeliveryHandler.new("pushr:#{@config.app}:#{@config.name}", connection, @config.app, i + 1)
46
+ handler.start
47
+ @handlers << handler
48
+ end
49
+ end
50
+
51
+ def stop
52
+ @handlers.map(&:stop)
53
+ @provider.stop
54
+ end
55
+
56
+ protected
57
+
58
+ def load_provider(klass, options)
59
+ begin
60
+ middleware = Pushr::Daemon.const_get("#{klass}".camelize)
61
+ rescue NameError
62
+ message = "Could not find matching push provider for #{klass.inspect}. " \
63
+ "You may need to install an additional gem (such as push-#{klass})."
64
+ raise LoadError, message
65
+ end
66
+
67
+ middleware.new(options)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ module Pushr
2
+ module Daemon
3
+ class DeliveryError < StandardError
4
+ attr_reader :code, :message, :description, :source, :notify
5
+
6
+ def initialize(code, message, description, source, notify = true)
7
+ @code = code
8
+ @message = message
9
+ @description = description
10
+ @source = source
11
+ @notify = notify
12
+ end
13
+
14
+ def message
15
+ "Unable to deliver message #{@message.inspect}, received #{@source} error #{@code} (#{@description})"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ module Pushr
2
+ module Daemon
3
+ class DeliveryHandler
4
+ attr_reader :name
5
+
6
+ def initialize(queue_name, connection, name, i)
7
+ @queue_name = queue_name
8
+ @connection = connection
9
+ @name = "#{name}: DeliveryHandler #{i}"
10
+ Pushr::Daemon.logger.info "[#{@name}] listening to #{@queue_name}"
11
+ end
12
+
13
+ def start
14
+ Thread.new do
15
+ loop do
16
+ handle_next
17
+ break if @stop
18
+ end
19
+ end
20
+ end
21
+
22
+ def stop
23
+ @stop = true
24
+ end
25
+
26
+ protected
27
+
28
+ def handle_next
29
+ message = Pushr::Message.next(@queue_name)
30
+ return if message.nil?
31
+
32
+ Pushr::Core.instrument('message', app: message.app, type: message.type) do
33
+ @connection.write(message)
34
+ Pushr::Daemon.logger.info("[#{@connection.name}] Message delivered to #{message.device}")
35
+ end
36
+ rescue => e
37
+ Pushr::Daemon.logger.error(e)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ module Pushr
2
+ module Daemon
3
+ class FeedbackHandler
4
+ attr_reader :name, :processor, :processor_path
5
+
6
+ def initialize(processor_path)
7
+ @name = 'FeedbackHandler'
8
+ @processor_path = processor_path
9
+ end
10
+
11
+ def start
12
+ return unless @processor_path
13
+ require "#{Dir.pwd}/#{@processor_path}"
14
+ @processor = Pushr::FeedbackProcessor.new
15
+
16
+ Thread.new do
17
+ loop do
18
+ handle_next
19
+ break if @stop
20
+ end
21
+ end
22
+ end
23
+
24
+ def stop
25
+ @stop = true
26
+ end
27
+
28
+ protected
29
+
30
+ def handle_next
31
+ feedback = Pushr::Feedback.next
32
+ @processor.process(feedback) if feedback
33
+ rescue => e
34
+ Pushr::Daemon.logger.error(e)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,57 @@
1
+ module Pushr
2
+ module Daemon
3
+ class Logger
4
+ def initialize(options)
5
+ @options = options
6
+
7
+ if @options[:foreground]
8
+ STDOUT.sync = true
9
+ @logger = ::Logger.new(STDOUT)
10
+ else
11
+ log_dir = File.join(Dir.pwd, 'log')
12
+ FileUtils.mkdir_p(log_dir)
13
+ log = File.open(File.join(log_dir, 'pushr.log'), 'a')
14
+ log.sync = true
15
+ @logger = ::Logger.new(log)
16
+ end
17
+
18
+ @logger.level = ::Logger::INFO
19
+ @logger.formatter = proc do |severity, datetime, progname, msg|
20
+ "[#{datetime}] #{severity}: #{msg}\n"
21
+ end
22
+ end
23
+
24
+ def info(msg)
25
+ log(::Logger::INFO, msg)
26
+ end
27
+
28
+ def error(msg)
29
+ error_notification(msg)
30
+ log(::Logger::ERROR, msg, 'ERROR')
31
+ end
32
+
33
+ def warn(msg)
34
+ log(::Logger::WARN, msg, 'WARNING')
35
+ end
36
+
37
+ private
38
+
39
+ def log(level, msg, prefix = nil)
40
+ if msg.is_a?(Exception)
41
+ msg = "#{msg.class.name}, #{msg.message}: #{msg.backtrace.join("\n") if msg.backtrace}"
42
+ end
43
+ @logger.add(level, msg)
44
+ end
45
+
46
+ def error_notification(e)
47
+ if do_error_notification?(e) && defined?(Airbrake)
48
+ Airbrake.notify_or_ignore(e)
49
+ end
50
+ end
51
+
52
+ def do_error_notification?(e)
53
+ @options[:error_notification] && ((e.is_a?(DeliveryError) && e.notify) || e.is_a?(Exception))
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,37 @@
1
+ module Pushr
2
+ class Feedback
3
+ include ActiveModel::Validations
4
+ validates :app, presence: true
5
+ validates :device, presence: true
6
+ validates :follow_up, presence: true
7
+ validates :failed_at, presence: true
8
+
9
+ def initialize(attributes = {})
10
+ attributes.each do |name, value|
11
+ send("#{name}=", value)
12
+ end
13
+ end
14
+
15
+ def save
16
+ if valid?
17
+ Pushr::Core.redis { |conn| conn.rpush('pushr:feedback', to_json) }
18
+ return true
19
+ else
20
+ return false
21
+ end
22
+ end
23
+
24
+ def self.next(timeout = 3)
25
+ Pushr::Core.redis do |conn|
26
+ feedback = conn.blpop('pushr:feedback', timeout: timeout)
27
+ return instantiate(feedback[1]) if feedback
28
+ end
29
+ end
30
+
31
+ def self.instantiate(config)
32
+ hsh = ::MultiJson.load(config)
33
+ klass = hsh['type'].split('::').reduce(Object) { |a, e| a.const_get e }
34
+ klass.new(hsh)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ module Pushr
2
+ class Message
3
+ include ActiveModel::Validations
4
+
5
+ validates :app, presence: true
6
+
7
+ def initialize(attributes = {})
8
+ attributes.each do |name, value|
9
+ send("#{name}=", value)
10
+ end
11
+ end
12
+
13
+ def save
14
+ if valid?
15
+ Pushr::Core.redis { |conn| conn.rpush("pushr:#{app}:#{self.class::POSTFIX}", to_json) }
16
+ return true
17
+ else
18
+ return false
19
+ end
20
+ end
21
+
22
+ def self.next(queue_name, timeout = 3)
23
+ Pushr::Core.redis do |conn|
24
+ message = conn.blpop(queue_name, timeout: timeout)
25
+ return instantiate(message[1]) if message
26
+ end
27
+ end
28
+
29
+ def self.instantiate(message)
30
+ return nil unless message
31
+ hsh = ::MultiJson.load(message)
32
+ klass = hsh['type'].split('::').reduce(Object) { |a, e| a.const_get e }
33
+ klass.new(hsh)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ require 'connection_pool'
2
+ require 'redis'
3
+ require 'redis/namespace'
4
+
5
+ module Pushr
6
+ class RedisConnection
7
+ def self.create(options = {})
8
+ url = options[:url] || determine_redis_provider || 'redis://localhost:6379/0'
9
+ driver = options[:driver] || 'ruby'
10
+ # need a connection for Fetcher and Retry
11
+ # size = options[:size] || (Pushr.server? ? (Pushr.options[:concurrency] + 2) : 5)
12
+ size = options[:size] || 5
13
+ namespace = options[:namespace] || Pushr::Core.options[:namespace]
14
+
15
+ ConnectionPool.new(timeout: 1, size: size) do
16
+ build_client(url, namespace, driver)
17
+ end
18
+ end
19
+
20
+ def self.build_client(url, namespace, driver)
21
+ client = Redis.connect(url: url, driver: driver)
22
+ if namespace
23
+ Redis::Namespace.new(namespace, redis: client)
24
+ else
25
+ client
26
+ end
27
+ end
28
+ private_class_method :build_client
29
+
30
+ # Not public
31
+ def self.determine_redis_provider
32
+ return ENV['PUSHR_URL'] if ENV['PUSHR_URL']
33
+ return ENV['REDISTOGO_URL'] if ENV['REDISTOGO_URL']
34
+ provider = ENV['REDIS_PROVIDER'] || 'REDIS_URL'
35
+ ENV[provider]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Pushr
2
+ VERSION = '1.0.0.pre.1'
3
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pushr::Configuration do
4
+
5
+ before(:each) do
6
+ Pushr::Core.configure do |config|
7
+ config.redis = ConnectionPool.new(size: 1, timeout: 1) { MockRedis.new }
8
+ end
9
+ end
10
+
11
+ describe 'all' do
12
+ it 'returns all configurations' do
13
+ expect(Pushr::Configuration.all).to eql([])
14
+ end
15
+ end
16
+
17
+ describe 'create' do
18
+ it 'should create a configuration' do
19
+ config = Pushr::ConfigurationDummy.new(app: 'app_name', connections: 2, enabled: true)
20
+ expect(config.key).to eql('app_name:dummy')
21
+ end
22
+ end
23
+
24
+ describe 'save' do
25
+ let(:config) { Pushr::ConfigurationDummy.new(app: 'app_name', connections: 2, enabled: true) }
26
+ let(:config_invalid) { Pushr::ConfigurationDummy.new }
27
+ it 'should return true' do
28
+ expect(config.save).to eql true
29
+ end
30
+
31
+ it 'should return false' do
32
+ expect(config_invalid.save).to eql false
33
+ end
34
+
35
+ it 'should save a configuration' do
36
+ config.save
37
+ expect(Pushr::Configuration.all.count).to eql(1)
38
+ end
39
+ end
40
+
41
+ describe 'find' do
42
+ let!(:config) { Pushr::ConfigurationDummy.new(app: 'app_name', connections: 2, enabled: true) }
43
+ it 'should find a configuration' do
44
+ config.save
45
+ expect(Pushr::Configuration.find(config.key)).to be_kind_of(Pushr::ConfigurationDummy)
46
+ end
47
+ end
48
+
49
+ describe 'delete' do
50
+ let!(:config) { Pushr::ConfigurationDummy.new(app: 'app_name', connections: 2, enabled: true) }
51
+ it 'should remove a configuration' do
52
+ config.save
53
+ expect(Pushr::Configuration.all.count).to eql(1)
54
+ config.delete
55
+ expect(Pushr::Configuration.all.count).to eql(0)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+ require 'pushr/daemon'
3
+
4
+ describe Pushr::Daemon::App do
5
+
6
+ before(:each) do
7
+ Pushr::Core.configure do |config|
8
+ config.redis = ConnectionPool.new(size: 1, timeout: 1) { MockRedis.new }
9
+ end
10
+
11
+ logger = double('logger')
12
+ allow(logger).to receive(:info)
13
+ allow(logger).to receive(:error)
14
+ allow(logger).to receive(:warn)
15
+ Pushr::Daemon.logger = logger
16
+ end
17
+
18
+ let(:config) { Pushr::ConfigurationDummy.new(app: 'app_name', connections: 1, enabled: true) }
19
+ describe 'self' do
20
+ before(:each) do
21
+ config.save
22
+ Pushr::Daemon::App.load
23
+ end
24
+
25
+ it 'should load show total_connections' do
26
+ expect(Pushr::Daemon::App.total_connections).to eql(1)
27
+ end
28
+
29
+ it 'should load app' do
30
+ expect(Pushr::Daemon::App.apps.count).to eql(1)
31
+ end
32
+
33
+ it 'should start/stop app' do
34
+ Pushr::Daemon::App.start
35
+ Pushr::Daemon::App.stop
36
+ end
37
+ end
38
+
39
+ describe 'class' do
40
+ it 'should start configuration' do
41
+ expect_any_instance_of(Pushr::Daemon::DeliveryHandler).to receive(:start)
42
+ config.save
43
+ app = Pushr::Daemon::App.new(config)
44
+ app.start
45
+ app.stop
46
+ end
47
+
48
+ it 'should not start configuration' do
49
+ config = Pushr::InvalidConfigurationDummy.new(app: 'app_name', connections: 2, enabled: true)
50
+ config.save
51
+ app = Pushr::Daemon::App.new(config)
52
+ expect { app.start }.to raise_error(LoadError)
53
+ end
54
+ end
55
+ end