fwd 0.3.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.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ tmp/
2
+ log/
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fwd (0.3.0)
5
+ connection_pool
6
+ eventmachine-le
7
+ servolux
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ connection_pool (1.0.0)
13
+ diff-lcs (1.1.3)
14
+ eventmachine-le (1.1.4)
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)
24
+ servolux (0.10.0)
25
+ yard (0.8.4.1)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ bundler
32
+ fwd!
33
+ rake
34
+ rspec
35
+ yard
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+
3
+ require 'rspec/mocks/version'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ require 'yard'
8
+ YARD::Rake::YardocTask.new
9
+
10
+ desc 'Default: run specs.'
11
+ task :default => :spec
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.expand_path('../../lib', __FILE__))
4
+
5
+ require 'bundler/setup'
6
+ require 'benchmark'
7
+ require 'tempfile'
8
+ require 'fwd'
9
+
10
+ root = Pathname.new(File.expand_path('../..', __FILE__))
11
+ FileUtils.rm_rf root.join("tmp/benchmark")
12
+ FileUtils.mkdir_p root.join("tmp/benchmark")
13
+
14
+ EVENTS = 10_000_000
15
+ DATA = "A" * 64
16
+ OUTF = root.join('tmp/benchmark/out.txt')
17
+
18
+ COLL = fork do
19
+ `nc -vlp 7291 > #{OUTF}`
20
+ sleep
21
+ end
22
+ EMIT = fork do
23
+ sock = TCPSocket.new "127.0.0.1", 7289
24
+ EVENTS.times { sock.write DATA }
25
+ sock.close
26
+ end
27
+
28
+ at_exit do
29
+ Process.kill(:TERM, COLL)
30
+ Process.kill(:TERM, EMIT)
31
+ end
32
+
33
+ until OUTF.exist?
34
+ sleep(1)
35
+ end
36
+
37
+ while OUTF.size < EVENTS * DATA.size
38
+ sleep(1)
39
+ puts "Written #{(OUTF.size / 1024.0 / 1024.0).round(1)}M"
40
+ end
data/bin/fwd-rb ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require "fwd/cli"
7
+ Fwd::CLI.run!
data/fwd.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.required_ruby_version = '>= 1.9.1'
7
+ s.required_rubygems_version = ">= 1.8.0"
8
+
9
+ s.name = File.basename(__FILE__, '.gemspec')
10
+ s.summary = "fwd >>"
11
+ s.description = "The minimalistic stream forwarder"
12
+ s.version = "0.3.0"
13
+
14
+ s.authors = ["Black Square Media"]
15
+ s.email = "info@blacksquaremedia.com"
16
+ s.homepage = "https://github.com/bsm/fwd"
17
+
18
+ s.require_path = 'lib'
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+
23
+ s.add_dependency "eventmachine-le"
24
+ s.add_dependency "servolux"
25
+ s.add_dependency "connection_pool"
26
+
27
+ s.add_development_dependency "rake"
28
+ s.add_development_dependency "bundler"
29
+ s.add_development_dependency "rspec"
30
+ s.add_development_dependency "yard"
31
+ end
@@ -0,0 +1,27 @@
1
+ class Fwd::Backend
2
+ attr_reader :url
3
+
4
+ def initialize(url)
5
+ @url = URI(url)
6
+ end
7
+
8
+ def write(data)
9
+ sock.write(data)
10
+ end
11
+
12
+ def close
13
+ @sock.close if @sock
14
+ @sock = nil
15
+ end
16
+
17
+ def to_s
18
+ url.to_s
19
+ end
20
+
21
+ protected
22
+
23
+ def sock
24
+ @sock ||= TCPSocket.new @url.host, @url.port
25
+ end
26
+
27
+ end
data/lib/fwd/buffer.rb ADDED
@@ -0,0 +1,79 @@
1
+ class Fwd::Buffer
2
+ extend Forwardable
3
+ def_delegators :core, :root, :prefix, :logger
4
+
5
+ MAX_SIZE = 64 * 1024 * 1024 # 64M
6
+ attr_reader :core, :interval, :rate, :count, :limit, :timer, :fd, :path
7
+
8
+ # Constructor
9
+ # @param [Fwd] core
10
+ def initialize(core)
11
+ @core = core
12
+ @interval = (core.opts[:flush_interval] || 60).to_i
13
+ @rate = (core.opts[:flush_rate] || 10_000).to_i
14
+ @limit = (core.opts[:flush_limit] || 0).to_i
15
+ @count = 0
16
+
17
+ reschedule!
18
+ rotate!
19
+ end
20
+
21
+ # @param [String] data binary data
22
+ def concat(data)
23
+ rotate! if rotate?
24
+ @fd.write(data)
25
+ @count += 1
26
+ flush! if flush?
27
+ end
28
+
29
+ # (Force) flush buffer
30
+ def flush!
31
+ @count = 0
32
+ rotate!
33
+ core.flush!
34
+ ensure
35
+ reschedule!
36
+ end
37
+
38
+ # @return [Boolean] true if flush is due
39
+ def flush?
40
+ (@rate > 0 && @count >= @rate) || (@limit > 0 && @path.size >= @limit)
41
+ end
42
+
43
+ # (Force) rotate buffer file
44
+ def rotate!
45
+ FileUtils.mv(@path.to_s, @path.to_s.sub(/\.open$/, ".closed")) if @path
46
+ @fd, @path = new_file
47
+ rescue Errno::ENOENT
48
+ end
49
+
50
+ # @return [Boolean] true if rotation is due
51
+ def rotate?
52
+ !@fd || @path.size > MAX_SIZE
53
+ end
54
+
55
+ private
56
+
57
+ def new_file
58
+ path = nil
59
+ until path && !path.exist?
60
+ path = root.join("#{generate_name}.open")
61
+ end
62
+ FileUtils.mkdir_p root.to_s
63
+ file = File.open(path, "wb")
64
+ file.sync = true
65
+ [file, path]
66
+ end
67
+
68
+ def reschedule!
69
+ return unless @interval > 0
70
+
71
+ @timer.cancel if @timer
72
+ @timer = EM.add_periodic_timer(@interval) { flush! }
73
+ end
74
+
75
+ def generate_name
76
+ [prefix, Time.now.utc.strftime("%Y%m%d%H%m%s"), SecureRandom.hex(4)].join(".")
77
+ end
78
+
79
+ end
data/lib/fwd/cli.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'optparse'
2
+ require 'fwd'
3
+
4
+ class Fwd::CLI < Hash
5
+
6
+ def self.run!(argv = ARGV)
7
+ new(argv).run!
8
+ end
9
+
10
+ attr_reader :core
11
+
12
+ def initialize(argv)
13
+ super()
14
+ parser.parse!(argv)
15
+ @core = Fwd.new(self)
16
+ end
17
+
18
+ def run!
19
+ @core.run!
20
+ end
21
+
22
+ def parser
23
+ @parser ||= OptionParser.new do |o|
24
+ o.banner = "Usage: fwd-rb [options]"
25
+ o.separator ""
26
+
27
+ o.on("-B", "--bind URI", "Listen on this address. Default: tcp://0.0.0.0:7289") do |uri|
28
+ update bind: URI.parse(uri).to_s
29
+ end
30
+
31
+ o.on("-F", "--forward U1,[..,Un]", Array, "Forward to these URIs") do |uris|
32
+ update forward: uris.map {|uri| URI.parse(uri).to_s }
33
+ end
34
+
35
+ o.on("-f", "--flush L:M:N",
36
+ "Flush after an interval of N seconds, " <<
37
+ "or after receiving M messages, " <<
38
+ "or after the limit of L bytes was reached. " <<
39
+ "Default: 0:10000:60") do |values|
40
+ l,m,n = values.split(":").map(&:to_i)
41
+ update flush_limit: l.to_i, flush_rate: m.to_i, flush_interval: n.to_i
42
+ end
43
+
44
+ o.on("--path PATH", "Root path for storage. Default: ./tmp") do |path|
45
+ update path: path
46
+ end
47
+
48
+ o.on("--prefix STRING", "Custom prefix for buffer files. Default: buffer") do |prefix|
49
+ update prefix: prefix
50
+ end
51
+
52
+ o.separator ""
53
+ o.on_tail("-h", "--help", "Show this message") do
54
+ puts o
55
+ exit
56
+ end
57
+ end
58
+ end
59
+
60
+ end
data/lib/fwd/input.rb ADDED
@@ -0,0 +1,22 @@
1
+ class Fwd::Input < EM::Connection
2
+ extend Forwardable
3
+ def_delegators :core, :logger
4
+
5
+ attr_reader :core, :buffer
6
+
7
+ # @param [Fwd] core
8
+ # @param [Hash] opts additional opts
9
+ def initialize(core)
10
+ @core = core
11
+ end
12
+
13
+ def post_init
14
+ @buffer = Fwd::Buffer.new(core)
15
+ end
16
+
17
+ # When receiving data, concat it to the buffer
18
+ def receive_data(data)
19
+ buffer.concat(data)
20
+ end
21
+
22
+ end
data/lib/fwd/output.rb ADDED
@@ -0,0 +1,66 @@
1
+ class Fwd::Output
2
+ extend Forwardable
3
+ def_delegators :core, :logger, :root, :prefix
4
+
5
+ RESCUABLE = [
6
+ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::EPIPE,
7
+ Errno::ENETUNREACH, Errno::ENETDOWN, Errno::EINVAL, Errno::ETIMEDOUT,
8
+ IOError, EOFError
9
+ ].freeze
10
+
11
+ attr_reader :pool, :core
12
+
13
+ # Constructor
14
+ # @param [Fwd] core
15
+ def initialize(core)
16
+ backends = Array(core.opts[:forward]).compact.map do |s|
17
+ Fwd::Backend.new(s)
18
+ end
19
+ @core = core
20
+ @pool = Fwd::Pool.new(backends)
21
+ end
22
+
23
+ # Callback
24
+ def forward!
25
+ Dir[root.join("#{prefix}.*.closed")].each do |file|
26
+ reserve(file) do |data|
27
+ logger.debug { "Flushing #{File.basename(file)}, #{data.size.fdiv(1024).round} kB" }
28
+ write(data)
29
+ end
30
+ end
31
+ end
32
+
33
+ # @param [String] binary data
34
+ def write(data)
35
+ pool.any? do |backend|
36
+ forward(backend, data)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def reserve(file)
43
+ return if File.size(file) < 1
44
+
45
+ target = Pathname.new(file.sub(/\.closed$/, ".reserved"))
46
+ FileUtils.mv file, target.to_s
47
+
48
+ if yield(target.read)
49
+ target.unlink
50
+ else
51
+ logger.error "Flushing of #{target} failed."
52
+ FileUtils.mv target.to_s, target.to_s.sub(/\.reserved$/, ".closed")
53
+ end
54
+ rescue Errno::ENOENT
55
+ # Ignore if file was alread flushed by another process
56
+ end
57
+
58
+ def forward(backend, data)
59
+ backend.write(data) && true
60
+ rescue *RESCUABLE => e
61
+ logger.error "Backend #{backend} failed: #{e.class.name} #{e.message}"
62
+ backend.close
63
+ false
64
+ end
65
+
66
+ end
data/lib/fwd/pool.rb ADDED
@@ -0,0 +1,35 @@
1
+ # "Clone" of normal ConnectionPool but tweaked for round-robin
2
+ class Fwd::Pool
3
+ include Enumerable
4
+
5
+ class IdleStack < ConnectionPool::TimedStack
6
+ def unshift(obj)
7
+ @mutex.synchronize do
8
+ @que.unshift obj
9
+ @resource.broadcast
10
+ end
11
+ end
12
+ end
13
+
14
+ def initialize(items)
15
+ @idle = IdleStack.new(0) {}
16
+ @size = items.size
17
+ @key = :"io-proxy-pool-#{@idle.object_id}"
18
+
19
+ items.each do |item|
20
+ @idle.push(item)
21
+ end
22
+ end
23
+
24
+ def each(&block)
25
+ @size.times { checkout(&block) }
26
+ end
27
+
28
+ def checkout
29
+ conn = @idle.pop(30)
30
+ yield conn
31
+ ensure
32
+ @idle.unshift(conn) if conn
33
+ end
34
+
35
+ end
data/lib/fwd/worker.rb ADDED
@@ -0,0 +1,43 @@
1
+ class Fwd::Worker
2
+
3
+ class << self
4
+ private :new
5
+
6
+ def fork(opts)
7
+ GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
8
+
9
+ child_read, parent_write = IO.pipe
10
+ parent_read, child_write = IO.pipe
11
+
12
+ pid = Process.fork do
13
+ begin
14
+ parent_write.close
15
+ parent_read.close
16
+ output = Fwd::Output.new(opts)
17
+ process(output, child_read, child_write)
18
+ ensure
19
+ child_read.close
20
+ child_write.close
21
+ end
22
+ end
23
+
24
+ child_read.close
25
+ child_write.close
26
+
27
+ new(pid, parent_read, parent_write)
28
+ end
29
+
30
+ def process(output, read, write)
31
+ while !read.eof?
32
+ path = Marshal.load(read)
33
+ begin
34
+ result = call_with_index(items, index, options, &block)
35
+ result = nil if options[:preserve_results] == false
36
+ rescue Exception => e
37
+ result = ExceptionWrapper.new(e)
38
+ end
39
+ Marshal.dump(result, write)
40
+ end
41
+ end
42
+
43
+ end
data/lib/fwd.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'eventmachine-le'
2
+ require 'forwardable'
3
+ require 'uri'
4
+ require 'logger'
5
+ require 'fileutils'
6
+ require 'pathname'
7
+ require 'securerandom'
8
+ require 'connection_pool'
9
+ require 'servolux'
10
+
11
+ class Fwd
12
+ FLUSH = "\000>>"
13
+
14
+ class << self
15
+
16
+ attr_writer :logger
17
+
18
+ # [Logger] logger instance
19
+ def logger
20
+ @logger ||= ::Logger.new(STDOUT)
21
+ end
22
+
23
+ end
24
+
25
+ # @attr_reader [URI] uri to bind to
26
+ attr_reader :bind
27
+
28
+ # @attr_reader [Pathname] root path
29
+ attr_reader :root
30
+
31
+ # @attr_reader [String] custom buffer file prefix
32
+ attr_reader :prefix
33
+
34
+ # @attr_reader [Fwd::Output] output
35
+ attr_reader :output
36
+
37
+ # @attr_reader [Hash] opts
38
+ attr_reader :opts
39
+
40
+ # Constructor
41
+ # @param [Hash] opts
42
+ # @option opts [String] path path where buffer files are stored
43
+ # @option opts [String] prefix buffer file prefix
44
+ # @option opts [URI] bind the endpoint to listen to
45
+ # @option opts [Array<URI>] forward the endpoints to forward to
46
+ # @option opts [Integer] flush_limit flush after L messages
47
+ # @option opts [Integer] flush_rate flush after M messages
48
+ # @option opts [Integer] flush_interval flush after N seconds
49
+ def initialize(opts = {})
50
+ @bind = URI.parse(opts[:bind] || "tcp://0.0.0.0:7289")
51
+ @root = Pathname.new(opts[:path] || "tmp")
52
+ @prefix = opts[:prefix] || "buffer"
53
+ @opts = opts
54
+ @output = Fwd::Output.new(self)
55
+ end
56
+
57
+ # Starts the loop
58
+ def run!
59
+ $0 = "fwd-rb (output)"
60
+
61
+ @piper = ::Servolux::Piper.new('rw')
62
+ at_exit do
63
+ @piper.signal("TERM")
64
+ end
65
+
66
+ @piper.child do
67
+ $0 = "fwd-rb (input)"
68
+ EM.run { listen! }
69
+ end
70
+
71
+ @piper.parent do
72
+ loop do
73
+ sleep(0.1)
74
+ case val = @piper.gets()
75
+ when FLUSH
76
+ output.forward!
77
+ else
78
+ logger.error "Received unknown message #{val.class.name} "
79
+ exit
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ # Starts the server
86
+ def listen!
87
+ logger.info "Starting server on #{@bind}"
88
+ EM.start_server @bind.host, @bind.port, Fwd::Input, self
89
+ end
90
+
91
+ # Initiates flush
92
+ def flush!
93
+ @piper.child do
94
+ @piper.puts(FLUSH)
95
+ end
96
+ end
97
+
98
+ # [Logger] logger instance
99
+ def logger
100
+ self.class.logger
101
+ end
102
+
103
+ end
104
+
105
+ %w|buffer output backend input pool cli|.each do |name|
106
+ require "fwd/#{name}"
107
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fwd::Backend do
4
+
5
+ subject do
6
+ described_class.new "tcp://127.0.0.1:7289"
7
+ end
8
+
9
+ before do
10
+ TCPSocket.any_instance.stub write: true
11
+ end
12
+
13
+ its(:url) { should be_instance_of(URI::Generic) }
14
+ its(:to_s) { should == "tcp://127.0.0.1:7289" }
15
+
16
+ end
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fwd::Buffer do
4
+
5
+ def files(glob = "*")
6
+ Dir[root.join(glob)]
7
+ end
8
+
9
+ let(:buffer) { described_class.new core }
10
+ let(:timer) { mock("Timer", cancel: true) }
11
+ subject { buffer }
12
+ before do
13
+ EM.stub add_periodic_timer: timer
14
+ end
15
+
16
+ its(:root) { should == root }
17
+ its(:root) { should be_exist }
18
+ its(:prefix) { should == "buffer" }
19
+ its(:core) { should be(core) }
20
+ its(:count) { should be(0) }
21
+ its(:interval) { should be(60) }
22
+ its(:rate) { should be(20) }
23
+ its(:limit) { should be(2048) }
24
+ its(:timer) { should be(timer) }
25
+ its(:fd) { should be_instance_of(File) }
26
+ its(:path) { should be_instance_of(Pathname) }
27
+ its(:logger) { should be(Fwd.logger) }
28
+
29
+ describe "concat" do
30
+ it 'should concat data' do
31
+ lambda {
32
+ subject.concat("x" * 1024)
33
+ }.should change {
34
+ subject.path.size
35
+ }.by(1024)
36
+ end
37
+ end
38
+
39
+ describe "rotate" do
40
+ before { buffer }
41
+ subject { lambda { buffer.rotate! } }
42
+
43
+ it { should change { buffer.path } }
44
+ it { should change { buffer.fd } }
45
+ it { should change { files.size }.by(1) }
46
+
47
+ it 'should archive previous file' do
48
+ previous = buffer.path
49
+ subject.call
50
+ files.should include(previous.sub("open", "closed").to_s)
51
+ end
52
+ end
53
+
54
+ describe "flush" do
55
+
56
+ before { core.stub flush!: true }
57
+
58
+ it 'should trigger when flush rate is reached' do
59
+ 19.times { subject.concat("x") }
60
+ lambda { subject.concat("x") }.should change { subject.count }.from(19).to(0)
61
+ end
62
+
63
+ it 'should trigger when flush limit is reached' do
64
+ subject.concat("x" * 1024)
65
+ lambda { subject.concat("x" * 1024) }.should change { subject.count }.from(1).to(0)
66
+ end
67
+
68
+ it 'should reset count' do
69
+ 3.times { subject.concat("x") }
70
+ lambda { subject.flush! }.should change { subject.count }.from(3).to(0)
71
+ end
72
+
73
+ it 'should rotate file' do
74
+ lambda { subject.flush! }.should change { subject.path }
75
+ files.size.should == 2
76
+ end
77
+
78
+ it 'should reset timer' do
79
+ subject.timer.should_receive(:cancel)
80
+ subject.flush!
81
+ end
82
+
83
+ it 'should forward data' do
84
+ 3.times { subject.concat("x") }
85
+ subject.core.should_receive(:flush!).and_return(true)
86
+ subject.flush!
87
+ sleep(0.1)
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fwd::CLI do
4
+
5
+ subject do
6
+ described_class.new [
7
+ "--path", root.to_s,
8
+ "--prefix", "prefix",
9
+ "--bind", "tcp://127.0.0.1:7289",
10
+ "--forward", "tcp://1.2.3.4:1234,tcp://1.2.3.5:1235",
11
+ "--flush", "30:1200:90",
12
+ ]
13
+ end
14
+
15
+ it { should be_a(Hash) }
16
+ its([:path]) { should == root.to_s }
17
+ its([:prefix]) { should == "prefix" }
18
+ its([:bind]) { should == "tcp://127.0.0.1:7289" }
19
+ its([:forward]) { should == ["tcp://1.2.3.4:1234", "tcp://1.2.3.5:1235"] }
20
+ its([:flush_limit]) { should == 30 }
21
+ its([:flush_rate]) { should == 1200 }
22
+ its([:flush_interval]) { should == 90 }
23
+ its(:core) { should be_instance_of(Fwd) }
24
+
25
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fwd::Input do
4
+
5
+ subject do
6
+ input = described_class.allocate
7
+ input.send(:initialize, core)
8
+ input
9
+ end
10
+ before { EM.stub :add_periodic_timer }
11
+
12
+ it { should be_a(EM::Connection) }
13
+ its(:buffer) { should be_nil }
14
+ its(:logger) { should be(Fwd.logger) }
15
+
16
+ describe "post init" do
17
+ before { subject.post_init }
18
+ its(:buffer) { should be_instance_of(Fwd::Buffer) }
19
+ end
20
+
21
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fwd::Output do
4
+
5
+ let(:output) { described_class.new core }
6
+ subject { output }
7
+
8
+ class MockServer
9
+ attr_reader :port, :data
10
+
11
+ def initialize(port)
12
+ @port = port
13
+ @data = ""
14
+ @server = ::TCPServer.new("127.0.0.1", port)
15
+ @thread = Thread.new do
16
+ loop do
17
+ conn = @server.accept
18
+ loop { @data << conn.readpartial(1024) }
19
+ end
20
+ end
21
+ sleep(0.001) until @thread.alive?
22
+ end
23
+
24
+ def stop
25
+ if @thread.alive?
26
+ @thread.kill
27
+ sleep(0.001) while @thread.alive?
28
+ end
29
+
30
+ unless @server.closed?
31
+ @server.close
32
+ end
33
+ end
34
+ end
35
+
36
+ def servers(*ports)
37
+ svs = ports.map {|port| MockServer.new(port) }
38
+ yield(*svs)
39
+ sleep(0.01)
40
+ svs.each(&:stop)
41
+ Hash[svs.map{|s| [s.port, s.data] }]
42
+ end
43
+
44
+ it 'should have a pool of backends' do
45
+ subject.pool.should be_instance_of(Fwd::Pool)
46
+ subject.pool.should have(2).items
47
+ subject.pool.checkout {|c| c.should be_instance_of(Fwd::Backend) }
48
+ end
49
+
50
+ describe "writing" do
51
+
52
+ it 'should forward data to backends' do
53
+ servers(7291, 7292) do
54
+ subject.write("A").should be(true)
55
+ subject.write("B").should be(true)
56
+ subject.write("C").should be(true)
57
+ subject.write("D").should be(true)
58
+ end.should == { 7291=>"BD", 7292=>"AC" }
59
+ end
60
+
61
+ it 'should handle partial fallouts' do
62
+ servers(7291) do
63
+ subject.write("A").should be(true)
64
+ subject.write("B").should be(true)
65
+ subject.write("C").should be(true)
66
+ subject.write("D").should be(true)
67
+ end.should == { 7291=>"ABCD" }
68
+ end
69
+
70
+ it 'should handle full fallouts' do
71
+ subject.write("A").should be(false)
72
+ subject.write("B").should be(false)
73
+ subject.write("C").should be(false)
74
+ subject.write("D").should be(false)
75
+ end
76
+
77
+ end
78
+
79
+ describe "forwarding" do
80
+
81
+ def write(file)
82
+ file.open("w") {|f| f << "x" }
83
+ file
84
+ end
85
+
86
+ def files(glob = "*")
87
+ Dir[root.join(glob)].map {|f| File.basename f }.sort
88
+ end
89
+
90
+ before { subject.stub! write: true }
91
+ before { FileUtils.mkdir_p root.to_s }
92
+ let!(:f1) { write root.join("buffer.1.closed") }
93
+ let!(:f2) { write root.join("buffer.2.open") }
94
+ let!(:f3) { write root.join("buffer.3.closed") }
95
+
96
+ it 'should write the data' do
97
+ subject.should_receive(:write).twice
98
+ subject.forward!
99
+ end
100
+
101
+ it 'should unlink written files' do
102
+ lambda { subject.forward! }.should change {
103
+ files
104
+ }.to(["buffer.2.open"])
105
+ end
106
+
107
+ it 'should handle failures files' do
108
+ subject.should_receive(:write).and_return(true)
109
+ subject.should_receive(:write).and_return(false)
110
+
111
+ lambda { subject.forward! }.should change {
112
+ files
113
+ }.to(["buffer.2.open", "buffer.3.closed"])
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fwd::Pool do
4
+
5
+ subject do
6
+ described_class.new ["A", "B", "C"]
7
+ end
8
+
9
+ it { should be_a(Enumerable) }
10
+ its(:to_a) { should == ["C", "B", "A"] }
11
+
12
+ it "should round-robin" do
13
+ subject.checkout {|c| c.should == "C" }
14
+ subject.checkout {|c| c.should == "B" }
15
+ subject.checkout {|c| c.should == "A" }
16
+ subject.checkout {|c| c.should == "C" }
17
+ subject.checkout {|c| c.should == "B" }
18
+ subject.checkout {|c| c.should == "A" }
19
+ end
20
+
21
+ end
22
+
data/spec/fwd_spec.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fwd do
4
+
5
+ subject { core }
6
+
7
+ its(:logger) { should be_instance_of(::Logger) }
8
+ its(:root) { should be_instance_of(::Pathname) }
9
+ its(:root) { should == root }
10
+ its(:bind) { should be_instance_of(URI::Generic) }
11
+ its(:bind) { should == URI("tcp://0.0.0.0:7289") }
12
+ its(:prefix) { should == "buffer" }
13
+ its(:opts) { should be_instance_of(Hash) }
14
+ its(:logger) { should be_instance_of(Logger) }
15
+ its(:output) { should be_instance_of(Fwd::Output) }
16
+
17
+ it "should listen the server" do
18
+ with_em do
19
+ subject.logger.should_receive(:info)
20
+ subject.listen!.should be_instance_of(Fixnum)
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,39 @@
1
+ require 'bundler/setup'
2
+ require 'rspec'
3
+ require 'fwd'
4
+
5
+ module Fwd::TestHelper
6
+
7
+ def with_em
8
+ EM.run do
9
+ begin
10
+ yield
11
+ ensure
12
+ EM.stop
13
+ end
14
+ end
15
+ end
16
+
17
+ def root
18
+ @_root ||= Pathname.new File.expand_path("../../tmp", __FILE__)
19
+ end
20
+
21
+ def core
22
+ @_core ||= Fwd.new \
23
+ path: root,
24
+ flush_rate: 20,
25
+ flush_limit: 2048,
26
+ forward: ["tcp://127.0.0.1:7291", "tcp://127.0.0.1:7292"]
27
+ end
28
+
29
+ end
30
+
31
+ RSpec.configure do |c|
32
+ c.include(Fwd::TestHelper)
33
+ c.before(:suite) do
34
+ Fwd.logger = Logger.new("/dev/null")
35
+ end
36
+ c.before(:each) do
37
+ FileUtils.rm_rf root.to_s
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fwd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Black Square Media
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine-le
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: servolux
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: connection_pool
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bundler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: yard
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: The minimalistic stream forwarder
127
+ email: info@blacksquaremedia.com
128
+ executables:
129
+ - fwd-rb
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - Gemfile
135
+ - Gemfile.lock
136
+ - Rakefile
137
+ - benchmark/performance.rb
138
+ - bin/fwd-rb
139
+ - fwd.gemspec
140
+ - lib/fwd.rb
141
+ - lib/fwd/backend.rb
142
+ - lib/fwd/buffer.rb
143
+ - lib/fwd/cli.rb
144
+ - lib/fwd/input.rb
145
+ - lib/fwd/output.rb
146
+ - lib/fwd/pool.rb
147
+ - lib/fwd/worker.rb
148
+ - spec/fwd/backend_spec.rb
149
+ - spec/fwd/buffer_spec.rb
150
+ - spec/fwd/cli_spec.rb
151
+ - spec/fwd/input_spec.rb
152
+ - spec/fwd/output_spec.rb
153
+ - spec/fwd/pool_spec.rb
154
+ - spec/fwd_spec.rb
155
+ - spec/spec_helper.rb
156
+ homepage: https://github.com/bsm/fwd
157
+ licenses: []
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ none: false
164
+ requirements:
165
+ - - ! '>='
166
+ - !ruby/object:Gem::Version
167
+ version: 1.9.1
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: 1.8.0
174
+ requirements: []
175
+ rubyforge_project:
176
+ rubygems_version: 1.8.24
177
+ signing_key:
178
+ specification_version: 3
179
+ summary: fwd >>
180
+ test_files: []
181
+ has_rdoc: