fwd 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Gemfile CHANGED
@@ -1,2 +1,2 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
  gemspec
@@ -7,22 +7,22 @@ PATH
7
7
  servolux
8
8
 
9
9
  GEM
10
- remote: http://rubygems.org/
10
+ remote: https://rubygems.org/
11
11
  specs:
12
12
  connection_pool (1.0.0)
13
- diff-lcs (1.1.3)
13
+ diff-lcs (1.2.1)
14
14
  eventmachine-le (1.1.4)
15
15
  rake (10.0.3)
16
- rspec (2.12.0)
17
- rspec-core (~> 2.12.0)
18
- rspec-expectations (~> 2.12.0)
19
- rspec-mocks (~> 2.12.0)
20
- rspec-core (2.12.2)
21
- rspec-expectations (2.12.1)
22
- diff-lcs (~> 1.1.3)
23
- rspec-mocks (2.12.2)
16
+ rspec (2.13.0)
17
+ rspec-core (~> 2.13.0)
18
+ rspec-expectations (~> 2.13.0)
19
+ rspec-mocks (~> 2.13.0)
20
+ rspec-core (2.13.0)
21
+ rspec-expectations (2.13.0)
22
+ diff-lcs (>= 1.1.3, < 2.0)
23
+ rspec-mocks (2.13.0)
24
24
  servolux (0.10.0)
25
- yard (0.8.4.1)
25
+ yard (0.8.5.2)
26
26
 
27
27
  PLATFORMS
28
28
  ruby
@@ -13,16 +13,14 @@ FileUtils.rm_rf TMP
13
13
  FileUtils.mkdir_p TMP
14
14
  FileUtils.touch(OUT)
15
15
 
16
- FWD = fork { exec "#{root}/bin/fwd-rb --flush 10000:2 -F tcp://0.0.0.0:7291 --path #{TMP} -v" }
17
- NCC = fork { exec "nc -vlp 7291 > #{OUT}" }
18
-
19
- sleep(5)
20
-
16
+ FWD = spawn "#{root}/bin/fwd-rb --flush 10000:2 -F tcp://0.0.0.0:7291 --path #{TMP} -v"
17
+ NCC = spawn "nc -kl 7291", out: [OUT, "w"]
21
18
  EVENTS = 10_000_000
22
- LENGTH = 100
23
- DATA = "A" * LENGTH
19
+ DATA = (("A".."Z").to_a + ("a".."z").to_a).join + "\n"
20
+ LENGTH = DATA.size
24
21
  CCUR = 5
25
22
 
23
+ sleep(5)
26
24
  ds = Benchmark.realtime do
27
25
  (1..CCUR).map do
28
26
  fork do
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.name = File.basename(__FILE__, '.gemspec')
10
10
  s.summary = "fwd >>"
11
11
  s.description = "The minimalistic stream forwarder"
12
- s.version = "0.3.3"
12
+ s.version = "0.4.0"
13
13
 
14
14
  s.authors = ["Black Square Media"]
15
15
  s.email = "info@blacksquaremedia.com"
data/lib/fwd.rb CHANGED
@@ -11,19 +11,6 @@ require 'servolux'
11
11
  class Fwd
12
12
  FLUSH = "\000>>"
13
13
 
14
- class << self
15
-
16
- attr_writer :logger
17
-
18
- # [Logger] logger instance
19
- def logger
20
- @logger ||= ::Logger.new(STDOUT).tap do |l|
21
- l.level = ::Logger::INFO
22
- end
23
- end
24
-
25
- end
26
-
27
14
  # @attr_reader [URI] uri to bind to
28
15
  attr_reader :bind
29
16
 
@@ -36,6 +23,9 @@ class Fwd
36
23
  # @attr_reader [Fwd::Output] output
37
24
  attr_reader :output
38
25
 
26
+ # @attr_reader [Logger] logger
27
+ attr_reader :logger
28
+
39
29
  # @attr_reader [Hash] opts
40
30
  attr_reader :opts
41
31
 
@@ -49,18 +39,19 @@ class Fwd
49
39
  # @option opts [Integer] flush_rate flush after N messages
50
40
  # @option opts [Integer] flush_interval flush after N seconds
51
41
  def initialize(opts = {})
42
+ @opts = opts
52
43
  @bind = URI.parse(opts[:bind] || "tcp://0.0.0.0:7289")
53
44
  @root = Pathname.new(opts[:path] || "tmp")
54
45
  @prefix = opts[:prefix] || "buffer"
