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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +156 -0
- data/bin/pushr +43 -0
- data/lib/generators/templates/feedback_processor.rb +33 -0
- data/lib/pushr/configuration.rb +49 -0
- data/lib/pushr/core.rb +54 -0
- data/lib/pushr/daemon.rb +116 -0
- data/lib/pushr/daemon/app.rb +71 -0
- data/lib/pushr/daemon/delivery_error.rb +19 -0
- data/lib/pushr/daemon/delivery_handler.rb +41 -0
- data/lib/pushr/daemon/feedback_handler.rb +38 -0
- data/lib/pushr/daemon/logger.rb +57 -0
- data/lib/pushr/feedback.rb +37 -0
- data/lib/pushr/message.rb +36 -0
- data/lib/pushr/redis_connection.rb +38 -0
- data/lib/pushr/version.rb +3 -0
- data/spec/lib/pushr/configuration_spec.rb +58 -0
- data/spec/lib/pushr/daemon/app_spec.rb +55 -0
- data/spec/lib/pushr/daemon/delivery_error_spec.rb +15 -0
- data/spec/lib/pushr/daemon/delivery_handler_spec.rb +48 -0
- data/spec/lib/pushr/daemon/feedback_handler_spec.rb +42 -0
- data/spec/lib/pushr/daemon/logger_spec.rb +25 -0
- data/spec/lib/pushr/daemon_spec.rb +15 -0
- data/spec/lib/pushr/feedback_spec.rb +27 -0
- data/spec/lib/pushr/message_spec.rb +33 -0
- data/spec/lib/pushr/redis_connection_spec.rb +14 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/logger.rb +11 -0
- data/spec/support/pushr_configuration_dummy.rb +22 -0
- data/spec/support/pushr_connection_dummy.rb +22 -0
- data/spec/support/pushr_dummy.rb +16 -0
- data/spec/support/pushr_feedback_dummy.rb +11 -0
- data/spec/support/pushr_feedback_processor_dummy.rb +9 -0
- data/spec/support/pushr_invalid_configuration_dummy.rb +8 -0
- data/spec/support/pushr_message_dummy.rb +13 -0
- 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,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
|