fwd 0.3.3 → 0.4.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.
@@ -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