fluq 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +6 -0
  4. data/Gemfile.lock +39 -0
  5. data/MIT-LICENCE +19 -0
  6. data/README.md +10 -0
  7. data/Rakefile +11 -0
  8. data/benchmark/logging.rb +37 -0
  9. data/benchmark/socket.rb +52 -0
  10. data/bin/fluq-rb +8 -0
  11. data/examples/common.rb +3 -0
  12. data/examples/simple.rb +5 -0
  13. data/fluq.gemspec +33 -0
  14. data/lib/fluq.rb +50 -0
  15. data/lib/fluq/buffer.rb +6 -0
  16. data/lib/fluq/buffer/base.rb +51 -0
  17. data/lib/fluq/buffer/file.rb +68 -0
  18. data/lib/fluq/cli.rb +142 -0
  19. data/lib/fluq/dsl.rb +49 -0
  20. data/lib/fluq/dsl/options.rb +27 -0
  21. data/lib/fluq/error.rb +2 -0
  22. data/lib/fluq/event.rb +55 -0
  23. data/lib/fluq/feed.rb +6 -0
  24. data/lib/fluq/feed/base.rb +18 -0
  25. data/lib/fluq/feed/json.rb +28 -0
  26. data/lib/fluq/feed/msgpack.rb +27 -0
  27. data/lib/fluq/feed/tsv.rb +30 -0
  28. data/lib/fluq/handler.rb +6 -0
  29. data/lib/fluq/handler/base.rb +80 -0
  30. data/lib/fluq/handler/log.rb +67 -0
  31. data/lib/fluq/handler/null.rb +4 -0
  32. data/lib/fluq/input.rb +6 -0
  33. data/lib/fluq/input/base.rb +59 -0
  34. data/lib/fluq/input/socket.rb +50 -0
  35. data/lib/fluq/input/socket/connection.rb +41 -0
  36. data/lib/fluq/mixins.rb +6 -0
  37. data/lib/fluq/mixins/loggable.rb +7 -0
  38. data/lib/fluq/mixins/logger.rb +26 -0
  39. data/lib/fluq/reactor.rb +76 -0
  40. data/lib/fluq/testing.rb +26 -0
  41. data/lib/fluq/url.rb +16 -0
  42. data/lib/fluq/version.rb +3 -0
  43. data/spec/fluq/buffer/base_spec.rb +21 -0
  44. data/spec/fluq/buffer/file_spec.rb +47 -0
  45. data/spec/fluq/dsl/options_spec.rb +24 -0
  46. data/spec/fluq/dsl_spec.rb +43 -0
  47. data/spec/fluq/event_spec.rb +25 -0
  48. data/spec/fluq/feed/base_spec.rb +15 -0
  49. data/spec/fluq/feed/json_spec.rb +27 -0
  50. data/spec/fluq/feed/msgpack_spec.rb +27 -0
  51. data/spec/fluq/feed/tsv_spec.rb +27 -0
  52. data/spec/fluq/handler/base_spec.rb +70 -0
  53. data/spec/fluq/handler/log_spec.rb +68 -0
  54. data/spec/fluq/handler/null_spec.rb +11 -0
  55. data/spec/fluq/input/base_spec.rb +29 -0
  56. data/spec/fluq/input/socket/connection_spec.rb +35 -0
  57. data/spec/fluq/input/socket_spec.rb +45 -0
  58. data/spec/fluq/mixins/loggable_spec.rb +10 -0
  59. data/spec/fluq/mixins/logger_spec.rb +25 -0
  60. data/spec/fluq/reactor_spec.rb +58 -0
  61. data/spec/fluq/url_spec.rb +16 -0
  62. data/spec/fluq_spec.rb +11 -0
  63. data/spec/scenario/config/nested/common.rb +3 -0
  64. data/spec/scenario/config/test.rb +3 -0
  65. data/spec/scenario/lib/fluq/handler/custom/test_handler.rb +4 -0
  66. data/spec/spec_helper.rb +12 -0
  67. data/spec/support/configuration.rb +25 -0
  68. 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
@@ -0,0 +1,6 @@
1
+ module FluQ::Mixins
2
+ end
3
+
4
+ %w'loggable logger'.each do |name|
5
+ require "fluq/mixins/#{name}"
6
+ end
@@ -0,0 +1,7 @@
1
+ module FluQ::Mixins::Loggable
2
+
3
+ def logger
4
+ FluQ.logger
5
+ end
6
+
7
+ end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module FluQ
2
+ VERSION = "0.7.0"
3
+ end
@@ -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