fluq 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +39 -0
- data/MIT-LICENCE +19 -0
- data/README.md +10 -0
- data/Rakefile +11 -0
- data/benchmark/logging.rb +37 -0
- data/benchmark/socket.rb +52 -0
- data/bin/fluq-rb +8 -0
- data/examples/common.rb +3 -0
- data/examples/simple.rb +5 -0
- data/fluq.gemspec +33 -0
- data/lib/fluq.rb +50 -0
- data/lib/fluq/buffer.rb +6 -0
- data/lib/fluq/buffer/base.rb +51 -0
- data/lib/fluq/buffer/file.rb +68 -0
- data/lib/fluq/cli.rb +142 -0
- data/lib/fluq/dsl.rb +49 -0
- data/lib/fluq/dsl/options.rb +27 -0
- data/lib/fluq/error.rb +2 -0
- data/lib/fluq/event.rb +55 -0
- data/lib/fluq/feed.rb +6 -0
- data/lib/fluq/feed/base.rb +18 -0
- data/lib/fluq/feed/json.rb +28 -0
- data/lib/fluq/feed/msgpack.rb +27 -0
- data/lib/fluq/feed/tsv.rb +30 -0
- data/lib/fluq/handler.rb +6 -0
- data/lib/fluq/handler/base.rb +80 -0
- data/lib/fluq/handler/log.rb +67 -0
- data/lib/fluq/handler/null.rb +4 -0
- data/lib/fluq/input.rb +6 -0
- data/lib/fluq/input/base.rb +59 -0
- data/lib/fluq/input/socket.rb +50 -0
- data/lib/fluq/input/socket/connection.rb +41 -0
- data/lib/fluq/mixins.rb +6 -0
- data/lib/fluq/mixins/loggable.rb +7 -0
- data/lib/fluq/mixins/logger.rb +26 -0
- data/lib/fluq/reactor.rb +76 -0
- data/lib/fluq/testing.rb +26 -0
- data/lib/fluq/url.rb +16 -0
- data/lib/fluq/version.rb +3 -0
- data/spec/fluq/buffer/base_spec.rb +21 -0
- data/spec/fluq/buffer/file_spec.rb +47 -0
- data/spec/fluq/dsl/options_spec.rb +24 -0
- data/spec/fluq/dsl_spec.rb +43 -0
- data/spec/fluq/event_spec.rb +25 -0
- data/spec/fluq/feed/base_spec.rb +15 -0
- data/spec/fluq/feed/json_spec.rb +27 -0
- data/spec/fluq/feed/msgpack_spec.rb +27 -0
- data/spec/fluq/feed/tsv_spec.rb +27 -0
- data/spec/fluq/handler/base_spec.rb +70 -0
- data/spec/fluq/handler/log_spec.rb +68 -0
- data/spec/fluq/handler/null_spec.rb +11 -0
- data/spec/fluq/input/base_spec.rb +29 -0
- data/spec/fluq/input/socket/connection_spec.rb +35 -0
- data/spec/fluq/input/socket_spec.rb +45 -0
- data/spec/fluq/mixins/loggable_spec.rb +10 -0
- data/spec/fluq/mixins/logger_spec.rb +25 -0
- data/spec/fluq/reactor_spec.rb +58 -0
- data/spec/fluq/url_spec.rb +16 -0
- data/spec/fluq_spec.rb +11 -0
- data/spec/scenario/config/nested/common.rb +3 -0
- data/spec/scenario/config/test.rb +3 -0
- data/spec/scenario/lib/fluq/handler/custom/test_handler.rb +4 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/configuration.rb +25 -0
- metadata +242 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
class FluQ::Input::Socket < FluQ::Input::Base
|
2
|
+
|
3
|
+
# @attr_reader [URI] url the URL
|
4
|
+
attr_reader :url
|
5
|
+
|
6
|
+
# Constructor.
|
7
|
+
# @option options [String] :bind the URL to bind to
|
8
|
+
# @raises [ArgumentError] when no bind URL provided
|
9
|
+
# @raises [URI::InvalidURIError] if invalid URL is given
|
10
|
+
# @example Launch a server
|
11
|
+
#
|
12
|
+
# server = FluQ::Server.new(reactor, bind: "tcp://localhost:7654")
|
13
|
+
#
|
14
|
+
def initialize(*)
|
15
|
+
super
|
16
|
+
|
17
|
+
raise ArgumentError, 'No URL to bind to provided, make sure you pass :bind option' unless config[:bind]
|
18
|
+
@url = FluQ::URL.parse(config[:bind], protocols)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String] descriptive name
|
22
|
+
def name
|
23
|
+
@name ||= "#{super} (#{@url})"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Start the server
|
27
|
+
def run
|
28
|
+
args = [self.class::Connection, self]
|
29
|
+
case @url.scheme
|
30
|
+
when 'tcp'
|
31
|
+
EventMachine.start_server @url.host, @url.port, *args
|
32
|
+
when 'udp'
|
33
|
+
EventMachine.open_datagram_socket @url.host, @url.port, *args
|
34
|
+
when 'unix'
|
35
|
+
EventMachine.start_server @url.path, *args
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
# @return [Array] supported protocols
|
42
|
+
def protocols
|
43
|
+
["tcp", "udp", "unix"]
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
%w'connection'.each do |name|
|
49
|
+
require "fluq/input/socket/#{name}"
|
50
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class FluQ::Input::Socket::Connection < EventMachine::Connection
|
2
|
+
include FluQ::Mixins::Loggable
|
3
|
+
|
4
|
+
# Constructor
|
5
|
+
# @param [FluQ::Input::Socket] parent the input
|
6
|
+
def initialize(parent)
|
7
|
+
super()
|
8
|
+
@parent = parent
|
9
|
+
end
|
10
|
+
|
11
|
+
# Callback
|
12
|
+
def post_init
|
13
|
+
self.comm_inactivity_timeout = 60
|
14
|
+
end
|
15
|
+
|
16
|
+
# Callback
|
17
|
+
def receive_data(data)
|
18
|
+
buffer.write(data)
|
19
|
+
flush! if buffer.full?
|
20
|
+
rescue => ex
|
21
|
+
logger.crash "#{self.class.name} failure: #{ex.message} (#{ex.class.name})", ex
|
22
|
+
end
|
23
|
+
|
24
|
+
# Callback
|
25
|
+
def unbind
|
26
|
+
flush!
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def buffer
|
32
|
+
@buffer ||= @parent.new_buffer
|
33
|
+
end
|
34
|
+
|
35
|
+
def flush!
|
36
|
+
current = buffer
|
37
|
+
@buffer = nil
|
38
|
+
@parent.flush!(current)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/lib/fluq/mixins.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module FluQ::Mixins::Logger
|
2
|
+
|
3
|
+
def exception_handlers
|
4
|
+
@exception_handlers ||= []
|
5
|
+
end
|
6
|
+
|
7
|
+
def exception_handler(&block)
|
8
|
+
exception_handlers << block
|
9
|
+
end
|
10
|
+
|
11
|
+
def crash(string, exception)
|
12
|
+
if exception.respond_to?(:backtrace) && exception.backtrace
|
13
|
+
trace = exception.backtrace.map {|line| " #{line}" }.join("\n")
|
14
|
+
end
|
15
|
+
error [string, trace].compact.join("\n")
|
16
|
+
|
17
|
+
exception_handlers.each do |handler|
|
18
|
+
begin
|
19
|
+
handler.call(exception)
|
20
|
+
rescue => ex
|
21
|
+
error "EXCEPTION HANDLER CRASHED: #{ex.message}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/fluq/reactor.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
class FluQ::Reactor
|
2
|
+
include FluQ::Mixins::Loggable
|
3
|
+
|
4
|
+
# attr_reader [Array] handlers
|
5
|
+
attr_reader :handlers
|
6
|
+
|
7
|
+
# attr_reader [Array] inputs
|
8
|
+
attr_reader :inputs
|
9
|
+
|
10
|
+
# Runs the reactor within EventMachine
|
11
|
+
def self.run
|
12
|
+
EM.run do
|
13
|
+
EM.threadpool_size = 100
|
14
|
+
yield new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Constructor
|
19
|
+
def initialize
|
20
|
+
super
|
21
|
+
@handlers = []
|
22
|
+
@inputs = []
|
23
|
+
end
|
24
|
+
|
25
|
+
# Listens to an input
|
26
|
+
# @param [Class<FluQ::Input::Base>] klass input class
|
27
|
+
# @param [multiple] args initialization arguments
|
28
|
+
def listen(klass, *args)
|
29
|
+
input = klass.new(self, *args).tap(&:run)
|
30
|
+
inputs.push(input)
|
31
|
+
logger.info "Listening to #{input.name}"
|
32
|
+
input
|
33
|
+
end
|
34
|
+
|
35
|
+
# Registers a handler
|
36
|
+
# @param [Class<FluQ::Handler::Base>] klass handler class
|
37
|
+
# @param [multiple] args initialization arguments
|
38
|
+
def register(klass, *args)
|
39
|
+
handler = klass.new(self, *args)
|
40
|
+
if handlers.any? {|h| h.name == handler.name }
|
41
|
+
raise ArgumentError, "Handler '#{handler.name}' is already registered. Please provide a unique :name option"
|
42
|
+
end
|
43
|
+
handlers.push(handler)
|
44
|
+
logger.info "Registered #{handler.name}"
|
45
|
+
handler
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [Array<Event>] events to process
|
49
|
+
def process(events)
|
50
|
+
on_events events
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String] introspection
|
55
|
+
def inspect
|
56
|
+
"#<#{self.class.name} inputs: #{inputs.size}, handlers: #{handlers.size}>"
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def on_events(events)
|
62
|
+
handlers.each do |handler|
|
63
|
+
start = Time.now
|
64
|
+
begin
|
65
|
+
matching = handler.select(events)
|
66
|
+
next if matching.empty?
|
67
|
+
|
68
|
+
handler.on_events(matching)
|
69
|
+
logger.info { "#{handler.name} processed #{matching.size}/#{events.size} events in #{((Time.now - start) * 1000).round}ms" }
|
70
|
+
rescue => ex
|
71
|
+
logger.crash "#{handler.class.name} #{handler.name} failed: #{ex.class.name} #{ex.message}", ex
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/lib/fluq/testing.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'fluq'
|
2
|
+
|
3
|
+
module FluQ::Testing
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def wait_until(opts = {}, &block)
|
7
|
+
tick = opts[:tick] || 0.01
|
8
|
+
max = opts[:max] || (tick * 50)
|
9
|
+
Timeout.timeout(max) { sleep(tick) until block.call }
|
10
|
+
rescue Timeout::Error
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class FluQ::Handler::Test < FluQ::Handler::Base
|
15
|
+
attr_reader :events
|
16
|
+
|
17
|
+
def initialize(*)
|
18
|
+
super
|
19
|
+
@events = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_events(events)
|
23
|
+
raise RuntimeError, "Test Failure!" if events.any? {|e| e.tag == "error.event" }
|
24
|
+
@events.concat events
|
25
|
+
end
|
26
|
+
end
|
data/lib/fluq/url.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module FluQ::URL
|
2
|
+
|
3
|
+
# @param [String] url the URL
|
4
|
+
# @params [Array] schemes allowed schemes
|
5
|
+
# @raises URI::InvalidURIError if URL or scheme is invalid
|
6
|
+
def self.parse(url, schemes = ["tcp", "unix"])
|
7
|
+
url = URI.parse(url)
|
8
|
+
case url.scheme
|
9
|
+
when *schemes
|
10
|
+
url
|
11
|
+
else
|
12
|
+
raise URI::InvalidURIError, "Invalid URI scheme, only #{schemes.join(', ')} are allowed"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/lib/fluq/version.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FluQ::Buffer::Base do
|
4
|
+
|
5
|
+
its(:config) { should == {max_size: 268435456} }
|
6
|
+
its(:size) { should be(0) }
|
7
|
+
its(:name) { should == "base" }
|
8
|
+
it { should respond_to(:write) }
|
9
|
+
it { should respond_to(:close) }
|
10
|
+
it { should_not be_full }
|
11
|
+
|
12
|
+
it 'should drain' do
|
13
|
+
subject.drain {|io| io.should be_instance_of(StringIO) }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'when size exeeds limit' do
|
17
|
+
before { subject.stub size: 268435457 }
|
18
|
+
it { should be_full }
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FluQ::Buffer::File do
|
4
|
+
|
5
|
+
let(:event) { FluQ::Event.new("some.tag", 1313131313, {}) }
|
6
|
+
|
7
|
+
it { should be_a(FluQ::Buffer::Base) }
|
8
|
+
its(:config) { should == {max_size: 268435456, path: "tmp/buffers"} }
|
9
|
+
its(:file) { should be_instance_of(File) }
|
10
|
+
its(:size) { should == 0 }
|
11
|
+
|
12
|
+
it "should return a name" do
|
13
|
+
Time.stub(now: Time.at(1313131313.45678))
|
14
|
+
subject.name.should == "file-fb-1313131313457.1"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should generate unique paths" do
|
18
|
+
Time.stub(now: Time.at(1313131313.45678))
|
19
|
+
subject.file.path.should == FluQ.root.join("tmp/buffers/fb-1313131313457.1").to_s
|
20
|
+
described_class.new.file.path.should == FluQ.root.join("tmp/buffers/fb-1313131313457.2").to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should write data" do
|
24
|
+
data = event.to_msgpack
|
25
|
+
100.times { subject.write(data) }
|
26
|
+
subject.size.should == 1900
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should drain contents" do
|
30
|
+
4.times { subject.write(event.to_msgpack) }
|
31
|
+
subject.drain do |io|
|
32
|
+
io.read.should == ([event.to_msgpack] * 4).join
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should prevent writes once buffer is 'drained'" do
|
37
|
+
subject.write(event.to_msgpack)
|
38
|
+
subject.drain {|*| }
|
39
|
+
lambda { subject.write(event.to_msgpack) }.should raise_error(IOError, /closed/)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should close and unlink files" do
|
43
|
+
subject.write(event.to_msgpack)
|
44
|
+
lambda { subject.close }.should change { File.exists?(subject.file.path) }.to(false)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FluQ::DSL::Options do
|
4
|
+
|
5
|
+
it 'should store value options' do
|
6
|
+
subject = described_class.new { val 42 }
|
7
|
+
subject.to_hash.should == { val: 42 }
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should store block options' do
|
11
|
+
subject = described_class.new { val { 42 } }
|
12
|
+
subject.to_hash[:val].().should == 42
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should store boolean options' do
|
16
|
+
subject = described_class.new { val }
|
17
|
+
subject.to_hash.should == { val: true }
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should store values with sub-options' do
|
21
|
+
described_class.new { val(42) { sub 21 } }.to_hash.should == { val: 42, val_options: { sub: 21 } }
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FluQ::DSL do
|
4
|
+
|
5
|
+
def dsl(reactor)
|
6
|
+
described_class.new reactor, FluQ.root.join('../scenario/config/test.rb')
|
7
|
+
end
|
8
|
+
|
9
|
+
subject do
|
10
|
+
dsl(reactor)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should find & configure input' do
|
14
|
+
subject.input(:socket) do
|
15
|
+
bind 'tcp://localhost:76543'
|
16
|
+
end
|
17
|
+
subject.should have(1).inputs
|
18
|
+
reactor.should have(:no).inputs
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should find & configure handler' do
|
22
|
+
subject.handler(:log)
|
23
|
+
subject.should have(1).handlers
|
24
|
+
reactor.should have(:no).handlers
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should find namespaced handler' do
|
28
|
+
subject.handler(:custom, :test_handler) do
|
29
|
+
to 'tcp://localhost:87654'
|
30
|
+
end
|
31
|
+
subject.should have(1).handlers
|
32
|
+
subject.handlers.last.first.should == FluQ::Handler::Custom::TestHandler
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should evaluate configuration' do
|
36
|
+
with_reactor do |reactor|
|
37
|
+
dsl(reactor).run
|
38
|
+
reactor.should have(1).handlers
|
39
|
+
reactor.should have(1).inputs
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FluQ::Event do
|
4
|
+
|
5
|
+
subject { described_class.new :"some.tag", "1313131313", "a" => "v1", "b" => "v2" }
|
6
|
+
|
7
|
+
it { should be_a(Hash) }
|
8
|
+
its(:tag) { should == "some.tag" }
|
9
|
+
its(:timestamp) { should == 1313131313 }
|
10
|
+
its(:time) { should be_instance_of(Time) }
|
11
|
+
its(:time) { should be_utc }
|
12
|
+
its(:to_a) { should == ["some.tag", 1313131313, "a" => "v1", "b" => "v2"] }
|
13
|
+
its(:to_tsv) { should == %(some.tag\t1313131313\t{"a":"v1","b":"v2"}) }
|
14
|
+
its(:to_json) { should == %({"a":"v1","b":"v2","=":"some.tag","@":1313131313}) }
|
15
|
+
its(:to_msgpack) { should == "\x84\xA1a\xA2v1\xA1b\xA2v2\xA1=\xA8some.tag\xA1@\xCEND\xCB1".force_encoding(Encoding::BINARY) }
|
16
|
+
its(:inspect) { should == %(["some.tag", 1313131313, {"a"=>"v1", "b"=>"v2"}]) }
|
17
|
+
|
18
|
+
it "should be comparable" do
|
19
|
+
subject.should == { "a" => "v1", "b" => "v2" }
|
20
|
+
subject.should == ["some.tag", 1313131313, "a" => "v1", "b" => "v2"]
|
21
|
+
[subject].should == [{ "a" => "v1", "b" => "v2" }]
|
22
|
+
[subject, subject].should == [["some.tag", 1313131313, "a" => "v1", "b" => "v2"]] * 2
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FluQ::Feed::Base do
|
4
|
+
|
5
|
+
let(:buffer) { FluQ::Buffer::Base.new }
|
6
|
+
|
7
|
+
subject do
|
8
|
+
described_class.new(buffer)
|
9
|
+
end
|
10
|
+
|
11
|
+
it { should be_a(Enumerable) }
|
12
|
+
its(:buffer) { should be(buffer) }
|
13
|
+
its(:to_a) { should == [] }
|
14
|
+
|
15
|
+
end
|