fluq 0.7.5 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +12 -1
  5. data/Gemfile.lock +44 -8
  6. data/README.md +24 -6
  7. data/Rakefile +8 -1
  8. data/benchmark/socket.rb +13 -25
  9. data/examples/config/multi.rb +52 -0
  10. data/examples/config/simple.rb +15 -0
  11. data/fluq.gemspec +3 -3
  12. data/lib/fluq.rb +22 -16
  13. data/lib/fluq/cli.rb +3 -12
  14. data/lib/fluq/dsl.rb +2 -45
  15. data/lib/fluq/dsl/base.rb +11 -0
  16. data/lib/fluq/dsl/feed.rb +24 -0
  17. data/lib/fluq/dsl/root.rb +35 -0
  18. data/lib/fluq/event.rb +9 -28
  19. data/lib/fluq/feed.rb +40 -5
  20. data/lib/fluq/format.rb +6 -0
  21. data/lib/fluq/format/base.rb +42 -0
  22. data/lib/fluq/format/json.rb +17 -0
  23. data/lib/fluq/format/lines.rb +27 -0
  24. data/lib/fluq/format/msgpack.rb +28 -0
  25. data/lib/fluq/format/tsv.rb +19 -0
  26. data/lib/fluq/handler.rb +1 -1
  27. data/lib/fluq/handler/base.rb +11 -38
  28. data/lib/fluq/handler/log.rb +12 -14
  29. data/lib/fluq/handler/noop.rb +2 -0
  30. data/lib/fluq/input/base.rb +33 -29
  31. data/lib/fluq/input/socket.rb +46 -16
  32. data/lib/fluq/mixins.rb +2 -2
  33. data/lib/fluq/runner.rb +41 -0
  34. data/lib/fluq/testing.rb +5 -11
  35. data/lib/fluq/version.rb +1 -1
  36. data/lib/fluq/worker.rb +73 -0
  37. data/spec/fluq/dsl/feed_spec.rb +33 -0
  38. data/spec/fluq/dsl/root_spec.rb +20 -0
  39. data/spec/fluq/event_spec.rb +17 -12
  40. data/spec/fluq/feed_spec.rb +24 -0
  41. data/spec/fluq/format/base_spec.rb +9 -0
  42. data/spec/fluq/format/json_spec.rb +22 -0
  43. data/spec/fluq/format/lines_spec.rb +20 -0
  44. data/spec/fluq/format/msgpack_spec.rb +22 -0
  45. data/spec/fluq/format/tsv_spec.rb +21 -0
  46. data/spec/fluq/handler/base_spec.rb +7 -52
  47. data/spec/fluq/handler/log_spec.rb +11 -14
  48. data/spec/fluq/handler/{null_spec.rb → noop_spec.rb} +1 -3
  49. data/spec/fluq/input/base_spec.rb +48 -15
  50. data/spec/fluq/input/socket_spec.rb +34 -26
  51. data/spec/fluq/mixins/loggable_spec.rb +2 -2
  52. data/spec/fluq/runner_spec.rb +18 -0
  53. data/spec/fluq/worker_spec.rb +87 -0
  54. data/spec/fluq_spec.rb +1 -2
  55. data/spec/scenario/config/nested/feed1.rb +6 -0
  56. data/spec/scenario/config/test.rb +8 -2
  57. data/spec/spec_helper.rb +7 -26
  58. metadata +62 -62
  59. data/benchmark/logging.rb +0 -37
  60. data/examples/common.rb +0 -3
  61. data/examples/simple.rb +0 -5
  62. data/lib/fluq/buffer.rb +0 -6
  63. data/lib/fluq/buffer/base.rb +0 -51
  64. data/lib/fluq/buffer/file.rb +0 -68
  65. data/lib/fluq/feed/base.rb +0 -37
  66. data/lib/fluq/feed/json.rb +0 -28
  67. data/lib/fluq/feed/msgpack.rb +0 -27
  68. data/lib/fluq/feed/tsv.rb +0 -30
  69. data/lib/fluq/handler/null.rb +0 -4
  70. data/lib/fluq/input/socket/connection.rb +0 -41
  71. data/lib/fluq/mixins/logger.rb +0 -26
  72. data/lib/fluq/reactor.rb +0 -79
  73. data/spec/fluq/buffer/base_spec.rb +0 -21
  74. data/spec/fluq/buffer/file_spec.rb +0 -47
  75. data/spec/fluq/dsl_spec.rb +0 -43
  76. data/spec/fluq/feed/base_spec.rb +0 -15
  77. data/spec/fluq/feed/json_spec.rb +0 -27
  78. data/spec/fluq/feed/msgpack_spec.rb +0 -27
  79. data/spec/fluq/feed/tsv_spec.rb +0 -27
  80. data/spec/fluq/input/socket/connection_spec.rb +0 -35
  81. data/spec/fluq/mixins/logger_spec.rb +0 -25
  82. data/spec/fluq/reactor_spec.rb +0 -69
  83. data/spec/scenario/config/nested/common.rb +0 -3
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Feed do
4
+
5
+ subject { described_class.new "my-feed" }
6
+
7
+ its(:name) { should == "my-feed" }
8
+ its(:handlers) { should == [] }
9
+ its(:inputs) { should == [] }
10
+
11
+ it "should listen to inputs" do
12
+ subject.listen(FluQ::Input::Socket, bind: "tcp://127.0.0.1:7654")
13
+ subject.should have(1).inputs
14
+ end
15
+
16
+ it "should register handlers" do
17
+ h1 = subject.register(FluQ::Handler::Test)
18
+ subject.should have(1).handlers
19
+
20
+ h2 = subject.register(FluQ::Handler::Test)
21
+ subject.should have(2).handlers
22
+ end
23
+
24
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Format::Base do
4
+
5
+ it "should parse" do
6
+ subject.parse("ANYTHING").should == []
7
+ end
8
+
9
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Format::Json do
4
+
5
+ let(:data) { %({"a":"b"}\n{"a":"b"}\n{"a":"b"}\n) }
6
+
7
+ it { should be_a(FluQ::Format::Lines) }
8
+
9
+ it 'should parse' do
10
+ events = subject.parse(data)
11
+ events.should have(3).items
12
+ events.first.timestamp.should be_within(5).of(Time.now.to_i)
13
+ events.first.should == FluQ::Event.new({"a" => "b"}, events.first.timestamp)
14
+ end
15
+
16
+ it 'should log invalid inputs' do
17
+ subject.logger.should_receive(:warn).once
18
+ events = subject.parse data + %(NOTJSON\n{"a":"b"}\n\n{"a":"b"})
19
+ events.should have(5).items
20
+ end
21
+
22
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Format::Lines do
4
+
5
+ subject { FluQ::Format::Json.new }
6
+
7
+ it { should be_a(described_class) }
8
+ it { should be_a(FluQ::Format::Base) }
9
+
10
+ it 'should parse' do
11
+ subject.parse(%({"a":1})).should have(1).item
12
+ subject.parse(%({"a":1}\n{"b":2}\n\n{"c":3}\n)).should have(3).items
13
+ end
14
+
15
+ it 'should deal with partials' do
16
+ subject.parse(%({"a":1}\n{"b")).should == [{"a"=>1}]
17
+ subject.parse(%(:2}\n)).should == [{"b"=>2}]
18
+ end
19
+
20
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Format::Msgpack do
4
+
5
+ let(:data) { ([{"a" => "b"}] * 3).map {|h| MessagePack.pack(h) }.join }
6
+
7
+ it { should be_a(FluQ::Format::Base) }
8
+
9
+ it 'should parse' do
10
+ events = subject.parse(data)
11
+ events.should have(3).items
12
+ events.first.timestamp.should be_within(5).of(Time.now.to_i)
13
+ events.first.should == FluQ::Event.new({"a" => "b"}, events.first.timestamp)
14
+ end
15
+
16
+ it 'should log invalid inputs' do
17
+ subject.logger.should_receive(:warn).at_least(:once)
18
+ events = subject.parse data + "NOTMP" + data
19
+ events.should have(6).items
20
+ end
21
+
22
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Format::Tsv do
4
+
5
+ let(:data) { %(1313131313\t{"a":"b"}\n1313131313\t{"a":"b"}\n1313131313\t{"a":"b"}\n) }
6
+
7
+ it { should be_a(FluQ::Format::Lines) }
8
+
9
+ it 'should parse' do
10
+ events = subject.parse(data)
11
+ events.should have(3).items
12
+ events.first.should == FluQ::Event.new({"a" => "b"}, 1313131313)
13
+ end
14
+
15
+ it 'should log invalid inputs' do
16
+ subject.logger.should_receive(:warn).once
17
+ events = subject.parse data + %(NOTTSV\n1313131313\t{"a":"b"}\n\n)
18
+ events.should have(4).items
19
+ end
20
+
21
+ end
@@ -2,69 +2,24 @@ require 'spec_helper'
2
2
 