55
- @opts = opts
46
+ @logger = ::Logger.new(opts[:log] || STDOUT)
47
+ @logger.level = opts[:log_level] || ::Logger::INFO
56
48
  @output = Fwd::Output.new(self)
57
49
  end
58
50
 
59
51
  # Starts the loop
60
52
  def run!
61
- $0 = "fwd-rb (output)"
62
-
63
53
  @piper = ::Servolux::Piper.new('rw')
54
+
64
55
  at_exit do
65
56
  @piper.signal("TERM")
66
57
  end
@@ -71,16 +62,8 @@ class Fwd
71
62
  end
72
63
 
73
64
  @piper.parent do
74
- loop do
75
- sleep(0.1)
76
- case val = @piper.gets()
77
- when FLUSH
78
- output.forward!
79
- else
80
- logger.error "Received unknown message #{val.class.name} "
81
- exit
82
- end
83
- end
65
+ $0 = "fwd-rb (output)"
66
+ output_loop!
84
67
  end
85
68
  end
86
69
 
@@ -91,6 +74,20 @@ class Fwd
91
74
  EM.start_server @bind.host, @bind.port, Fwd::Input, self, buffer
92
75
  end
93
76
 
77
+ # Starts the output loop
78
+ def output_loop!
79
+ loop do
80
+ sleep(0.1)
81
+ case val = @piper.gets()
82
+ when FLUSH
83
+ @output.forward!
84
+ else
85
+ logger.error "Received unknown message #{val.class.name}: #{val.inspect}"
86
+ exit
87
+ end
88
+ end
89
+ end
90
+
94
91
  # Initiates flush
95
92
  def flush!
96
93
  @piper.child do
@@ -98,11 +95,6 @@ class Fwd
98
95
  end
99
96
  end
100
97
 
101
- # [Logger] logger instance
102
- def logger
103
- self.class.logger
104
- end
105
-
106
98
  end
107
99
 
108
100
  %w|buffer output backend input pool cli|.each do |name|
@@ -5,8 +5,8 @@ class Fwd::Backend
5
5
  @url = URI(url)
6
6
  end
7
7
 
8
- def write(data)
9
- sock.write(data)
8
+ def stream(file)
9
+ IO.copy_stream file.to_s, sock
10
10
  end
11
11
 
12
12
  def close
@@ -13,12 +13,15 @@ class Fwd::Buffer
13
13
  @rate = (core.opts[:flush_rate] || 10_000).to_i
14
14
  @limit = [core.opts[:buffer_limit].to_i, MAX_LIMIT].reject(&:zero?).min
15
15
  @count = 0
16
+ cleanup!
16
17
  reschedule!
18
+
19
+ at_exit { rotate! }
17
20
  end
18
21
 
19
22
  # @param [String] data binary data
20
23
  def concat(data)
21
- rotate! if rotate?
24
+ rotate! if limit_reached?
22
25
  @fd.write(data)
23
26
  @count += 1
24
27
  flush! if flush?
@@ -43,8 +46,8 @@ class Fwd::Buffer
43
46
  return if @fd && @fd.size.zero?
44
47
 
45
48
  if @fd
46
- logger.debug { "Rotating #{File.basename(@fd.path)}, #{@fd.size / 1024} kB" }
47
- FileUtils.mv(@fd.path, @fd.path.sub(/\.open$/, ".closed"))
49
+ close(@fd.path)
50
+ logger.debug { "Rotated #{File.basename(@fd.path)}, #{@fd.size / 1024}k" }
48
51
  end
49
52
 
50
53
  @fd = new_file
@@ -52,8 +55,8 @@ class Fwd::Buffer
52
55
  logger.warn "Rotation delayed: #{e.message}"
53
56
  end
54
57
 
55
- # @return [Boolean] true if rotation is due
56
- def rotate?
58
+ # @return [Boolean] true if limit reached
59
+ def limit_reached?
57
60
  @fd.nil? || @fd.size >= @limit
58
61
  rescue Errno::ENOENT
59
62
  false
@@ -72,6 +75,16 @@ class Fwd::Buffer
72
75
  file
73
76
  end
74
77
 
78
+ def close(file)
79
+ FileUtils.mv(file, file.sub(/\.open$/, ".closed"))
80
+ end
81
+
82
+ def cleanup!
83
+ Dir[root.join("#{prefix}.*.open")].each do |file|
84
+ File.size(file).zero? ? File.unlink(file) : close(file)
85
+ end
86
+ end
87
+
75
88
  def reschedule!
