funl 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/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:
|