3
3
  describe FluQ::Handler::Base do
4
4
 
5
- subject { described_class.new reactor }
5
+ let(:event) { FluQ::Event.new({}) }
6
6
 
7
7
  it { should respond_to(:on_events) }
8
8
  it { should be_a(FluQ::Mixins::Loggable) }
9
- its(:reactor) { should be(reactor) }
10
- its(:config) { should == { pattern: /./, timeout: 60 } }
11
- its(:pattern) { should == /./ }
12
- its(:name) { should == "base-AxPGxv" }
13
-
14
- def events(*tags)
15
- tags.map {|tag| event(tag) }
16
- end
17
-
18
- def event(tag)
19
- FluQ::Event.new(tag, 1313131313, {})
20
- end
9
+ its(:config) { should == { timeout: 60 } }
10
+ its(:name) { should == "base" }
11
+ its(:timers) { should be_instance_of(Timers) }
21
12
 
22
13
  it 'should have a type' do
23
14
  described_class.type.should == "base"
24
15
  end
25
16
 
26
17
  it 'can have custom names' do
27
- described_class.new(reactor, name: "visitors").name.should == "visitors"
28
- end
29
-
30
- it 'should match tags via patters' do
31
- subject = described_class.new(reactor, pattern: "visits.????.*")
32
- subject.match?(event("visits.site.1")).should be(true)
33
- subject.match?(event("visits.page.2")).should be(true)
34
- subject.match?(event("visits.other.1")).should be(false)
35
- subject.match?(event("visits.site")).should be(false)
36
- subject.match?(event("visits.site.")).should be(true)
37
- subject.match?(event("prefix.visits.site.1")).should be(false)
38
- subject.match?(event("visits.site.1.suffix")).should be(true)
39
- end
40
-
41
- it 'should support "or" patterns' do
42
- subject = described_class.new(reactor, pattern: "visits.{site,page}.*")
43
- subject.match?(event("visits.site.1")).should be(true)
44
- subject.match?(event("visits.page.2")).should be(true)
45
- subject.match?(event("visits.other.1")).should be(false)
46
- subject.match?(event("visits.site")).should be(false)
47
- subject.match?(event("visits.site.")).should be(true)
48
- subject.match?(event("prefix.visits.site.1")).should be(false)
49
- subject.match?(event("visits.site.1.suffix")).should be(true)
50
- end
51
-
52
- it 'should support regular expression patterns' do
53
- subject = described_class.new(reactor, pattern: /^visits\.(?:s|p)\w{3}\..*/)
54
- subject.match?(event("visits.site.1")).should be(true)
55
- subject.match?(event("visits.page.2")).should be(true)
56
- subject.match?(event("visits.other.1")).should be(false)
57
- subject.match?(event("visits.site")).should be(false)
58
- subject.match?(event("visits.site.")).should be(true)
59
- subject.match?(event("prefix.visits.site.1")).should be(false)
60
- subject.match?(event("visits.site.1.suffix")).should be(true)
18
+ described_class.new(name: "visitors").name.should == "visitors"
61
19
  end