76
89
  return unless @interval > 0
77
90
 
@@ -80,7 +93,7 @@ class Fwd::Buffer
80
93
  end
81
94
 
82
95
  def generate_name
83
- [prefix, Time.now.utc.strftime("%Y%m%d%H%m%s"), SecureRandom.hex(4)].join(".")
96
+ [prefix, (Time.now.utc.to_f * 1000).round, SecureRandom.hex(2)].join(".")
84
97
  end
85
98
 
86
99
  end
@@ -48,8 +48,12 @@ class Fwd::CLI < Hash
48
48
  update prefix: prefix
49
49
  end
50
50
 
51
- o.on("-v", "--verbose", "Enable verbose logging.") do |_|
52
- Fwd.logger.level = Logger::DEBUG
51
+ o.on("-l", "--log PATH", "Custom log path. Default: STDOUT") do |path|
52
+ update log: path
53
+ end
54
+
55
+ o.on("-v", "--verbose", "Enable verbose logging.") do
56
+ update log_level: ::Logger::DEBUG
53
57
  end
54
58
 
55
59
  o.separator ""
@@ -2,7 +2,6 @@ class Fwd::Output
2
2
  extend Forwardable
3
3
  def_delegators :core, :logger, :root, :prefix
4
4
 
5
- CHUNK_SIZE = 16 * 1024
6
5
  RESCUABLE = [
7
6
  Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::EPIPE,
8
7
  Errno::ENETUNREACH, Errno::ENETDOWN, Errno::EINVAL, Errno::ETIMEDOUT,
@@ -26,23 +25,24 @@ class Fwd::Output
26
25
  return if @forwarding
27
26
 
28
27
  @forwarding = true
29
- begin
30
- Dir[root.join("#{prefix}.*.closed")].sort.each do |file|
31
- ok = reserve(file) do |io|
32
- logger.debug { "Flushing #{File.basename(io.path)}, #{io.size.fdiv(1024).round} kB" }
33
- write(io)
34
- end
35
- ok or break
28
+ while (q = closed_files) && (file = q.shift)
29
+ ok = reserve(file) do |reserved|
30
+ start = Time.now
31
+ success = stream_file(reserved)
32
+ real = Time.now - start
33
+ logger.info { "Flushed #{reserved.basename}, #{reserved.size.fdiv(1024).round}k in #{real.round(1)}s (Q: #{q.size})" }
34
+ success
36
35
  end
37
- ensure
38
- @forwarding = false
36
+ ok || break
39
37
  end
38
+ ensure
39
+ @forwarding = false
40
40
  end
41
41
 
42
- # @param [IO] io source stream
43
- def write(io)
42
+ # @param [Pathname] file file to stream
43
+ def stream_file(file)
44
44
  pool.any? do |backend|
45
- forward(backend, io)
45
+ stream_to(backend, file)
46
46
  end
47
47
  end
48
48
 
@@ -54,29 +54,22 @@ class Fwd::Output
54
54
  target = Pathname.new(file.sub(/\.closed$/, ".reserved"))
55
55
  FileUtils.mv file, target.to_s
56
56
 
57
- result = false
58
- target.open("r") do |io|
59
- result = yield(io)
60
- end
61
-
62
- if result
57
+ success = yield(target)
58
+ if success
63
59
  target.unlink
64
60
  else
65
- logger.error "Flushing of #{target} failed."
66
- FileUtils.mv target.to_s, target.to_s.sub(/\.reserved$/, ".closed")
61
+ logger.error "Flushing #{File.basename(file)} failed"
62
+ FileUtils.mv target.to_s, file
67
63
  end
68
64
 
69
- result
65
+ success
70
66
  rescue Errno::ENOENT => e
71
67
  # Ignore if file was alread flushed by another process
72
- logger.warn "Flushing of #{File.basename(file)} postponed: #{e.message}"
68
+ logger.warn "Flushing #{File.basename(file)} postponed: #{e.message}"
73
69
  end
74
70
 
75
- def forward(backend, io)
76
- io.rewind
77
- until io.eof?
78
- backend.write(io.read(CHUNK_SIZE))
79
- end
71
+ def stream_to(backend, file)
72
+ backend.stream(file)
80
73
  true
81
74
  rescue *RESCUABLE => e
82
75
  logger.error "Backend #{backend} failed: #{e.class.name} #{e.message}"
@@ -84,4 +77,8 @@ class Fwd::Output
84
77
  false
85
78
  end
86
79
 
80
+ def closed_files
81
+ Dir[root.join("#{prefix}.*.closed")].sort
82
+ end
83
+
87
84
  end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe Fwd::Buffer do
4
4
 
5
5
  def files(glob = "*")
6
- Dir[root.join(glob)]
6
+ Dir[root.join(glob)].map {|f| File.basename(f) }.sort
7
7
  end
8
8
 
9
9
  let(:buffer) { described_class.new core }
@@ -18,7 +18,22 @@ describe Fwd::Buffer do
18
18
  its(:limit) { should be(2048) }
19
19
  its(:timer) { should be(timer) }
20
20
  its(:fd) { should be_nil }
21
- its(:logger) { should be(Fwd.logger) }
21
+ its(:logger) { should be(core.logger) }
22
+
23
+ it 'should clean up existing files' do
24
+ FileUtils.mkdir_p(root.to_s)
25
+ f1, f2 = "buffer.0.blank.open", "buffer.0.filled.open"
26
+ root.join(f1).open("wb") {}
27
+ root.join(f2).open("wb") {|f| f << "A" }
28
+ lambda { subject }.should change { files }.from([f1, f2]).to(["buffer.0.filled.closed"])
29
+ end
30
+
31
+ it 'should generate unique buffer file names' do
32
+ Time.stub now: Time.at(1313131313.2345678)
33
+ SecureRandom.stub hex: "6a7b8c9d"
34
+ buffer.concat("x")
35
+ buffer.fd.path.should == root.join("buffer.1313131313235.6a7b8c9d.open").to_s
36
+ end
22
37
 
23
38
  describe "concat" do
24
39
  it 'should concat data' do
@@ -51,7 +66,7 @@ describe Fwd::Buffer do
51
66
  it { should change { files.size }.by(1) }
52
67
 
53
68
  it 'should archive previous file' do
54
- previous = buffer.fd.path
69
+ previous = File.basename(buffer.fd.path)
55
70
  subject.call
56
71
  files.should include(previous.sub("open", "closed").to_s)
57
72
  end
@@ -9,6 +9,8 @@ describe Fwd::CLI do
9
9
  "--bind", "tcp://127.0.0.1:7289",
10
10
  "--forward", "tcp://1.2.3.4:1234,tcp://1.2.3.5:1235",
11
11
  "--flush", "1200:90",
12
+ "--log", "/dev/null",
13
+ "--verbose",
12
14
  ]
