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.
@@ -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
@@ -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,14 @@
1
+ require 'slotz'
2
+
3
+ module Faymora
4
+ module Provision
5
+ include Slotz::Reservation
6
+
7
+ def self.included( other )
8
+ def other.provision( options = {} )
9
+ Slotz::Reservation.provision( self, options )
10
+ end
11
+ end
12
+
13
+ end
14
+ 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
@@ -0,0 +1,17 @@
1
+ module Faymora
2
+ module RPC
3
+
4
+ def client
5
+ Client.new( url: self.url, handler: :instance )
6
+ end
7
+
8
+ def server
9
+ self
10
+ end
11
+
12
+ # @abstract
13
+ def node
14
+ end
15
+ end
16
+
17
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Faymora
2
+ VERSION = '0.1'
3
+ 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