62
20
 
63
- it 'should select events' do
64
- stream = events("visits.site.1", "visits.page.2", "visits.other.1", "visits.site.2")
65
- described_class.new(reactor, pattern: "visits.????.*").select(stream).map(&:tag).should == [
66
- "visits.site.1", "visits.page.2", "visits.site.2"
67
- ]
21
+ it 'should not filter events by default' do
22
+ subject.filter([event, event]).should have(2).items
68
23
  end
69
24
 
70
25
  end
@@ -2,33 +2,30 @@ require 'spec_helper'
2
2
 
3
3
  describe FluQ::Handler::Log do
4
4
 
5
- let(:event) do
6
- FluQ::Event.new("my.special.tag", 1313131313, { "a" => "1" })
7
- end
8
- let(:root) { FluQ.root.join("../scenario/log/raw") }
9
- subject { described_class.new reactor }
10
- before { FileUtils.rm_rf(root); FileUtils.mkdir_p(root) }
5
+ let(:event) { FluQ::Event.new({"a" => "1"}, 1313131313) }
6
+ let(:root) { FluQ.root.join("../scenario/log/raw") }
7
+ before { FileUtils.rm_rf(root); FileUtils.mkdir_p(root) }
11
8
 
12
9
  it { should be_a(FluQ::Handler::Base) }
