funl 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +22 -0
- data/README.md +4 -0
- data/Rakefile +9 -0
- data/lib/funl/blobber.rb +31 -0
- data/lib/funl/client-sequencer.rb +67 -0
- data/lib/funl/client.rb +74 -0
- data/lib/funl/history-client.rb +12 -0
- data/lib/funl/history-worker.rb +13 -0
- data/lib/funl/message-sequencer.rb +135 -0
- data/lib/funl/message.rb +104 -0
- data/lib/funl/stream.rb +34 -0
- data/test/test-client-sequencer.rb +69 -0
- data/test/test-client.rb +50 -0
- data/test/test-message-sequencer.rb +122 -0
- data/test/test-message.rb +75 -0
- data/test/test-stream.rb +53 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 015bd0c0d3e5537bb8beeda5ccd1aa7502dd66c5
|
4
|
+
data.tar.gz: c5ce417f5a23cfce7a9d9aa920b6a65cc405dfff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ccf71263bcda9155f8733c2a7f7adc0d23d526af9af16faa75ba0fed4fc4f59885444d2782550283aa52a7ec0e8c77435bb1aca9752d60ae349f00ebc73629dc
|
7
|
+
data.tar.gz: 10f240e8925c2aa63f21ac05f5ca12871d0df7f10d775e741019ecc1a889e927b57fdb183e211f4a4ce76b1c471883ec8b1f5222b57dd4b9ed899fe1b409268b
|
data/COPYING
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013, Joel VanderWerf, vjoel@users.sourceforge.net
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
14
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
15
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
17
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
18
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
19
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
20
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
21
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
22
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/lib/funl/blobber.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'object-stream'
|
2
|
+
|
3
|
+
module Funl
|
4
|
+
module Blobber
|
5
|
+
MARSHAL_TYPE = ObjectStream::MARSHAL_TYPE
|
6
|
+
YAML_TYPE = ObjectStream::YAML_TYPE
|
7
|
+
JSON_TYPE = ObjectStream::JSON_TYPE
|
8
|
+
MSGPACK_TYPE = ObjectStream::MSGPACK_TYPE
|
9
|
+
|
10
|
+
# Returns something which responds to #dump(obj) and #load(str).
|
11
|
+
def self.for type
|
12
|
+
case type
|
13
|
+
when MARSHAL_TYPE
|
14
|
+
Marshal
|
15
|
+
when YAML_TYPE
|
16
|
+
require 'yaml'
|
17
|
+
YAML
|
18
|
+
when JSON_TYPE
|
19
|
+
require 'yajl'
|
20
|
+
require 'yajl/json_gem'
|
21
|
+
## would 'json' conflict with yajl required from other libs?
|
22
|
+
JSON
|
23
|
+
when MSGPACK_TYPE
|
24
|
+
require 'msgpack'
|
25
|
+
MessagePack
|
26
|
+
else
|
27
|
+
raise ArgumentError, "unknown type: #{type.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'object-stream'
|
3
|
+
require 'funl/stream'
|
4
|
+
|
5
|
+
module Funl
|
6
|
+
# Assigns unique ids to clients.
|
7
|
+
class ClientSequencer
|
8
|
+
attr_reader :server
|
9
|
+
attr_reader :server_thread
|
10
|
+
attr_reader :next_id
|
11
|
+
attr_reader :log
|
12
|
+
attr_reader :stream_type
|
13
|
+
|
14
|
+
def initialize server, *conns, log: Logger.new($stderr),
|
15
|
+
stream_type: ObjectStream::MSGPACK_TYPE,
|
16
|
+
next_id: 0
|
17
|
+
|
18
|
+
@server = server
|
19
|
+
@log = log
|
20
|
+
@stream_type = stream_type
|
21
|
+
@next_id = next_id
|
22
|
+
|
23
|
+
conns.each do |conn|
|
24
|
+
handle_conn conn
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
@server_thread = Thread.new do
|
30
|
+
run
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def stop
|
35
|
+
server_thread.kill if server_thread
|
36
|
+
end
|
37
|
+
|
38
|
+
def wait
|
39
|
+
server_thread.join
|
40
|
+
end
|
41
|
+
|
42
|
+
def run
|
43
|
+
loop do
|
44
|
+
conn = server.accept
|
45
|
+
log.debug {"accepted #{conn.inspect}"}
|
46
|
+
handle_conn conn
|
47
|
+
end
|
48
|
+
rescue => ex
|
49
|
+
log.error ex
|
50
|
+
raise
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_conn conn
|
54
|
+
stream = ObjectStream.new(conn, type: stream_type)
|
55
|
+
msg = {"client_id" => next_id}
|
56
|
+
@next_id += 1
|
57
|
+
stream << msg
|
58
|
+
rescue IOError, SystemCallError => ex
|
59
|
+
log.error "write error for client #{next_id}: #{ex}"
|
60
|
+
else
|
61
|
+
log.info "recognized client #{next_id}"
|
62
|
+
ensure
|
63
|
+
stream.close if stream and not stream.closed?
|
64
|
+
end
|
65
|
+
private :handle_conn
|
66
|
+
end
|
67
|
+
end
|
data/lib/funl/client.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'funl/stream'
|
3
|
+
require 'funl/message'
|
4
|
+
require 'funl/blobber'
|
5
|
+
|
6
|
+
module Funl
|
7
|
+
# Generic client base class. Manages the setup and handshake on the streams
|
8
|
+
# to the client sequencer and the message sequencer.
|
9
|
+
class Client
|
10
|
+
include Funl::Stream
|
11
|
+
|
12
|
+
attr_reader :seq
|
13
|
+
attr_reader :cseq
|
14
|
+
attr_reader :arc
|
15
|
+
attr_reader :log
|
16
|
+
attr_reader :stream_type
|
17
|
+
attr_reader :message_class
|
18
|
+
attr_reader :client_id
|
19
|
+
attr_reader :greeting
|
20
|
+
attr_reader :start_tick
|
21
|
+
attr_reader :blob_type
|
22
|
+
attr_reader :blobber
|
23
|
+
|
24
|
+
def initialize(seq: seq!, cseq: cseq!, arc: nil,
|
25
|
+
log: Logger.new($stderr),
|
26
|
+
stream_type: ObjectStream::MSGPACK_TYPE,
|
27
|
+
message_class: Message)
|
28
|
+
|
29
|
+
@log = log
|
30
|
+
@stream_type = stream_type ## discover this thru connections
|
31
|
+
@message_class = message_class
|
32
|
+
|
33
|
+
@seq = client_stream_for(seq)
|
34
|
+
@cseq = client_stream_for(cseq)
|
35
|
+
@arcio = arc
|
36
|
+
end
|
37
|
+
|
38
|
+
# Handshake with both cseq and seq. Does not start any threads--that is left
|
39
|
+
# to subclasses. Yields after getting client id so that caller can set
|
40
|
+
# log.progname, for example.
|
41
|
+
def start
|
42
|
+
cseq_read_client_id
|
43
|
+
yield if block_given?
|
44
|
+
seq_read_greeting
|
45
|
+
end
|
46
|
+
|
47
|
+
def cseq_read_client_id
|
48
|
+
log.info "getting client_id from cseq"
|
49
|
+
@client_id = cseq.read["client_id"]
|
50
|
+
log.info "client_id = #{client_id}"
|
51
|
+
cseq.close rescue nil
|
52
|
+
@cseq = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def seq_read_greeting
|
56
|
+
log.info "getting greeting from seq"
|
57
|
+
@greeting = seq.read
|
58
|
+
@start_tick = greeting["tick"]
|
59
|
+
log.info "start_tick = #{start_tick}"
|
60
|
+
@blob_type = greeting["blob"]
|
61
|
+
log.info "blob_type = #{blob_type}"
|
62
|
+
@blobber = Blobber.for(blob_type)
|
63
|
+
seq.expect message_class
|
64
|
+
|
65
|
+
@arc = @arcio && client_stream_for(@arcio, type: blob_type)
|
66
|
+
# note: @arc is nil when client is the archiver itself
|
67
|
+
end
|
68
|
+
|
69
|
+
def arc_server_stream_for io
|
70
|
+
server_stream_for(io, type: blob_type)
|
71
|
+
# note: blob_type, not stream_type, since we are sending bare objects
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Funl::HistoryWorker
|
2
|
+
attr_reader :history
|
3
|
+
|
4
|
+
def initialize client
|
5
|
+
super
|
6
|
+
@history = client.history_size && Array.new(client.history_size)
|
7
|
+
# nil in case of use without UDP
|
8
|
+
end
|
9
|
+
|
10
|
+
def record_history msg
|
11
|
+
history[global_tick % history.size] = msg if history
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'funl/stream'
|
3
|
+
require 'funl/message'
|
4
|
+
require 'funl/blobber'
|
5
|
+
|
6
|
+
module Funl
|
7
|
+
# Assigns a unique sequential ids to each message and relays it to its
|
8
|
+
# destinations.
|
9
|
+
class MessageSequencer
|
10
|
+
include Funl::Stream
|
11
|
+
|
12
|
+
attr_reader :server
|
13
|
+
attr_reader :server_thread
|
14
|
+
attr_reader :streams
|
15
|
+
attr_reader :tick
|
16
|
+
attr_reader :log
|
17
|
+
attr_reader :stream_type
|
18
|
+
attr_reader :message_class
|
19
|
+
attr_reader :blob_type
|
20
|
+
attr_reader :greeting
|
21
|
+
|
22
|
+
def initialize server, *conns, log: Logger.new($stderr),
|
23
|
+
stream_type: ObjectStream::MSGPACK_TYPE,
|
24
|
+
message_class: Message,
|
25
|
+
blob_type: Blobber::MSGPACK_TYPE,
|
26
|
+
tick: 0
|
27
|
+
|
28
|
+
@server = server
|
29
|
+
@log = log
|
30
|
+
@stream_type = stream_type
|
31
|
+
@message_class = message_class
|
32
|
+
@blob_type = blob_type
|
33
|
+
@greeting = default_greeting
|
34
|
+
@tick = tick
|
35
|
+
|
36
|
+
@streams = []
|
37
|
+
conns.each do |conn|
|
38
|
+
try_conn conn
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_greeting
|
43
|
+
{
|
44
|
+
"blob" => blob_type
|
45
|
+
}.freeze # can't change after initial conns read it
|
46
|
+
end
|
47
|
+
|
48
|
+
def try_conn conn
|
49
|
+
stream = message_server_stream_for(conn)
|
50
|
+
current_greeting = greeting.merge({"tick" => tick})
|
51
|
+
if write_succeeds?(current_greeting, stream)
|
52
|
+
log.info "connected #{stream.inspect}"
|
53
|
+
@streams << stream
|
54
|
+
end
|
55
|
+
end
|
56
|
+
private :try_conn
|
57
|
+
|
58
|
+
def start
|
59
|
+
@server_thread = Thread.new do
|
60
|
+
run
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop
|
65
|
+
server_thread.kill if server_thread
|
66
|
+
end
|
67
|
+
|
68
|
+
def wait
|
69
|
+
server_thread.join
|
70
|
+
end
|
71
|
+
|
72
|
+
def run
|
73
|
+
loop do
|
74
|
+
readables, _ = select [server, *streams]
|
75
|
+
|
76
|
+
readables.each do |readable|
|
77
|
+
case readable
|
78
|
+
when server
|
79
|
+
begin
|
80
|
+
conn, addr = readable.accept_nonblock
|
81
|
+
log.info "accepted #{conn.inspect} from #{addr.inspect}"
|
82
|
+
try_conn conn
|
83
|
+
rescue IO::WaitReadable
|
84
|
+
next
|
85
|
+
end
|
86
|
+
|
87
|
+
else
|
88
|
+
log.debug {"readable = #{readable}"}
|
89
|
+
begin
|
90
|
+
msgs = []
|
91
|
+
readable.read do |msg|
|
92
|
+
msgs << msg
|
93
|
+
end
|
94
|
+
rescue IOError, SystemCallError => ex
|
95
|
+
log.debug {"closing #{readable}: #{ex}"}
|
96
|
+
@streams.delete readable
|
97
|
+
readable.close unless readable.closed?
|
98
|
+
else
|
99
|
+
log.debug {
|
100
|
+
"read #{msgs.size} messages from #{readable.peer_name}"}
|
101
|
+
end
|
102
|
+
|
103
|
+
msgs.each do |msg|
|
104
|
+
handle_message msg
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
rescue => ex
|
110
|
+
log.error ex
|
111
|
+
raise
|
112
|
+
end
|
113
|
+
|
114
|
+
def handle_message msg
|
115
|
+
log.debug {"handling message #{msg.inspect}"}
|
116
|
+
@tick += 1
|
117
|
+
msg.global_tick = tick
|
118
|
+
msg.delta = nil
|
119
|
+
@streams.keep_if do |stream|
|
120
|
+
write_succeeds? msg, stream
|
121
|
+
end
|
122
|
+
end
|
123
|
+
private :handle_message
|
124
|
+
|
125
|
+
def write_succeeds? data, stream
|
126
|
+
stream << data
|
127
|
+
true
|
128
|
+
rescue IOError, SystemCallError => ex
|
129
|
+
log.debug {"closing #{stream}: #{ex}"}
|
130
|
+
stream.close unless stream.closed?
|
131
|
+
false
|
132
|
+
end
|
133
|
+
private :write_succeeds?
|
134
|
+
end
|
135
|
+
end
|
data/lib/funl/message.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
module Funl
|
2
|
+
class Message
|
3
|
+
# Unique (per funl instance) sequential id of client who sent message.
|
4
|
+
attr_accessor :client_id
|
5
|
+
|
6
|
+
# Client's sequential id of message, unique only in client scope.
|
7
|
+
attr_accessor :local_tick
|
8
|
+
|
9
|
+
# Global sequential id of message, unique in scope of funl instance.
|
10
|
+
# In client request to funl, this means last ack-ed global tick.
|
11
|
+
# Assummed to be 64 bits, to avoid rollover errors.
|
12
|
+
attr_accessor :global_tick
|
13
|
+
|
14
|
+
# In client request, how far ahead of ack this message is. When
|
15
|
+
# messages are pipelined, delta > 1.
|
16
|
+
attr_accessor :delta
|
17
|
+
|
18
|
+
# Application-defined metadata. May be used for filtering etc.
|
19
|
+
attr_accessor :tags
|
20
|
+
|
21
|
+
# Application-defined payload data. See blobber.rb.
|
22
|
+
attr_accessor :blob
|
23
|
+
|
24
|
+
def initialize(*args)
|
25
|
+
@client_id, @local_tick, @global_tick, @delta, @tags, @blob = *args
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.[](
|
29
|
+
client: nil, local: nil, global: nil, delta: nil, tags: nil, blob: nil)
|
30
|
+
new client, local, global, delta, tags, blob
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
d = delta ? "+#{delta}" : nil
|
35
|
+
t = tags ? " #{tags}" : nil
|
36
|
+
s = [
|
37
|
+
"client #{client_id}",
|
38
|
+
"local #{local_tick}",
|
39
|
+
"global #{global_tick}#{d}"
|
40
|
+
].join(", ")
|
41
|
+
"<Message: #{s}#{t}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_a
|
45
|
+
[@client_id, @local_tick, @global_tick, @delta, @tags, @blob]
|
46
|
+
end
|
47
|
+
|
48
|
+
def == other
|
49
|
+
other.kind_of? Message and
|
50
|
+
@client_id = other.client_id and
|
51
|
+
@local_tick = other.local_tick and
|
52
|
+
@global_tick = other.global_tick and
|
53
|
+
@delta = other.delta and
|
54
|
+
@tags = other.tags and
|
55
|
+
@blob = other.blob
|
56
|
+
end
|
57
|
+
alias eql? ==
|
58
|
+
|
59
|
+
def hash
|
60
|
+
@client_id.hash ^ @local_tick.hash ^ @global_tick.hash
|
61
|
+
end
|
62
|
+
|
63
|
+
# Call with Packer, nil, or IO. If +pk+ is nil, returns string. If +pk+ is
|
64
|
+
# a Packer, returns the Packer, which will need to be flushed. If +pk+ is
|
65
|
+
# IO, returns nil.
|
66
|
+
def to_msgpack(pk = nil)
|
67
|
+
case pk
|
68
|
+
when MessagePack::Packer
|
69
|
+
pk.write_array_header(6)
|
70
|
+
pk.write @client_id
|
71
|
+
pk.write @local_tick
|
72
|
+
pk.write @global_tick
|
73
|
+
pk.write @delta
|
74
|
+
pk.write @tags
|
75
|
+
pk.write @blob
|
76
|
+
return pk
|
77
|
+
|
78
|
+
else # nil or IO
|
79
|
+
MessagePack.pack(self, pk)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_json
|
84
|
+
to_a.to_json
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.from_serialized ary
|
88
|
+
new *ary
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.from_msgpack(src)
|
92
|
+
case src
|
93
|
+
when MessagePack::Unpacker
|
94
|
+
new(*src.read)
|
95
|
+
|
96
|
+
when IO, StringIO
|
97
|
+
from_msgpack(MessagePack::Unpacker.new(src))
|
98
|
+
|
99
|
+
else # String
|
100
|
+
from_msgpack(MessagePack::Unpacker.new.feed(src))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/funl/stream.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'object-stream-wrapper'
|
2
|
+
|
3
|
+
module Funl
|
4
|
+
# Mixin depends on stream_type, log, client_id, message_class.
|
5
|
+
module Stream
|
6
|
+
def client_stream_for io, type: stream_type
|
7
|
+
ObjectStreamWrapper.new(io, type: type).tap do |stream|
|
8
|
+
stream.write_to_outbox {{"client_id" => client_id}}
|
9
|
+
# client_id will be nil in the case of cseq, but that's ok.
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def server_stream_for io, type: stream_type
|
14
|
+
ObjectStreamWrapper.new(io, type: type).tap do |stream|
|
15
|
+
stream.consume do |h|
|
16
|
+
client_id = h["client_id"]
|
17
|
+
stream.peer_name = "client #{client_id}"
|
18
|
+
log.info "peer is #{stream.peer_name}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def message_server_stream_for io, type: stream_type
|
24
|
+
ObjectStreamWrapper.new(io, type: type).tap do |stream|
|
25
|
+
stream.consume do |h|
|
26
|
+
client_id = h["client_id"]
|
27
|
+
stream.peer_name = "client #{client_id}"
|
28
|
+
log.info "peer is #{stream.peer_name}"
|
29
|
+
stream.expect message_class
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'funl/client-sequencer'
|
2
|
+
require 'socket'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
require 'minitest/autorun'
|
6
|
+
|
7
|
+
class TestClientSequencer < Minitest::Test
|
8
|
+
attr_reader :log
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@dir = Dir.mktmpdir "funl-test-cseq-"
|
12
|
+
@path = File.join(@dir, "sock")
|
13
|
+
@log = Logger.new($stderr)
|
14
|
+
log.level = Logger::WARN
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
FileUtils.remove_entry @dir
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_initial_conns
|
22
|
+
as = []; bs = []
|
23
|
+
3.times {a, b = UNIXSocket.pair; as << a; bs << b}
|
24
|
+
cseq = Funl::ClientSequencer.new nil, *as, log: log
|
25
|
+
bs.each_with_index do |b, i|
|
26
|
+
stream = ObjectStream.new(b, type: cseq.stream_type)
|
27
|
+
client_id = stream.read["client_id"]
|
28
|
+
assert_equal i, client_id
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_later_conns
|
33
|
+
svr = UNIXServer.new(@path)
|
34
|
+
cseq = Funl::ClientSequencer.new svr, log: log
|
35
|
+
cseq.start
|
36
|
+
3.times do |i|
|
37
|
+
conn = UNIXSocket.new(@path)
|
38
|
+
stream = ObjectStream.new(conn, type: cseq.stream_type)
|
39
|
+
client_id = stream.read["client_id"]
|
40
|
+
assert_equal i, client_id
|
41
|
+
end
|
42
|
+
ensure
|
43
|
+
cseq.stop rescue nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_persist
|
47
|
+
saved_next_id = 0
|
48
|
+
3.times do |i|
|
49
|
+
assert_equal i, saved_next_id
|
50
|
+
|
51
|
+
path = "#{@path}-#{i}"
|
52
|
+
svr = UNIXServer.new(path)
|
53
|
+
cseq = Funl::ClientSequencer.new svr, log: log,
|
54
|
+
next_id: saved_next_id
|
55
|
+
cseq.start
|
56
|
+
|
57
|
+
conn = UNIXSocket.new(path)
|
58
|
+
stream = ObjectStream.new(conn, type: cseq.stream_type)
|
59
|
+
client_id = stream.read["client_id"]
|
60
|
+
assert_equal i, client_id
|
61
|
+
|
62
|
+
cseq.stop
|
63
|
+
cseq.wait
|
64
|
+
saved_next_id = cseq.next_id
|
65
|
+
end
|
66
|
+
ensure
|
67
|
+
cseq.stop rescue nil
|
68
|
+
end
|
69
|
+
end
|
data/test/test-client.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'funl/client'
|
2
|
+
require 'funl/blobber'
|
3
|
+
require 'funl/message-sequencer'
|
4
|
+
require 'funl/client-sequencer'
|
5
|
+
require 'socket'
|
6
|
+
require 'tmpdir'
|
7
|
+
|
8
|
+
include Funl
|
9
|
+
|
10
|
+
require 'minitest/autorun'
|
11
|
+
|
12
|
+
class TestClient < Minitest::Test
|
13
|
+
attr_reader :log
|
14
|
+
|
15
|
+
def setup
|
16
|
+
@dir = Dir.mktmpdir "funl-test-client-"
|
17
|
+
@cseq_path = File.join(@dir, "cseq")
|
18
|
+
@seq_path = File.join(@dir, "seq")
|
19
|
+
@log = Logger.new($stderr)
|
20
|
+
log.level = Logger::WARN
|
21
|
+
end
|
22
|
+
|
23
|
+
def teardown
|
24
|
+
FileUtils.remove_entry @dir
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_client
|
28
|
+
cseq_sock = UNIXServer.new(@cseq_path)
|
29
|
+
cseq = ClientSequencer.new cseq_sock, log: log
|
30
|
+
cseq.start
|
31
|
+
|
32
|
+
seq_sock = UNIXServer.new(@seq_path)
|
33
|
+
seq = MessageSequencer.new seq_sock, log: log
|
34
|
+
seq.start
|
35
|
+
|
36
|
+
client = Client.new(
|
37
|
+
seq: UNIXSocket.new(@seq_path),
|
38
|
+
cseq: UNIXSocket.new(@cseq_path),
|
39
|
+
log: log)
|
40
|
+
|
41
|
+
client.start
|
42
|
+
|
43
|
+
assert_equal(0, client.client_id)
|
44
|
+
assert_equal(0, client.start_tick)
|
45
|
+
assert_equal(Funl::Blobber::MSGPACK_TYPE, client.blob_type)
|
46
|
+
ensure
|
47
|
+
cseq.stop rescue nil
|
48
|
+
seq.stop rescue nil
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'funl/message-sequencer'
|
2
|
+
require 'socket'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
include Funl
|
6
|
+
|
7
|
+
require 'minitest/autorun'
|
8
|
+
|
9
|
+
class TestMessageSequencer < Minitest::Test
|
10
|
+
attr_reader :log
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@dir = Dir.mktmpdir "funl-test-mseq-"
|
14
|
+
@path = File.join(@dir, "sock")
|
15
|
+
@log = Logger.new($stderr)
|
16
|
+
log.level = Logger::WARN
|
17
|
+
@n_clients = 3
|
18
|
+
end
|
19
|
+
|
20
|
+
def teardown
|
21
|
+
FileUtils.remove_entry @dir
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_initial_conns
|
25
|
+
as = []; bs = []
|
26
|
+
@n_clients.times {a, b = UNIXSocket.pair; as << a; bs << b}
|
27
|
+
mseq = MessageSequencer.new nil, *as, log: log
|
28
|
+
bs.each_with_index do |b, i|
|
29
|
+
stream = ObjectStreamWrapper.new(b, type: mseq.stream_type)
|
30
|
+
stream.write_to_outbox({"client_id" => "test_initial_conns #{i}"})
|
31
|
+
global_tick = stream.read["tick"]
|
32
|
+
assert_equal 0, global_tick
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_later_conns
|
37
|
+
stream_type = ObjectStream::MSGPACK_TYPE
|
38
|
+
svr = UNIXServer.new(@path)
|
39
|
+
pid = fork do
|
40
|
+
log.progname = "mseq"
|
41
|
+
mseq = MessageSequencer.new svr, log: log, stream_type: stream_type
|
42
|
+
mseq.start
|
43
|
+
sleep
|
44
|
+
end
|
45
|
+
|
46
|
+
log.progname = "client"
|
47
|
+
streams = (0...@n_clients).map do
|
48
|
+
conn = UNIXSocket.new(@path)
|
49
|
+
stream = ObjectStreamWrapper.new(conn, type: stream_type)
|
50
|
+
stream.write_to_outbox{{"client_id" => "test_later_conns"}}
|
51
|
+
global_tick = stream.read["tick"]
|
52
|
+
assert_equal 0, global_tick
|
53
|
+
stream
|
54
|
+
end
|
55
|
+
|
56
|
+
m1 = Message[
|
57
|
+
client: 0, local: 12, global: 34,
|
58
|
+
delta: 1, tags: ["foo"], blob: "BLOB"]
|
59
|
+
send_msg(src: streams[0], message: m1, dst: streams, expected_tick: 1)
|
60
|
+
|
61
|
+
if @n_clients > 1
|
62
|
+
m2 = Message[
|
63
|
+
client: 1, local: 23, global: 45,
|
64
|
+
delta: 4, tags: ["bar"], blob: "BLOB"]
|
65
|
+
send_msg(src: streams[1], message: m2, dst: streams, expected_tick: 2)
|
66
|
+
end
|
67
|
+
|
68
|
+
ensure
|
69
|
+
Process.kill "TERM", pid if pid
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_msg(src: nil, message: nil, dst: nil, expected_tick: nil)
|
73
|
+
src << message
|
74
|
+
|
75
|
+
replies = dst.map do |stream|
|
76
|
+
stream.expect Message
|
77
|
+
stream.read
|
78
|
+
end
|
79
|
+
|
80
|
+
assert_equal @n_clients, replies.size
|
81
|
+
|
82
|
+
replies.each do |r|
|
83
|
+
assert_equal(message.client_id, r.client_id)
|
84
|
+
assert_equal(message.local_tick, r.local_tick)
|
85
|
+
assert_equal(expected_tick, r.global_tick)
|
86
|
+
assert_equal(nil, r.delta)
|
87
|
+
assert_equal(message.tags, r.tags)
|
88
|
+
assert_equal(message.blob, r.blob)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_persist
|
93
|
+
saved_tick = 0
|
94
|
+
n_write = 0
|
95
|
+
3.times do |i|
|
96
|
+
assert_equal n_write, saved_tick
|
97
|
+
|
98
|
+
path = "#{@path}-#{i}"
|
99
|
+
svr = UNIXServer.new(path)
|
100
|
+
mseq = Funl::MessageSequencer.new svr, log: log,
|
101
|
+
tick: saved_tick
|
102
|
+
mseq.start
|
103
|
+
|
104
|
+
conn = UNIXSocket.new(path)
|
105
|
+
stream = ObjectStreamWrapper.new(conn, type: mseq.stream_type)
|
106
|
+
stream.write_to_outbox{{"client_id" => "test_persist"}} # not needed
|
107
|
+
tick = stream.read["tick"]
|
108
|
+
assert_equal n_write, tick
|
109
|
+
|
110
|
+
stream.write Message.new
|
111
|
+
stream.read
|
112
|
+
n_write += 1
|
113
|
+
|
114
|
+
mseq.stop
|
115
|
+
mseq.wait
|
116
|
+
saved_tick = mseq.tick
|
117
|
+
end
|
118
|
+
|
119
|
+
ensure
|
120
|
+
mseq.stop rescue nil
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'funl/message'
|
2
|
+
require 'socket'
|
3
|
+
require 'stringio'
|
4
|
+
require 'msgpack'
|
5
|
+
|
6
|
+
require 'minitest/autorun'
|
7
|
+
|
8
|
+
class TestMessage < Minitest::Test
|
9
|
+
attr_reader :m, :sio
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@m = Funl::Message.new
|
13
|
+
m.client_id = 12
|
14
|
+
m.local_tick = 34
|
15
|
+
m.global_tick = 56
|
16
|
+
m.delta = 78
|
17
|
+
m.tags = [ "aaa", "bbb" ]
|
18
|
+
m.blob = "BLOB"
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_from_serialized
|
22
|
+
a = m.to_a
|
23
|
+
m2 = Funl::Message.from_serialized a
|
24
|
+
assert_equal(m, m2)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_to_msgpack
|
28
|
+
assert_kind_of(String, m.to_msgpack(nil))
|
29
|
+
assert_kind_of(String, m.to_msgpack)
|
30
|
+
assert_equal(MessagePack.pack(m), m.to_msgpack(nil))
|
31
|
+
assert_equal(MessagePack.pack(m), m.to_msgpack)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_to_msgpack_packer
|
35
|
+
pk = MessagePack::Packer.new
|
36
|
+
assert_kind_of(MessagePack::Packer, m.to_msgpack(pk))
|
37
|
+
assert_equal(pk, m.to_msgpack(pk))
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_to_msgpack_io
|
41
|
+
src, dst = UNIXSocket.pair
|
42
|
+
assert_nil(m.to_msgpack(src))
|
43
|
+
src.close
|
44
|
+
assert_equal(MessagePack.pack(m), dst.read)
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_pack_io
|
48
|
+
src, dst = UNIXSocket.pair
|
49
|
+
MessagePack.pack(m, src)
|
50
|
+
src.close
|
51
|
+
assert_equal(MessagePack.pack(m), dst.read)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_pack_unpack
|
55
|
+
assert_equal(m.to_a, MessagePack.unpack(MessagePack.pack(m)))
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_from_msgpack_str
|
59
|
+
assert_equal(m, Funl::Message.from_msgpack(MessagePack.pack(m)))
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_from_msgpack_stringio
|
63
|
+
sio = StringIO.new
|
64
|
+
MessagePack.pack(m, sio)
|
65
|
+
sio.rewind
|
66
|
+
assert_equal(m, Funl::Message.from_msgpack(sio))
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_from_msgpack_io
|
70
|
+
src, dst = UNIXSocket.pair
|
71
|
+
MessagePack.pack(m, src)
|
72
|
+
src.close
|
73
|
+
assert_equal(m, Funl::Message.from_msgpack(dst))
|
74
|
+
end
|
75
|
+
end
|
data/test/test-stream.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'funl/stream'
|
2
|
+
require 'funl/message'
|
3
|
+
require 'stringio'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
require 'minitest/autorun'
|
7
|
+
|
8
|
+
class TestStream < Minitest::Test
|
9
|
+
include Funl::Stream
|
10
|
+
|
11
|
+
attr_reader :client_id
|
12
|
+
attr_reader :stream_type
|
13
|
+
attr_reader :message_class
|
14
|
+
attr_reader :log
|
15
|
+
|
16
|
+
def setup
|
17
|
+
# for the Stream mixin:
|
18
|
+
@client_id = 42
|
19
|
+
@stream_type = ObjectStream::MSGPACK_TYPE
|
20
|
+
@message_class = Funl::Message
|
21
|
+
@log = Logger.new($stderr)
|
22
|
+
log.level = Logger::WARN
|
23
|
+
|
24
|
+
@sio = StringIO.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_greeting
|
28
|
+
client = client_stream_for @sio
|
29
|
+
client.write "message"
|
30
|
+
|
31
|
+
@sio.rewind
|
32
|
+
|
33
|
+
server = server_stream_for @sio
|
34
|
+
m = server.read
|
35
|
+
|
36
|
+
assert_equal "message", m
|
37
|
+
assert_equal "client #{client_id}", server.peer_name
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_message_server_stream
|
41
|
+
client = client_stream_for @sio
|
42
|
+
client.write Funl::Message[client: client_id]
|
43
|
+
|
44
|
+
@sio.rewind
|
45
|
+
|
46
|
+
server = message_server_stream_for @sio
|
47
|
+
m = server.read
|
48
|
+
|
49
|
+
assert_kind_of Funl::Message, m
|
50
|
+
assert_equal client_id, m.client_id
|
51
|
+
assert_equal "client #{client_id}", server.peer_name
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: funl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joel VanderWerf
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-07-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: object-stream
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Sequences messages.
|
28
|
+
email: vjoel@users.sourceforge.net
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.md
|
33
|
+
- COPYING
|
34
|
+
files:
|
35
|
+
- README.md
|
36
|
+
- COPYING
|
37
|
+
- Rakefile
|
38
|
+
- lib/funl/stream.rb
|
39
|
+
- lib/funl/blobber.rb
|
40
|
+
- lib/funl/history-client.rb
|
41
|
+
- lib/funl/history-worker.rb
|
42
|
+
- lib/funl/client.rb
|
43
|
+
- lib/funl/message-sequencer.rb
|
44
|
+
- lib/funl/client-sequencer.rb
|
45
|
+
- lib/funl/message.rb
|
46
|
+
- test/test-message-sequencer.rb
|
47
|
+
- test/test-client-sequencer.rb
|
48
|
+
- test/test-message.rb
|
49
|
+
- test/test-stream.rb
|
50
|
+
- test/test-client.rb
|
51
|
+
homepage: https://github.com/vjoel/funl
|
52
|
+
licenses:
|
53
|
+
- BSD
|
54
|
+
metadata: {}
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options:
|
57
|
+
- --quiet
|
58
|
+
- --line-numbers
|
59
|
+
- --inline-source
|
60
|
+
- --title
|
61
|
+
- funl
|
62
|
+
- --main
|
63
|
+
- README.md
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements: []
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 2.0.4
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Sequences messages
|
82
|
+
test_files:
|
83
|
+
- test/test-message-sequencer.rb
|
84
|
+
- test/test-client-sequencer.rb
|
85
|
+
- test/test-message.rb
|
86
|
+
- test/test-stream.rb
|
87
|
+
- test/test-client.rb
|
88
|
+
has_rdoc:
|