13
15
  end
14
16
 
@@ -19,6 +21,8 @@ describe Fwd::CLI do
19
21
  its([:forward]) { should == ["tcp://1.2.3.4:1234", "tcp://1.2.3.5:1235"] }
20
22
  its([:flush_rate]) { should == 1200 }
21
23
  its([:flush_interval]) { should == 90 }
24
+ its([:log]) { should == "/dev/null" }
25
+ its([:log_level]) { should == 0 }
22
26
  its(:core) { should be_instance_of(Fwd) }
23
27
 
24
28
  end
@@ -14,6 +14,6 @@ describe Fwd::Input do
14
14
  it { should be_a(EM::Connection) }
15
15
  its(:core) { should be(core) }
16
16
  its(:buffer) { should be(buffer) }
17
- its(:logger) { should be(Fwd.logger) }
17
+ its(:logger) { should be(core.logger) }
18
18
 
19
19
  end
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Fwd::Output do
4
4
 
5
+ before { FileUtils.mkdir_p root.to_s }
5
6
  let(:output) { described_class.new core }
6
7
  subject { output }
7
8
 
@@ -47,35 +48,37 @@ describe Fwd::Output do
47
48
  subject.pool.checkout {|c| c.should be_instance_of(Fwd::Backend) }
48
49
  end
49
50
 
50
- describe "writing" do
51
- def write(data)
52
- subject.write(StringIO.new(data))
51
+ describe "streaming data" do
52
+
53
+ def stream(data)
54
+ tmp = root.join("temp.file")
55
+ tmp.open("wb") {|f| f << data }
56
+ subject.stream_file(tmp)
53
57
  end
54
58
 
55
59
  it 'should forward data to backends' do
56
60
  servers(7291, 7292) do
57
- write("A").should be(true)
58
- write("B").should be(true)
59
- write("C").should be(true)
60
- write("D").should be(true)
61
+ stream("A").should be(true)
62
+ stream("B").should be(true)
63
+ stream("C").should be(true)
64
+ stream("D").should be(true)
61
65
  end.should == { 7291=>"BD", 7292=>"AC" }