13
- its("config.keys") { should =~ [:convert, :path, :pattern, :rewrite, :cache_max, :cache_ttl, :timeout] }
10
+ its("config.keys") { should =~ [:convert, :path, :cache_max, :cache_ttl, :timeout] }
14
11
 
15
12
  it "can log events" do
16
13
  subject.on_events [event]
17
14
  subject.pool.each_key {|k| subject.pool[k].flush }
18
- root.join("my/special/tag/20110812/06.log").read.should == %(my.special.tag\t1313131313\t{"a":"1"}\n)
15
+ root.join("20110812.log").read.should == %(1313131313\t{"a":"1"}\n)
19
16
  end
20
17
 
21
18
  it 'can have custom conversions' do
22
- subject = described_class.new reactor, convert: lambda {|e| e.merge(ts: e.timestamp).map {|k,v| "#{k}=#{v}" }.join(',') }
19
+ subject = described_class.new convert: ->e { e.merge(ts: e.timestamp).map {|k,v| "#{k}=#{v}" }.join(',') }
23
20
  subject.on_events [event]
24
21
  subject.pool.each_key {|k| subject.pool[k].flush }
25
- root.join("my/special/tag/20110812/06.log").read.should == "a=1,ts=1313131313\n"
22
+ root.join("20110812.log").read.should == "a=1,ts=1313131313\n"
26
23
  end
27
24
 
28
- it 'can rewrite tags' do
29
- subject = described_class.new reactor, rewrite: lambda {|t| t.split('.').reverse.first(2).join(".") }
25
+ it 'can rewrite events' do
26
+ subject = described_class.new rewrite: ->e { e["a"].to_i * 1000 }, path: "log/raw/%Y%m/%t.log"
30
27
  subject.on_events [event]
31
- root.join("tag.special/20110812/06.log").should be_file
28
+ root.join("201108/1000.log").should be_file
32
29
  end
33
30
 
34
31
  it 'should not fail on temporary file errors' do
@@ -36,7 +33,7 @@ describe FluQ::Handler::Log do
36
33
  subject.pool.each_key {|k| subject.pool[k].close }
37
34
  subject.on_events [event]
38
35
  subject.pool.each_key {|k| subject.pool[k].flush }
39
- root.join("my/special/tag/20110812/06.log").read.should have(2).lines
36
+ root.join("20110812.log").read.should have(2).lines
40
37
  end
41
38
 
42
39
  describe described_class::FilePool do
@@ -1,8 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe FluQ::Handler::Null do
4
-
5
- subject { described_class.new reactor }
3
+ describe FluQ::Handler::Noop do
6
4
 
7
5
  it 'should handle events' do
8
6
  subject.on_events []
