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