62
66
  end
63
67
 
64
68
  it 'should handle partial fallouts' do
65
69
  servers(7291) do
66
- write("A").should be(true)
67
- write("B").should be(true)
68
- write("C").should be(true)
69
- write("D").should be(true)
70
- sleep(1)
70
+ stream("A").should be(true)
71
+ stream("B").should be(true)
72
+ stream("C").should be(true)
73
+ stream("D").should be(true)
71
74
  end.should == { 7291=>"ABCD" }
72
75
  end
73
76
 
74
77
  it 'should handle full fallouts' do
75
- write("A").should be(false)
76
- write("B").should be(false)
77
- write("C").should be(false)
78
- write("D").should be(false)
78
+ stream("A").should be(false)
79
+ stream("B").should be(false)
80
+ stream("C").should be(false)
81
+ stream("D").should be(false)
79
82
  end
80
83
 
81
84
  end
@@ -83,7 +86,7 @@ describe Fwd::Output do
83
86
  describe "forwarding" do
84
87
 
85
88
  def write(file)
86
- file.open("w") {|f| f << "x" }
89
+ file.open("wb") {|f| f << "x" }
87
90
  file
88
91
  end
89
92
 
@@ -91,14 +94,18 @@ describe Fwd::Output do
91
94
  Dir[root.join(glob)].map {|f| File.basename f }.sort
92
95
  end
93
96
 
94
- before { subject.stub! write: true }
95
- before { FileUtils.mkdir_p root.to_s }
97
+ before { subject.stub! stream_file: true }
96
98
  let!(:f1) { write root.join("buffer.1.closed") }
97
99
  let!(:f2) { write root.join("buffer.2.open") }
98
100
  let!(:f3) { write root.join("buffer.3.closed") }
99
101
 
100
- it 'should write the data' do
101
- subject.should_receive(:write).twice
102
+ it 'should send the data' do
103
+ subject.should_receive(:stream_file).twice.and_return(true)
104
+ subject.forward!
105
+ end
106
+
107
+ it 'should stop on first failure' do
108
+ subject.should_receive(:stream_file).once.and_return(false)
102
109
  subject.forward!
103
110
  end
104
111
 
@@ -108,9 +115,9 @@ describe Fwd::Output do
108
115
  }.to(["buffer.2.open"])
109
116
  end
110
117
 
111
- it 'should handle failures files' do
112
- subject.should_receive(:write).and_return(true)
113
- subject.should_receive(:write).and_return(false)
118
+ it 'should handle revert failed files' do
119
+ subject.should_receive(:stream_file).and_return(true)
120
+ subject.should_receive(:stream_file).and_return(false)
114
121
 
115
122
  lambda { subject.forward! }.should change {
116
123
  files
@@ -5,6 +5,7 @@ describe Fwd do
5
5
  subject { core }
6
6
 
7
7
  its(:logger) { should be_instance_of(::Logger) }
8
+ its("logger.level") { should == 0 }
8
9
  its(:root) { should be_instance_of(::Pathname) }
9
10
  its(:root) { should == root }
10
11
  its(:bind) { should be_instance_of(URI::Generic) }
@@ -21,6 +21,8 @@ module Fwd::TestHelper
21
21
  def core
22
22
  @_core ||= Fwd.new \
23
23
  path: root,
24
+ log: "/dev/null",
25
+ log_level: Logger::DEBUG,
24
26
  flush_rate: 20,
25
27
  buffer_limit: 2048,
26
28
  forward: ["tcp://127.0.0.1:7291", "tcp://127.0.0.1:7292"]
@@ -34,9 +36,6 @@ end
34
36
 
35
37
  RSpec.configure do |c|
36
38
  c.include(Fwd::TestHelper)
37
- c.before(:suite) do
38
- Fwd.logger = Logger.new("/dev/null")
39
- end
40
39
  c.before(:each) do
41
40
  FileUtils.rm_rf root.to_s
42
41
  EM.stub add_periodic_timer: timer
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fwd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-21 00:00:00.000000000 Z
12
+ date: 2013-03-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine-le
@@ -131,6 +131,7 @@ extensions: []
131
131
  extra_rdoc_files: []
132
132
  files:
133
133
  - .gitignore
134
+ - .travis.yml
134
135
  - Gemfile
135
136
  - Gemfile.lock
136
137
  - Rakefile