pushr-core 1.0.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|