@@ -2,28 +2,61 @@ require 'spec_helper'
2
2
 
3
3
  describe FluQ::Input::Base do
4
4
 
5
- let(:event) { FluQ::Event.new("some.tag", 1313131313, {}) }
6
- let!(:handler) { reactor.register FluQ::Handler::Test }
7
- subject { described_class.new(reactor, feed: "json") }
5
+ subject { described_class.new "my-feed", [FluQ::Handler::Test], format: "msgpack" }
6
+ let(:subject2) { described_class.new "my-feed", [FluQ::Handler::Test], format: "json" }
7
+ let(:handler) { subject.worker.handlers.first }
8
+ let(:handler2) { subject2.worker.handlers.first }
9
+ let(:data) { [{"a" => 1}, {"b" => 2}].map {|h| MessagePack.pack(h) }.join }
8
10
 
9
11
  it { should be_a(FluQ::Mixins::Loggable) }
10
- its(:reactor) { should be(reactor) }
11
- its(:config) { should == {feed: "json", buffer: "file", buffer_options: {}} }
12
+ its(:wrapped_object) { should be_instance_of(described_class) }
13
+
14
+ its(:worker) { should be_instance_of(FluQ::Worker) }
15
+ its(:config) { should == {format: "msgpack", format_options: {}} }
12
16
  its(:name) { should == "base" }
13
- its(:feed_klass) { should == FluQ::Feed::Json }
14
- its(:buffer_klass) { should == FluQ::Buffer::File }
17
+ its(:description) { should == "base" }
18
+ its(:format) { should be_instance_of(FluQ::Format::Msgpack) }
19
+
20
+ it 'should process' do
21
+ subject.process(data)
22
+ handler.should have(2).events
23
+ end
15
24
 
16
- it 'should create new buffers' do
17
- (b1 = subject.new_buffer).should be_instance_of(FluQ::Buffer::File)
18
- (b2 = subject.new_buffer).should be_instance_of(FluQ::Buffer::File)
19
- b1.should_not be(b2)
25
+ it 'should maintain separate handler instances per input' do
26
+ -> {
27
+ subject.process data
28
+ }.should change { handler.events.size }.by(2)
29
+
30
+ -> {
31
+ subject2.process %({"a":1,"b":2}\n{"a":1,"b":2}\n{"a":1,"b":2}\n)
32
+ }.should_not change { handler.events.size }
33
+ handler2.should have(3).events
20
34
  end
21
35
 
22
- it 'should flush buffers' do
23
- buf = subject.new_buffer
24
- buf.write [event, event].map(&:to_json).join("\n")
25
- subject.flush!(buf)
36
+ it 'should handle partial messages' do
37
+ m1, m2 = data + data[0..1], data[2..-1]
38
+ subject.process(m1)
26
39
  handler.should have(2).events
40
+ subject.process(m2)
41
+ handler.should have(4).events
42
+
43
+ m1, m2 = data[0..-3], data[-3..-1] + data
44
+ subject.process(m1)
45
+ handler.should have(5).events
46
+ subject.process(m2)
47
+ handler.should have(8).events
48
+
49
+ m1, m2 = %({"a":1,"b":2}\n{"a":1,"b":2}\n{"a":1), %(,"b":2}\n{"a":1,"b":2}\n)
50
+ subject2.process(m1)
51
+ handler2.should have(2).events
52
+ subject2.process(m2)
53
+ handler2.should have(4).events
54
+
55
+ m1, m2 = %({"a":1,"b":2}\n{"a":1,), %("b":2}\n{"a":1,"b":2}\n{"a":1,"b":2}\n)
56
+ subject2.process(m1)
57
+ handler2.should have(5).events
58
+ subject2.process(m2)
59
+ handler2.should have(8).events
27
60
  end
28
61
 
29
62
  end
@@ -2,44 +2,52 @@ require 'spec_helper'
2
2
 
3
3
  describe FluQ::Input::Socket do
4
4
 
5
- let(:event) { FluQ::Event.new("some.tag", 1313131313, {}) }
5
+ let(:event) { {a: 1, b: 2} }
6
+ let(:actors) { [] }
6
7
 
7
- def input(reactor)
8
- described_class.new(reactor, bind: "tcp://127.0.0.1:26712")
8
+ def input(opts = {})
9
+ actor = described_class.new "my-feed", [[FluQ::Handler::Test]], opts
10
+ actors << actor
11
+ actor
9
12
  end
10
13
 
11
- subject { input(reactor) }
14
+ def wait_for(server)
15
+ 30.times do
16
+ break if server.listening?
17
+ sleep(0.01)
18
+ end
19
+ end
20
+
21
+ subject { input bind: "tcp://127.0.0.1:26712", format: "msgpack" }
22
+ after { actors.each &:terminate }
23
+
12
24
  it { should be_a(FluQ::Input::Base) }
13
- its(:name) { should == "socket (tcp://127.0.0.1:26712)" }
14
- its(:config) { should == {feed: "msgpack", buffer: "file", buffer_options: {}, bind: "tcp://127.0.0.1:26712"} }
25
+ its(:description) { should == "socket (tcp://127.0.0.1:26712)" }
26
+ its(:name) { should == "tcp" }
27
+ its(:config) { should == {format: "msgpack", format_options: {}, bind: "tcp://127.0.0.1:26712"} }
15
28
 
16
29
  it 'should require bind option' do
17
- lambda { described_class.new(reactor) }.should raise_error(ArgumentError, /No URL to bind/)
30
+ -> { input }.should raise_error(ArgumentError, /No URL to bind/)
18
31
  end
19
32
 
20
33
  it 'should handle requests' do
21
- with_reactor do |reactor|
22
- server = input(reactor)
23
- lambda { TCPSocket.open("127.0.0.1", 26712) }.should raise_error(Errno::ECONNREFUSED)
24
-
25
- server.run
26
- client = TCPSocket.open("127.0.0.1", 26712)
27
-
28
- client.write event.to_msgpack
29
- client.close
30
- end
34
+ wait_for(subject)
35
+ client = TCPSocket.open("127.0.0.1", 26712)
36
+ client.write MessagePack.pack(event)
37
+ client.close
38
+ subject.worker.should have(1).handlers
39
+ subject.worker.handlers.first.should have(1).events
31
40
  end
32
41
 
33
42
  it 'should support UDP' do
34
- h = nil
35
- with_reactor do |reactor|
36
- h = reactor.register FluQ::Handler::Test
37
- reactor.listen described_class, bind: "udp://127.0.0.1:26713"
38
- client = UDPSocket.new
39
- client.send event.to_msgpack, 0, "127.0.0.1", 26713
40
- client.close
41
- end
42
- h.should have(1).events
43
+ udp = input bind: "udp://127.0.0.1:26713", format: "msgpack"
44
+ wait_for(udp)
45
+
46
+ client = UDPSocket.new
47
+ client.send MessagePack.pack(event), 0, "127.0.0.1", 26713
48
+ client.close
49
+ udp.worker.should have(1).handlers
50
+ udp.worker.handlers.first.should have(1).events
43
51
  end
44
52
 
45
53
  end
@@ -2,9 +2,9 @@ require 'spec_helper'
2
2
 
3
3
  describe FluQ::Mixins::Loggable do
4
4
 
5
- subject { FluQ::Handler::Base.new reactor }
5
+ subject { FluQ::Handler::Base.new }
6
6
 
7
7
  it { should be_a(described_class) }
8
8
  its(:logger) { should be(FluQ.logger) }
9
9
 
10
- end
10
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Runner do
4
+
5
+ after { subject.terminate }
6
+
7
+ its(:feeds) { should == [] }
8
+ its(:inspect) { should == "#<FluQ::Runner feeds: []>" }
9
+
10
+ it "should register feeds" do
11
+ subject.feed("my-feed")
12
+ subject.should have(1).feeds
13
+
14
+ subject.feed("other-feed")
15
+ subject.should have(2).feeds
16
+ end
17
+
18
+ end