fluq 0.7.5 → 0.8.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 (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