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 +2 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +35 -0
- data/Rakefile +11 -0
- data/benchmark/performance.rb +40 -0
- data/bin/fwd-rb +7 -0
- data/fwd.gemspec +31 -0
- data/lib/fwd/backend.rb +27 -0
- data/lib/fwd/buffer.rb +79 -0
- data/lib/fwd/cli.rb +60 -0
- data/lib/fwd/input.rb +22 -0
- data/lib/fwd/output.rb +66 -0
- data/lib/fwd/pool.rb +35 -0
- data/lib/fwd/worker.rb +43 -0
- data/lib/fwd.rb +107 -0
- data/spec/fwd/backend_spec.rb +16 -0
- data/spec/fwd/buffer_spec.rb +91 -0
- data/spec/fwd/cli_spec.rb +25 -0
- data/spec/fwd/input_spec.rb +21 -0
- data/spec/fwd/output_spec.rb +117 -0
- data/spec/fwd/pool_spec.rb +22 -0
- data/spec/fwd_spec.rb +24 -0
- data/spec/spec_helper.rb +39 -0
- metadata +181 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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
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
|
data/lib/fwd/backend.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|