faymora 0.1
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.
- checksums.yaml +7 -0
- data/Gemfile +42 -0
- data/LICENSE.md +375 -0
- data/README.md +13 -0
- data/faymora.gemspec +35 -0
- data/lib/faymora/client.rb +26 -0
- data/lib/faymora/connection.rb +33 -0
- data/lib/faymora/instance.rb +120 -0
- data/lib/faymora/item.rb +29 -0
- data/lib/faymora/link.rb +55 -0
- data/lib/faymora/node.rb +52 -0
- data/lib/faymora/provision.rb +14 -0
- data/lib/faymora/rectifier.rb +18 -0
- data/lib/faymora/rpc.rb +17 -0
- data/lib/faymora/service.rb +59 -0
- data/lib/faymora/throughput.rb +46 -0
- data/lib/faymora/version.rb +3 -0
- data/lib/faymora.rb +17 -0
- data/spec/faymora/client_spec.rb +56 -0
- data/spec/faymora/connection_spec.rb +89 -0
- data/spec/faymora/item_spec.rb +80 -0
- data/spec/faymora/link_spec.rb +140 -0
- data/spec/faymora/node_spec.rb +107 -0
- data/spec/faymora/provision_spec.rb +33 -0
- data/spec/faymora/rpc_spec.rb +43 -0
- data/spec/faymora/service_spec.rb +102 -0
- data/spec/faymora/throughput_spec.rb +125 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/gloox/node.rb +21 -0
- data/spec/support/gloox.rb +12 -0
- data/spec/support/slotz.rb +10 -0
- metadata +127 -0
data/lib/faymora/link.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Faymora
|
|
2
|
+
module Link
|
|
3
|
+
|
|
4
|
+
BUFFER = []
|
|
5
|
+
|
|
6
|
+
def input( item )
|
|
7
|
+
item = Item.from_data( item )
|
|
8
|
+
item.passes ||= []
|
|
9
|
+
|
|
10
|
+
on_item item
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def output_to( other, propagate = true )
|
|
15
|
+
other = Client.from_data( other )
|
|
16
|
+
@output = other
|
|
17
|
+
@output.input_from( self.client, false ){} if propagate
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def input_from( other, propagate = true )
|
|
22
|
+
other = Client.from_data( other )
|
|
23
|
+
@input = other
|
|
24
|
+
@input.output_to( self.client, false ){} if propagate
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def has_output?
|
|
29
|
+
!!@output
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def has_input?
|
|
33
|
+
!!@input
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def forward( item )
|
|
37
|
+
return if !has_output?
|
|
38
|
+
|
|
39
|
+
item.passes << Item::Pass.new( path: self.class.to_s )
|
|
40
|
+
|
|
41
|
+
BUFFER << item
|
|
42
|
+
while !BUFFER.empty?
|
|
43
|
+
begin
|
|
44
|
+
@output.input( BUFFER.pop ) {}
|
|
45
|
+
rescue => e
|
|
46
|
+
ap e
|
|
47
|
+
ap e.backtrace
|
|
48
|
+
BUFFER << item if !BUFFER.include? item
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
data/lib/faymora/node.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'gloox/node'
|
|
2
|
+
|
|
3
|
+
module Faymora
|
|
4
|
+
class Node < GlooX::Node
|
|
5
|
+
|
|
6
|
+
class <<self
|
|
7
|
+
|
|
8
|
+
def when_ready( url, &block )
|
|
9
|
+
client = Faymora::Client.new(
|
|
10
|
+
url: url,
|
|
11
|
+
client_max_retries: 0,
|
|
12
|
+
connection_pool_size: 1,
|
|
13
|
+
handler: :node )
|
|
14
|
+
|
|
15
|
+
r = Raktr.new
|
|
16
|
+
r.run_in_thread
|
|
17
|
+
r.delay( 0.1 ) do |task|
|
|
18
|
+
client.alive? do |r|
|
|
19
|
+
if r.rpc_exception?
|
|
20
|
+
r.delay( 0.1, &task )
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
client.close
|
|
25
|
+
block.call
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def connect( options = {} )
|
|
31
|
+
Faymora::Client.new(
|
|
32
|
+
options.merge( handler: :node )
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def boot( options = {} )
|
|
37
|
+
connect( url: start( options ).url )
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def start( options = {} )
|
|
41
|
+
n = new( options )
|
|
42
|
+
n.start
|
|
43
|
+
n
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize( options = {} )
|
|
48
|
+
super( options.merge serializer: ::MessagePack )
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Faymora
|
|
2
|
+
module Rectifier
|
|
3
|
+
# rectifies the bottleneck
|
|
4
|
+
def set_rectifier( rectifier )
|
|
5
|
+
@rectifier = rectifier
|
|
6
|
+
# use its output as input, keep the usual output for the process.
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def rectifier
|
|
10
|
+
@rectifier ||= eXipno.spawn( :rectifier, self.client )
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def rectify
|
|
14
|
+
rectifier.rectify
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
data/lib/faymora/rpc.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Faymora
|
|
2
|
+
module Service
|
|
3
|
+
include Link
|
|
4
|
+
include RPC
|
|
5
|
+
include Connection
|
|
6
|
+
include Throughput
|
|
7
|
+
# include Rectifier
|
|
8
|
+
|
|
9
|
+
# SHARE = Share.new( ARGUMENTS['peers'] )
|
|
10
|
+
|
|
11
|
+
def self.included( service )
|
|
12
|
+
def service.interested_in( &block )
|
|
13
|
+
@would_interest = block
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def service.would_interest?( item )
|
|
17
|
+
return true if !@would_interest
|
|
18
|
+
@would_interest.call item
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def service.upon( &block )
|
|
22
|
+
@upon = block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def service._upon
|
|
26
|
+
@upon
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def join( &block )
|
|
31
|
+
block.call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def whereis?( name )
|
|
35
|
+
# find client for service by absolute path name. Ask Agent.
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def process( item )
|
|
39
|
+
item = Item.from_data( item ) if !item.is_a?( Item )
|
|
40
|
+
return consume( item ) if interested?( item )
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Interested in working on the item?
|
|
45
|
+
def interested?( item )
|
|
46
|
+
!!self.class.would_interest?( item )
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def interested!( item )
|
|
50
|
+
item.interesting = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def consume( item )
|
|
54
|
+
self.class._upon.call( item )
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Faymora
|
|
2
|
+
module Throughput
|
|
3
|
+
WARMED_UP_CLICKS = 50
|
|
4
|
+
|
|
5
|
+
def initialize(*)
|
|
6
|
+
super(*)
|
|
7
|
+
@clock_counter = 0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def throughput
|
|
11
|
+
return nil unless @click_counter && @click_counter >= WARMED_UP_CLICKS
|
|
12
|
+
return nil if @clock_counter.to_i <= 0
|
|
13
|
+
|
|
14
|
+
@click_counter.to_f / @clock_counter.to_f
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def bottleneck?
|
|
18
|
+
return false unless has_input? && throughput
|
|
19
|
+
|
|
20
|
+
input_t = @input.throughput
|
|
21
|
+
return false unless input_t
|
|
22
|
+
|
|
23
|
+
input_t > throughput
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clock( item, &block )
|
|
27
|
+
@clock_item = Time.now
|
|
28
|
+
@clock_counter += 1
|
|
29
|
+
|
|
30
|
+
block.call item
|
|
31
|
+
ensure
|
|
32
|
+
click!( item )
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def click!( item )
|
|
36
|
+
@clock_item = @clock_item.to_i
|
|
37
|
+
@click_counter ||= 0
|
|
38
|
+
@click_counter += 1
|
|
39
|
+
|
|
40
|
+
rectify if bottleneck?
|
|
41
|
+
ensure
|
|
42
|
+
GC.start
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
data/lib/faymora.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'gloox'
|
|
2
|
+
require 'msgpack'
|
|
3
|
+
|
|
4
|
+
module Faymora
|
|
5
|
+
require_relative 'faymora/version'
|
|
6
|
+
require_relative 'faymora/provision'
|
|
7
|
+
require_relative 'faymora/item'
|
|
8
|
+
require_relative 'faymora/rpc'
|
|
9
|
+
require_relative 'faymora/connection'
|
|
10
|
+
require_relative 'faymora/rectifier'
|
|
11
|
+
require_relative 'faymora/instance'
|
|
12
|
+
require_relative 'faymora/client'
|
|
13
|
+
require_relative 'faymora/node'
|
|
14
|
+
require_relative 'faymora/link'
|
|
15
|
+
require_relative 'faymora/throughput'
|
|
16
|
+
require_relative 'faymora/service'
|
|
17
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Faymora::Client do
|
|
4
|
+
let(:options_string_keys) { { 'host' => 'example.org', 'port' => 5222, 'user' => 'alice' } }
|
|
5
|
+
let(:options) { { host: 'example.org', port: 5222, user: 'alice' } }
|
|
6
|
+
|
|
7
|
+
describe '#initialize' do
|
|
8
|
+
it 'inherits from GlooX::Client' do
|
|
9
|
+
expect(described_class < GlooX::Client).to be true
|
|
10
|
+
expect(described_class.new(options)).to be_a(GlooX::Client)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'symbolizes option keys and forces serializer to MessagePack' do
|
|
14
|
+
c = described_class.new(options_string_keys)
|
|
15
|
+
expect(c.opts).to include(options)
|
|
16
|
+
expect(c.opts[:serializer]).to be(MessagePack)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'overrides any provided serializer option' do
|
|
20
|
+
c = described_class.new(options.merge(serializer: :something_else))
|
|
21
|
+
expect(c.opts[:serializer]).to be(MessagePack)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '#to_data' do
|
|
26
|
+
it 'returns options without :serializer' do
|
|
27
|
+
c = described_class.new(options)
|
|
28
|
+
data = c.to_data
|
|
29
|
+
expect(data).to eq(options)
|
|
30
|
+
expect(data).not_to have_key(:serializer)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#to_msgpack' do
|
|
35
|
+
it 'packs #to_data into MessagePack using a provided packer' do
|
|
36
|
+
c = described_class.new(options)
|
|
37
|
+
packer = MessagePack::Packer.new
|
|
38
|
+
c.to_msgpack(packer)
|
|
39
|
+
packed = packer.to_s
|
|
40
|
+
expect(MessagePack.unpack(packed).symbolize_keys).to eq(c.to_data)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '.from_data' do
|
|
45
|
+
it 'builds a client from Hash with string keys equivalent to symbolized options' do
|
|
46
|
+
c = described_class.from_data(options_string_keys)
|
|
47
|
+
expect(c).to be_a(described_class)
|
|
48
|
+
expect(c.to_data).to eq(options)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'builds a client from Hash with symbol keys as-is' do
|
|
52
|
+
c = described_class.from_data(options)
|
|
53
|
+
expect(c.to_data).to eq(options)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Faymora::Connection do
|
|
4
|
+
# Minimal harness including the module under test
|
|
5
|
+
class DummyConnection
|
|
6
|
+
include Faymora::Connection
|
|
7
|
+
|
|
8
|
+
attr_reader :forwarded, :clocked
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@forwarded = []
|
|
12
|
+
@clocked = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def forward(item)
|
|
16
|
+
@forwarded << item
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Simulate Throughput#clock without side effects; just yield the item
|
|
21
|
+
def clock(item)
|
|
22
|
+
@clocked << item
|
|
23
|
+
yield item if block_given?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Allow tests to stub this
|
|
27
|
+
def process(item)
|
|
28
|
+
item
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:conn) { DummyConnection.new }
|
|
33
|
+
let(:item) { Faymora::Item.new(foo: 'bar') }
|
|
34
|
+
|
|
35
|
+
describe '#on_item' do
|
|
36
|
+
context 'when passthrough? is true' do
|
|
37
|
+
it 'forwards the original item and returns nil' do
|
|
38
|
+
allow(conn).to receive(:passthrough?).with(item).and_return(true)
|
|
39
|
+
|
|
40
|
+
result = conn.on_item(item)
|
|
41
|
+
|
|
42
|
+
expect(conn.forwarded).to eq([item])
|
|
43
|
+
expect(result).to be_nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context 'when passthrough? is false' do
|
|
48
|
+
before { allow(conn).to receive(:passthrough?).with(item).and_return(false) }
|
|
49
|
+
|
|
50
|
+
it 'calls clock with the item and processes it' do
|
|
51
|
+
expect(conn).to receive(:clock).with(item).and_call_original
|
|
52
|
+
expect(conn).to receive(:process).with(item).and_return(item)
|
|
53
|
+
|
|
54
|
+
conn.on_item(item)
|
|
55
|
+
|
|
56
|
+
expect(conn.clocked).to eq([item])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'forwards the result if process returns an Item' do
|
|
60
|
+
processed = Faymora::Item.new(baz: 1)
|
|
61
|
+
allow(conn).to receive(:process).and_return(processed)
|
|
62
|
+
|
|
63
|
+
result = conn.on_item(item)
|
|
64
|
+
|
|
65
|
+
expect(conn.forwarded).to eq([processed])
|
|
66
|
+
expect(result).to be_nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'does not forward if process returns a non-Item' do
|
|
70
|
+
allow(conn).to receive(:process).and_return(:noop)
|
|
71
|
+
|
|
72
|
+
conn.on_item(item)
|
|
73
|
+
|
|
74
|
+
expect(conn.forwarded).to be_empty
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '#on_error' do
|
|
80
|
+
it 'stores the error handler block' do
|
|
81
|
+
blk = proc { |e| :handled }
|
|
82
|
+
conn.on_error(&blk)
|
|
83
|
+
|
|
84
|
+
handler = conn.instance_variable_get(:@on_error)
|
|
85
|
+
expect(handler).to be_a(Proc)
|
|
86
|
+
expect(handler.call(StandardError.new)).to eq(:handled)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Faymora::Item do
|
|
4
|
+
describe '.from_data' do
|
|
5
|
+
it 'converts passes (array of hashes) into Pass instances' do
|
|
6
|
+
data = { foo: 1, passes: [{ path: 'A' }, { path: 'B', extra: 2 }] }
|
|
7
|
+
item = described_class.from_data(data)
|
|
8
|
+
|
|
9
|
+
expect(item).to be_a(described_class)
|
|
10
|
+
expect(item.foo).to eq(1)
|
|
11
|
+
expect(item.passes).to all(be_a(Faymora::Item::Pass))
|
|
12
|
+
expect(item.passes.map(&:path)).to eq(%w[A B])
|
|
13
|
+
expect(item.passes.last.extra).to eq(2)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'handles missing passes key by creating an empty array' do
|
|
17
|
+
item = described_class.from_data(foo: 'bar')
|
|
18
|
+
expect(item.passes).to eq([])
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#to_msgpack' do
|
|
23
|
+
it 'packs all attributes including passes converted via Pass#to_msgpack' do
|
|
24
|
+
pass1 = Faymora::Item::Pass.new(path: 'X', n: 1)
|
|
25
|
+
pass2 = Faymora::Item::Pass.new(path: 'Y')
|
|
26
|
+
item = described_class.new(foo: 'bar', passes: [pass1, pass2])
|
|
27
|
+
|
|
28
|
+
packer = MessagePack::Packer.new
|
|
29
|
+
item.to_msgpack(packer)
|
|
30
|
+
packed = packer.to_s
|
|
31
|
+
|
|
32
|
+
unpacked = MessagePack.unpack(packed)
|
|
33
|
+
unpacked = unpacked.symbolize_keys if unpacked.respond_to?(:symbolize_keys)
|
|
34
|
+
|
|
35
|
+
expect(unpacked[:foo]).to eq('bar')
|
|
36
|
+
# Passes are stored as MessagePack-dumped strings
|
|
37
|
+
expect(unpacked[:passes]).to be_an(Array)
|
|
38
|
+
expect(unpacked[:passes].size).to eq(2)
|
|
39
|
+
|
|
40
|
+
# Validate the first pass payload decodes to the original hash
|
|
41
|
+
decoded_first = MessagePack.unpack(unpacked[:passes][0])
|
|
42
|
+
decoded_first = decoded_first.symbolize_keys if decoded_first.respond_to?(:symbolize_keys)
|
|
43
|
+
expect(decoded_first).to include(path: 'X', n: 1)
|
|
44
|
+
|
|
45
|
+
decoded_second = MessagePack.unpack(unpacked[:passes][1])
|
|
46
|
+
decoded_second = decoded_second.symbolize_keys if decoded_second.respond_to?(:symbolize_keys)
|
|
47
|
+
expect(decoded_second).to include(path: 'Y')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'works when passes is nil by treating it as empty' do
|
|
51
|
+
item = described_class.new(foo: 1, passes: nil)
|
|
52
|
+
packer = MessagePack::Packer.new
|
|
53
|
+
expect { item.to_msgpack(packer) }.not_to raise_error
|
|
54
|
+
unpacked = MessagePack.unpack(packer.to_s)
|
|
55
|
+
unpacked = unpacked.symbolize_keys if unpacked.respond_to?(:symbolize_keys)
|
|
56
|
+
expect(unpacked[:passes]).to eq([])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe 'Pass' do
|
|
61
|
+
describe '.from_data' do
|
|
62
|
+
it 'builds a Pass from provided hash' do
|
|
63
|
+
pass = described_class::Pass.from_data(path: 'P', hop: 3)
|
|
64
|
+
expect(pass).to be_a(described_class::Pass)
|
|
65
|
+
expect(pass.path).to eq('P')
|
|
66
|
+
expect(pass.hop).to eq(3)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#to_msgpack' do
|
|
71
|
+
it 'serializes the pass to MessagePack bytes' do
|
|
72
|
+
pass = described_class::Pass.new(path: 'Route', ok: true)
|
|
73
|
+
bytes = pass.to_msgpack
|
|
74
|
+
decoded = MessagePack.unpack(bytes)
|
|
75
|
+
decoded = decoded.symbolize_keys if decoded.respond_to?(:symbolize_keys)
|
|
76
|
+
expect(decoded).to eq({ path: 'Route', ok: true })
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Faymora::Link do
|
|
4
|
+
# A simple host class including the module under test
|
|
5
|
+
class DummyLink
|
|
6
|
+
include Faymora::Link
|
|
7
|
+
|
|
8
|
+
attr_reader :handled
|
|
9
|
+
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client_obj = client
|
|
12
|
+
@handled = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_item(item)
|
|
16
|
+
@handled << item
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def client
|
|
20
|
+
@client_obj
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
let(:self_client) { instance_double(Faymora::Client) }
|
|
25
|
+
let(:dummy) { DummyLink.new(self_client) }
|
|
26
|
+
|
|
27
|
+
before do
|
|
28
|
+
# Ensure the shared buffer does not bleed across examples
|
|
29
|
+
Faymora::Link::BUFFER.clear
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#input' do
|
|
33
|
+
it 'converts data to Item via Item.from_data, calls on_item, and returns nil' do
|
|
34
|
+
raw = { foo: 1 }
|
|
35
|
+
expect(Faymora::Item).to receive(:from_data).with(raw).and_call_original
|
|
36
|
+
|
|
37
|
+
result = dummy.input(raw)
|
|
38
|
+
|
|
39
|
+
expect(result).to be_nil
|
|
40
|
+
expect(dummy.handled.size).to eq(1)
|
|
41
|
+
expect(dummy.handled.first).to be_a(Faymora::Item)
|
|
42
|
+
expect(dummy.handled.first.foo).to eq(1)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe '#output_to' do
|
|
47
|
+
let(:other_opts) { { host: 'h', port: 1 } }
|
|
48
|
+
|
|
49
|
+
it 'sets @output from Client.from_data and propagates to input_from by default' do
|
|
50
|
+
output_client = instance_double(Faymora::Link)
|
|
51
|
+
expect(Faymora::Client).to receive(:from_data).with(other_opts).and_return(output_client)
|
|
52
|
+
expect(output_client).to receive(:input_from).with(self_client, false)
|
|
53
|
+
|
|
54
|
+
expect(dummy.output_to(other_opts)).to be_nil
|
|
55
|
+
expect(dummy.has_output?).to be true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'does not propagate when propagate=false' do
|
|
59
|
+
output_client = instance_double(Faymora::Link)
|
|
60
|
+
expect(Faymora::Client).to receive(:from_data).with(other_opts).and_return(output_client)
|
|
61
|
+
expect(output_client).not_to receive(:input_from)
|
|
62
|
+
|
|
63
|
+
expect(dummy.output_to(other_opts, false)).to be_nil
|
|
64
|
+
expect(dummy.has_output?).to be true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#input_from' do
|
|
69
|
+
let(:other_opts) { { host: 'h2', port: 2 } }
|
|
70
|
+
|
|
71
|
+
it 'sets @input from Client.from_data and propagates to output_to by default' do
|
|
72
|
+
input_client = instance_double(Faymora::Link)
|
|
73
|
+
expect(Faymora::Client).to receive(:from_data).with(other_opts).and_return(input_client)
|
|
74
|
+
expect(input_client).to receive(:output_to).with(self_client, false)
|
|
75
|
+
|
|
76
|
+
expect(dummy.input_from(other_opts)).to be_nil
|
|
77
|
+
expect(dummy.has_input?).to be true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'does not propagate when propagate=false' do
|
|
81
|
+
input_client = instance_double(Faymora::Link)
|
|
82
|
+
expect(Faymora::Client).to receive(:from_data).with(other_opts).and_return(input_client)
|
|
83
|
+
expect(input_client).not_to receive(:output_to)
|
|
84
|
+
|
|
85
|
+
expect(dummy.input_from(other_opts, false)).to be_nil
|
|
86
|
+
expect(dummy.has_input?).to be true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#forward' do
|
|
91
|
+
let(:item) { Faymora::Item.new(data: 123) }
|
|
92
|
+
|
|
93
|
+
it 'is a no-op when there is no output' do
|
|
94
|
+
expect(dummy.has_output?).to be false
|
|
95
|
+
# Nothing should be enqueued after forward is attempted
|
|
96
|
+
dummy.forward(item)
|
|
97
|
+
expect(Faymora::Link::BUFFER).to be_empty
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'sends item to @output.input and drains the buffer' do
|
|
101
|
+
output_client = instance_double(Faymora::Link)
|
|
102
|
+
allow(Faymora::Client).to receive(:from_data).and_return(output_client)
|
|
103
|
+
allow(output_client).to receive(:input_from)
|
|
104
|
+
dummy.output_to(host: 'h', port: 1)
|
|
105
|
+
|
|
106
|
+
expect(output_client).to receive(:input) do |arg, &blk|
|
|
107
|
+
expect(arg).to eq(item)
|
|
108
|
+
blk&.call # block is accepted but unused here
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
dummy.forward(item)
|
|
112
|
+
expect(Faymora::Link::BUFFER).to be_empty
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'retries when @output.input raises once' do
|
|
116
|
+
# Provide Kernel.ap to avoid NameError from logging in rescue
|
|
117
|
+
unless Kernel.method_defined?(:ap)
|
|
118
|
+
module Kernel
|
|
119
|
+
def ap(*)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
output_client = instance_double(Faymora::Link)
|
|
125
|
+
allow(Faymora::Client).to receive(:from_data).and_return(output_client)
|
|
126
|
+
allow(output_client).to receive(:input_from)
|
|
127
|
+
dummy.output_to(host: 'h', port: 1)
|
|
128
|
+
|
|
129
|
+
calls = 0
|
|
130
|
+
allow(output_client).to receive(:input) do |_arg|
|
|
131
|
+
calls += 1
|
|
132
|
+
raise 'boom' if calls == 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
dummy.forward(item)
|
|
136
|
+
expect(calls).to eq(2)
|
|
137
|
+
expect(Faymora::Link::BUFFER).to be_empty
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|