fwd 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: