fluq 0.7.0
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.
- 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
|