estore 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/estore.rb +2 -3
- data/lib/estore/commands.rb +8 -0
- data/lib/estore/commands/append.rb +56 -0
- data/lib/estore/commands/base.rb +51 -0
- data/lib/estore/commands/catch_up_subscription.rb +60 -0
- data/lib/estore/commands/ping.rb +16 -0
- data/lib/estore/commands/promise.rb +19 -0
- data/lib/estore/commands/read_batch.rb +25 -0
- data/lib/estore/commands/read_forward.rb +39 -0
- data/lib/estore/commands/subscription.rb +53 -0
- data/lib/estore/connection.rb +19 -55
- data/lib/estore/connection/buffer.rb +11 -21
- data/lib/estore/connection/{commands.rb → protocol.rb} +1 -1
- data/lib/estore/connection_context.rb +12 -76
- data/lib/estore/errors.rb +3 -3
- data/lib/estore/message_extensions.rb +1 -1
- data/lib/estore/messages.rb +1 -1
- data/lib/estore/package.rb +1 -1
- data/lib/estore/session.rb +22 -48
- data/lib/estore/version.rb +2 -2
- data/spec/integration/session_spec.rb +44 -30
- metadata +12 -5
- data/lib/estore/catchup_subscription.rb +0 -87
- data/lib/estore/subscription.rb +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1eaa9c5801e991ca2643e6412176c10144e2dcb7
|
4
|
+
data.tar.gz: 61800929c52882a40cb9fd234a0238902d7e66b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5231c8eb6a0e0713b71b2f80c9640892202cee0aaed8f224b975dad02739b9c3f90c2feec3bdc33302a67e80fd895e3ec11b03f0cc546f4eab43359231fd278
|
7
|
+
data.tar.gz: 9da5606ca43d26bcac0ce1e860ae290b162de59d49691107a7fd92dd83ab643ffd1a726ff8cc1369b8c94633b1ddf4ba516153e36579add0b5c66ce6dd9d9cfa
|
data/lib/estore.rb
CHANGED
@@ -6,6 +6,5 @@ require 'estore/message_extensions'
|
|
6
6
|
require 'estore/connection_context'
|
7
7
|
require 'estore/connection'
|
8
8
|
require 'estore/connection/buffer'
|
9
|
-
require 'estore/connection/
|
10
|
-
require 'estore/
|
11
|
-
require 'estore/catchup_subscription'
|
9
|
+
require 'estore/connection/protocol'
|
10
|
+
require 'estore/commands'
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'estore/commands/promise'
|
2
|
+
require 'estore/commands/base'
|
3
|
+
require 'estore/commands/append'
|
4
|
+
require 'estore/commands/ping'
|
5
|
+
require 'estore/commands/read_batch'
|
6
|
+
require 'estore/commands/read_forward'
|
7
|
+
require 'estore/commands/subscription'
|
8
|
+
require 'estore/commands/catch_up_subscription'
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Estore
|
2
|
+
module Commands
|
3
|
+
class Append
|
4
|
+
include Command
|
5
|
+
|
6
|
+
CONTENT_TYPES = {
|
7
|
+
json: 1
|
8
|
+
}
|
9
|
+
|
10
|
+
def initialize(connection, stream, events, options = {})
|
11
|
+
super(connection)
|
12
|
+
@stream, @events, @options = stream, events, options
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
msg = WriteEvents.new(
|
17
|
+
event_stream_id: @stream,
|
18
|
+
expected_version: @options[:expected_version] || -2,
|
19
|
+
events: Array(@events).map { |event| new_event(event) },
|
20
|
+
require_master: true
|
21
|
+
)
|
22
|
+
|
23
|
+
register!
|
24
|
+
write('WriteEvents', msg)
|
25
|
+
promise
|
26
|
+
end
|
27
|
+
|
28
|
+
def handle(message, *)
|
29
|
+
response = decode(WriteEventsCompleted, message)
|
30
|
+
|
31
|
+
if response.result != OperationResult::Success
|
32
|
+
# TODO: Create custom exceptions
|
33
|
+
raise "WriteEvents command failed with uuid #{@uuid}"
|
34
|
+
end
|
35
|
+
|
36
|
+
remove!
|
37
|
+
promise.fulfill(response)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def new_event(event)
|
43
|
+
uuid = event[:id] || SecureRandom.uuid
|
44
|
+
content_type = event.fetch(:content_type, :json)
|
45
|
+
|
46
|
+
NewEvent.new(
|
47
|
+
event_id: Package.encode_uuid(uuid),
|
48
|
+
event_type: event[:type],
|
49
|
+
data: event[:data],
|
50
|
+
data_content_type: CONTENT_TYPES.fetch(content_type, 0),
|
51
|
+
metadata_content_type: 1
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Estore
|
2
|
+
module Commands
|
3
|
+
module Command
|
4
|
+
attr_reader :uuid
|
5
|
+
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
@uuid = SecureRandom.uuid
|
9
|
+
end
|
10
|
+
|
11
|
+
def register!
|
12
|
+
@connection.register(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def remove!
|
16
|
+
@connection.remove(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def write(command, message = nil)
|
20
|
+
@connection.write(@uuid, command, message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def promise
|
24
|
+
@promise ||= Promise.new(@uuid)
|
25
|
+
end
|
26
|
+
|
27
|
+
def decode(type, message)
|
28
|
+
type.decode(message)
|
29
|
+
rescue => error
|
30
|
+
puts "Protobuf decoding error on connection #{object_id}"
|
31
|
+
puts type: type, message: message
|
32
|
+
puts error.backtrace
|
33
|
+
raise error
|
34
|
+
end
|
35
|
+
|
36
|
+
module ReadStreamForward
|
37
|
+
def read(stream, from, limit)
|
38
|
+
msg = ReadStreamEvents.new(
|
39
|
+
event_stream_id: stream,
|
40
|
+
from_event_number: from,
|
41
|
+
max_count: limit,
|
42
|
+
resolve_link_tos: true,
|
43
|
+
require_master: false
|
44
|
+
)
|
45
|
+
|
46
|
+
write('ReadStreamEventsForward', msg)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Estore
|
2
|
+
module Commands
|
3
|
+
class CatchUpSubscription < Subscription
|
4
|
+
include Command
|
5
|
+
|
6
|
+
def initialize(connection, stream, from, options = {})
|
7
|
+
super(connection, options)
|
8
|
+
@stream = stream
|
9
|
+
@from = from
|
10
|
+
@batch = options[:batch_size]
|
11
|
+
@mutex = Mutex.new
|
12
|
+
@queue = []
|
13
|
+
@caught_up = false
|
14
|
+
@last_worker = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
super
|
19
|
+
|
20
|
+
# TODO: Think about doing something more clever?
|
21
|
+
read = ReadForward.new(@connection, @stream, @from, @batch) do |events|
|
22
|
+
@last_worker = Thread.new(@last_worker) do |last_worker|
|
23
|
+
last_worker.join if last_worker
|
24
|
+
events.each { |event| dispatch(event) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
read.call.sync
|
29
|
+
@last_worker.join if @last_worker
|
30
|
+
|
31
|
+
switch_to_live
|
32
|
+
end
|
33
|
+
|
34
|
+
def switch_to_live
|
35
|
+
@mutex.synchronize do
|
36
|
+
queued_events.each { |event| dispatch(event) }
|
37
|
+
@caught_up = true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def queued_events
|
42
|
+
@queue.find_all { |event| event.original_event_number > @position }
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle(message, type)
|
46
|
+
if type == 'StreamEventAppeared'
|
47
|
+
event = decode(StreamEventAppeared, message).event
|
48
|
+
|
49
|
+
unless @caught_up
|
50
|
+
@mutex.synchronize do
|
51
|
+
@queue << event unless @caught_up
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
dispatch(event) if @caught_up
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Estore
|
2
|
+
module Commands
|
3
|
+
class Promise < ::Promise
|
4
|
+
attr_reader :correlation_id
|
5
|
+
|
6
|
+
def initialize(correlation_id)
|
7
|
+
super()
|
8
|
+
@correlation_id = correlation_id
|
9
|
+
end
|
10
|
+
|
11
|
+
def wait
|
12
|
+
t = Thread.current
|
13
|
+
resume = proc { t.wakeup }
|
14
|
+
self.then(resume, resume)
|
15
|
+
sleep
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Estore
|
2
|
+
module Commands
|
3
|
+
class ReadBatch
|
4
|
+
include Command
|
5
|
+
include Command::ReadStreamForward
|
6
|
+
|
7
|
+
def initialize(connection, stream, from, limit)
|
8
|
+
super(connection)
|
9
|
+
@stream, @from, @limit = stream, from, limit
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
register!
|
14
|
+
read(@stream, @from, @limit)
|
15
|
+
promise
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle(message, *)
|
19
|
+
remove!
|
20
|
+
response = decode(ReadStreamEventsCompleted, message)
|
21
|
+
promise.fulfill(Array(response.events))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Estore
|
2
|
+
module Commands
|
3
|
+
class ReadForward
|
4
|
+
include Command
|
5
|
+
include Command::ReadStreamForward
|
6
|
+
|
7
|
+
def initialize(connection, stream, from, batch_size = nil, &block)
|
8
|
+
super(connection)
|
9
|
+
|
10
|
+
@stream = stream
|
11
|
+
@from = from
|
12
|
+
@batch_size = batch_size || 1000
|
13
|
+
@block = block
|
14
|
+
@events = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
register!
|
19
|
+
read(@stream, @from, @batch_size)
|
20
|
+
promise
|
21
|
+
end
|
22
|
+
|
23
|
+
def handle(message, *)
|
24
|
+
response = decode(ReadStreamEventsCompleted, message)
|
25
|
+
events = Array(response.events)
|
26
|
+
|
27
|
+
@from += events.size
|
28
|
+
read(@stream, @from, @batch_size) unless response.is_end_of_stream
|
29
|
+
|
30
|
+
@block ? @block.call(events) : @events.push(*events)
|
31
|
+
|
32
|
+
if response.is_end_of_stream
|
33
|
+
remove!
|
34
|
+
promise.fulfill(@block ? nil : @events)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Estore
|
2
|
+
module Commands
|
3
|
+
class Subscription
|
4
|
+
include Command
|
5
|
+
|
6
|
+
def initialize(connection, stream, options = {})
|
7
|
+
super(connection)
|
8
|
+
@has_finished = false
|
9
|
+
@stream = stream
|
10
|
+
@resolve_link_tos = options.fetch(:resolve_link_tos, true)
|
11
|
+
end
|
12
|
+
|
13
|
+
def finished?
|
14
|
+
@has_finished
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
start
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
raise 'Subscription block not defined' unless @handler
|
23
|
+
|
24
|
+
msg = SubscribeToStream.new(
|
25
|
+
event_stream_id: @stream,
|
26
|
+
resolve_link_tos: @resolve_link_tos
|
27
|
+
)
|
28
|
+
|
29
|
+
register!
|
30
|
+
write('SubscribeToStream', msg)
|
31
|
+
end
|
32
|
+
|
33
|
+
def close
|
34
|
+
write('UnsubscribeFromStream', UnsubscribeFromStream.new)
|
35
|
+
remove!
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_event(&block)
|
39
|
+
@handler = block
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle(message, type)
|
43
|
+
dispatch(decode(StreamEventAppeared, message).event) if
|
44
|
+
type == 'StreamEventAppeared'
|
45
|
+
end
|
46
|
+
|
47
|
+
def dispatch(event)
|
48
|
+
@position = event.original_event_number
|
49
|
+
@handler.call(event)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/estore/connection.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
1
|
+
module Estore
|
2
2
|
# Connection owns the TCP socket, formats and sends commands over the socket.
|
3
3
|
# It also starts a background thread to read from the TCP socket and handle
|
4
4
|
# received packages, dispatching them to the calling app.
|
5
5
|
class Connection
|
6
|
-
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
delegate [:register, :remove] => :@context
|
7
9
|
|
8
10
|
def initialize(host, port, context)
|
9
11
|
@host = host
|
@@ -18,65 +20,25 @@ class Estore
|
|
18
20
|
socket.close
|
19
21
|
end
|
20
22
|
|
21
|
-
def
|
22
|
-
code = COMMANDS.fetch(command)
|
23
|
+
def write(uuid, command, msg = nil)
|
23
24
|
msg.validate! if msg
|
24
25
|
|
25
|
-
|
26
|
-
frame = Package.encode(code,
|
26
|
+
code = COMMANDS.fetch(command)
|
27
|
+
frame = Package.encode(code, uuid, msg)
|
27
28
|
|
28
|
-
mutex.synchronize do
|
29
|
-
promise = context.register_command(correlation_id, command, handler)
|
29
|
+
@mutex.synchronize do
|
30
30
|
socket.write(frame.to_s)
|
31
|
-
promise
|
32
31
|
end
|
33
32
|
end
|
34
33
|
|
35
34
|
private
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
case command
|
41
|
-
when 'Pong'
|
42
|
-
context.fulfill(uuid, 'Pong')
|
43
|
-
when 'HeartbeatRequestCommand'
|
44
|
-
send_command('HeartbeatResponseCommand')
|
45
|
-
when 'SubscriptionConfirmation'
|
46
|
-
context.fulfill(uuid, decode(SubscriptionConfirmation, message))
|
47
|
-
when 'ReadStreamEventsForwardCompleted'
|
48
|
-
context.fulfill(uuid, decode(ReadStreamEventsCompleted, message))
|
49
|
-
when 'StreamEventAppeared'
|
50
|
-
resolved_event = decode(StreamEventAppeared, message).event
|
51
|
-
context.trigger(uuid, :event_appeared, resolved_event)
|
52
|
-
when 'WriteEventsCompleted'
|
53
|
-
on_write_events_completed(uuid, decode(WriteEventsCompleted, message))
|
36
|
+
def on_received_package(message, type, uuid, _flags)
|
37
|
+
if type == 'HeartbeatRequestCommand'
|
38
|
+
write(SecureRandom.uuid, 'HeartbeatResponseCommand')
|
54
39
|
else
|
55
|
-
|
56
|
-
end
|
57
|
-
end
|
58
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize
|
59
|
-
# rubocop:enable Metrics/MethodLength
|
60
|
-
|
61
|
-
def on_write_events_completed(uuid, response)
|
62
|
-
if response.result != OperationResult::Success
|
63
|
-
p fn: 'on_write_events_completed', at: error, result: response.result
|
64
|
-
context.rejected_command(uuid, response)
|
65
|
-
return
|
40
|
+
@context.dispatch(uuid, message, type)
|
66
41
|
end
|
67
|
-
|
68
|
-
context.fulfill(uuid, response)
|
69
|
-
end
|
70
|
-
|
71
|
-
def decode(type, message)
|
72
|
-
type.decode(message)
|
73
|
-
rescue => error
|
74
|
-
puts "Protobuf decoding error on connection #{object_id}"
|
75
|
-
puts error.inspect
|
76
|
-
p type: type, message: message
|
77
|
-
puts "\n\n"
|
78
|
-
puts(*error.backtrace)
|
79
|
-
raise error
|
80
42
|
end
|
81
43
|
|
82
44
|
def socket
|
@@ -84,7 +46,7 @@ class Estore
|
|
84
46
|
end
|
85
47
|
|
86
48
|
def connect
|
87
|
-
@socket = TCPSocket.open(host, port)
|
49
|
+
@socket = TCPSocket.open(@host, @port)
|
88
50
|
Thread.new do
|
89
51
|
process_downstream
|
90
52
|
end
|
@@ -92,12 +54,12 @@ class Estore
|
|
92
54
|
rescue TimeoutError, Errno::ECONNREFUSED, Errno::EHOSTDOWN,
|
93
55
|
Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ETIMEDOUT
|
94
56
|
raise CannotConnectError, "Error connecting to Eventstore on "\
|
95
|
-
"#{host.inspect}:#{port.inspect} (#{$ERROR_INFO.class})"
|
57
|
+
"#{@host.inspect}:#{@port.inspect} (#{$ERROR_INFO.class})"
|
96
58
|
end
|
97
59
|
|
98
60
|
def process_downstream
|
99
61
|
loop do
|
100
|
-
buffer << socket.sysread(4096)
|
62
|
+
@buffer << socket.sysread(4096)
|
101
63
|
end
|
102
64
|
rescue IOError, EOFError
|
103
65
|
on_disconnect
|
@@ -112,8 +74,10 @@ class Estore
|
|
112
74
|
end
|
113
75
|
|
114
76
|
def on_exception(error)
|
115
|
-
puts "process_downstream_error
|
116
|
-
|
77
|
+
puts "process_downstream_error"
|
78
|
+
puts error.message
|
79
|
+
puts error.backtrace
|
80
|
+
@context.on_error(error)
|
117
81
|
end
|
118
82
|
end
|
119
83
|
end
|
@@ -1,10 +1,9 @@
|
|
1
|
-
|
1
|
+
module Estore
|
2
2
|
class Connection
|
3
3
|
# Buffer receives data from the TCP connection, and parses the binary
|
4
4
|
# packages.
|
5
5
|
# Parsed packages are given back to the given handler as they are decoded.
|
6
6
|
class Buffer
|
7
|
-
attr_reader :buffer, :handler, :mutex
|
8
7
|
def initialize(&block)
|
9
8
|
@mutex = Mutex.new
|
10
9
|
@buffer = ''.force_encoding('BINARY')
|
@@ -15,46 +14,37 @@ class Estore
|
|
15
14
|
bytes = bytes.force_encoding('BINARY') if
|
16
15
|
bytes.respond_to? :force_encoding
|
17
16
|
|
18
|
-
mutex.synchronize do
|
17
|
+
@mutex.synchronize do
|
19
18
|
@buffer << bytes
|
20
19
|
end
|
21
20
|
|
22
|
-
|
21
|
+
consume_packages
|
23
22
|
end
|
24
23
|
|
25
|
-
def
|
26
|
-
while
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def consume_package
|
31
|
-
pkg = read_package
|
32
|
-
if pkg
|
24
|
+
def consume_packages
|
25
|
+
while (pkg = read_package)
|
33
26
|
handle(pkg)
|
34
27
|
discard_bytes(pkg)
|
35
|
-
true
|
36
|
-
else
|
37
|
-
false
|
38
28
|
end
|
39
29
|
end
|
40
30
|
|
41
31
|
def read_package
|
42
|
-
return nil if buffer.length < 4
|
43
|
-
package_length = buffer[0...4].unpack('l<').first
|
44
|
-
bytes = buffer[4...(4 + package_length)].dup
|
32
|
+
return nil if @buffer.length < 4
|
33
|
+
package_length = @buffer[0...4].unpack('l<').first
|
34
|
+
bytes = @buffer[4...(4 + package_length)].dup
|
45
35
|
bytes if bytes.bytesize >= package_length
|
46
36
|
end
|
47
37
|
|
48
38
|
def discard_bytes(pkg)
|
49
|
-
mutex.synchronize do
|
50
|
-
@buffer = buffer[(4 + pkg.bytesize)..-1]
|
39
|
+
@mutex.synchronize do
|
40
|
+
@buffer = @buffer[(4 + pkg.bytesize)..-1]
|
51
41
|
end
|
52
42
|
end
|
53
43
|
|
54
44
|
def handle(pkg)
|
55
45
|
code, flags, uuid_bytes, message = parse(pkg)
|
56
46
|
command = Estore::Connection.command_name(code)
|
57
|
-
handler.call(
|
47
|
+
@handler.call(message, command, Package.parse_uuid(uuid_bytes), flags)
|
58
48
|
end
|
59
49
|
|
60
50
|
def parse(pkg)
|
@@ -1,92 +1,28 @@
|
|
1
1
|
require 'promise'
|
2
2
|
|
3
|
-
|
4
|
-
#
|
5
|
-
# that carries the correlation id of the command.
|
6
|
-
# @see https://github.com/lgierth/promise.rb
|
7
|
-
class Promise < ::Promise
|
8
|
-
attr_reader :correlation_id
|
9
|
-
|
10
|
-
def initialize(correlation_id)
|
11
|
-
super()
|
12
|
-
@correlation_id = correlation_id
|
13
|
-
end
|
14
|
-
|
15
|
-
def wait
|
16
|
-
t = Thread.current
|
17
|
-
resume = proc { t.wakeup }
|
18
|
-
self.then(resume, resume)
|
19
|
-
sleep
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# Registry storing handlers for the outstanding commands and
|
24
|
-
# current subscriptions
|
3
|
+
module Estore
|
4
|
+
# Registry storing handlers for the pending commands
|
25
5
|
class ConnectionContext
|
26
|
-
attr_reader :mutex, :requests, :targets
|
27
6
|
def initialize
|
28
7
|
@mutex = Mutex.new
|
29
|
-
@
|
30
|
-
@targets = {}
|
31
|
-
end
|
32
|
-
|
33
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
34
|
-
def register_command(uuid, command, target = nil)
|
35
|
-
case command
|
36
|
-
when 'Ping' then promise(uuid)
|
37
|
-
when 'ReadStreamEventsForward' then promise(uuid)
|
38
|
-
when 'SubscribeToStream' then promise(uuid, target)
|
39
|
-
when 'WriteEvents' then promise(uuid)
|
40
|
-
when 'HeartbeatResponseCommand' then :nothing_to_do
|
41
|
-
when 'UnsubscribeFromStream' then :nothing_to_do
|
42
|
-
else raise "Unknown command #{command}"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
46
|
-
|
47
|
-
def fulfill(uuid, value)
|
48
|
-
prom = nil
|
49
|
-
|
50
|
-
mutex.synchronize do
|
51
|
-
prom = requests.delete(uuid)
|
52
|
-
end
|
53
|
-
|
54
|
-
prom.fulfill(value) if prom
|
8
|
+
@commands = {}
|
55
9
|
end
|
56
10
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
mutex.synchronize do
|
61
|
-
prom = requests.delete(uuid)
|
11
|
+
def register(command)
|
12
|
+
@mutex.synchronize do
|
13
|
+
@commands[command.uuid] = command
|
62
14
|
end
|
63
|
-
|
64
|
-
prom.reject(error) if prom
|
65
|
-
end
|
66
|
-
|
67
|
-
def trigger(uuid, method, *args)
|
68
|
-
target = mutex.synchronize { targets[uuid] }
|
69
|
-
return if target.nil?
|
70
|
-
target.__send__(method, *args)
|
71
15
|
end
|
72
16
|
|
73
|
-
def
|
74
|
-
|
75
|
-
@
|
76
|
-
else
|
77
|
-
@error_handler.call(error) if @error_handler
|
17
|
+
def remove(command)
|
18
|
+
@mutex.synchronize do
|
19
|
+
@commands.delete(command.uuid)
|
78
20
|
end
|
79
21
|
end
|
80
22
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
prom = Promise.new(uuid)
|
85
|
-
mutex.synchronize do
|
86
|
-
requests[uuid] = prom
|
87
|
-
targets[uuid] = target
|
88
|
-
end
|
89
|
-
prom
|
23
|
+
def dispatch(uuid, message, type)
|
24
|
+
command = @commands[uuid]
|
25
|
+
command.handle(message, type) if command
|
90
26
|
end
|
91
27
|
end
|
92
28
|
end
|
data/lib/estore/errors.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module Estore
|
2
|
+
CannotConnectError = Class.new(RuntimeError)
|
3
|
+
DisconnectionError = Class.new(RuntimeError)
|
4
4
|
end
|
data/lib/estore/messages.rb
CHANGED
data/lib/estore/package.rb
CHANGED
data/lib/estore/session.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
|
3
|
-
|
3
|
+
module Estore
|
4
4
|
# The Session class is responsible for maintaining a full-duplex connection
|
5
5
|
# between the client and the Event Store server.
|
6
6
|
# An Estore session is thread-safe, and it is recommended to only have one
|
@@ -31,70 +31,44 @@ class Estore # TODO: Change to module
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def ping
|
34
|
-
command(
|
34
|
+
command(Commands::Ping)
|
35
35
|
end
|
36
36
|
|
37
|
-
def read(stream,
|
38
|
-
|
39
|
-
|
40
|
-
from_event_number: start,
|
41
|
-
max_count: limit,
|
42
|
-
resolve_link_tos: true,
|
43
|
-
require_master: false
|
44
|
-
)
|
37
|
+
def read(stream, options = {})
|
38
|
+
from = options[:from] || 0
|
39
|
+
limit = options[:limit]
|
45
40
|
|
46
|
-
|
41
|
+
if limit
|
42
|
+
read_batch(stream, from, limit)
|
43
|
+
else
|
44
|
+
read_forward(stream, from)
|
45
|
+
end
|
47
46
|
end
|
48
47
|
|
49
|
-
def
|
50
|
-
|
51
|
-
event_stream_id: stream,
|
52
|
-
expected_version: options[:expected_version] || -2,
|
53
|
-
events: Array(events).map { |event| new_event(event) },
|
54
|
-
require_master: true
|
55
|
-
)
|
56
|
-
|
57
|
-
command('WriteEvents', msg)
|
48
|
+
def read_batch(stream, from, limit)
|
49
|
+
command(Commands::ReadBatch, stream, from, limit).call
|
58
50
|
end
|
59
51
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
resolve_link_tos: options[:resolve_link_tos]
|
64
|
-
)
|
52
|
+
def read_forward(stream, from, batch_size = nil, &block)
|
53
|
+
command(Commands::ReadForward, stream, from, batch_size, &block).call
|
54
|
+
end
|
65
55
|
|
66
|
-
|
56
|
+
def append(stream, events, options = {})
|
57
|
+
command(Commands::Append, stream, events, options).call
|
67
58
|
end
|
68
59
|
|
69
60
|
def subscription(stream, options = {})
|
70
|
-
if options[:
|
71
|
-
CatchUpSubscription
|
61
|
+
if options[:from]
|
62
|
+
command(Commands::CatchUpSubscription, stream, options[:from], options)
|
72
63
|
else
|
73
|
-
Subscription
|
64
|
+
command(Commands::Subscription, stream, options)
|
74
65
|
end
|
75
66
|
end
|
76
67
|
|
77
68
|
private
|
78
69
|
|
79
|
-
|
80
|
-
|
81
|
-
}
|
82
|
-
|
83
|
-
def new_event(event)
|
84
|
-
uuid = event[:id] || SecureRandom.uuid
|
85
|
-
content_type = event.fetch(:content_type, :json)
|
86
|
-
|
87
|
-
NewEvent.new(
|
88
|
-
event_id: Package.encode_uuid(uuid),
|
89
|
-
event_type: event[:type],
|
90
|
-
data: event[:data],
|
91
|
-
data_content_type: CONTENT_TYPES.fetch(content_type, 0),
|
92
|
-
metadata_content_type: 1
|
93
|
-
)
|
94
|
-
end
|
95
|
-
|
96
|
-
def command(*args)
|
97
|
-
connection.send_command(*args)
|
70
|
+
def command(command, *args)
|
71
|
+
command.new(connection, *args)
|
98
72
|
end
|
99
73
|
end
|
100
74
|
end
|
data/lib/estore/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.0.
|
1
|
+
module Estore
|
2
|
+
VERSION = '0.0.3'
|
3
3
|
end
|
@@ -33,74 +33,88 @@ describe Estore::Session do
|
|
33
33
|
"test-#{SecureRandom.uuid}"
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
36
|
+
def stream_with(count, stream = nil)
|
37
37
|
stream ||= random_stream
|
38
38
|
session.append(stream, events(count)).sync
|
39
39
|
stream
|
40
40
|
end
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
RSpec::Matchers.define :start_from do |start|
|
43
|
+
match do |events|
|
44
|
+
events.each_with_index do |event, index|
|
45
|
+
expect(parse_data(event)).to eql('id' => index + start)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
45
49
|
|
46
|
-
|
50
|
+
it 'reads all the events from a stream' do
|
51
|
+
events = session.read(stream_with(200)).sync
|
47
52
|
|
48
|
-
|
49
|
-
|
50
|
-
|
53
|
+
expect(events.size).to be(200)
|
54
|
+
expect(events).to start_from(0)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'reads all the events forward from a stream' do
|
58
|
+
events = session.read(stream_with(100), from: 20).sync
|
59
|
+
|
60
|
+
expect(events.size).to be(80)
|
61
|
+
expect(events).to start_from(20)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'reads a batch of events from a stream' do
|
65
|
+
events = session.read(stream_with(30), from: 10, limit: 15).sync
|
66
|
+
|
67
|
+
expect(events.size).to be(15)
|
68
|
+
expect(events).to start_from(10)
|
51
69
|
end
|
52
70
|
|
53
71
|
it 'allows to make a live subscription' do
|
54
72
|
stream = random_stream
|
55
|
-
received =
|
73
|
+
received = []
|
56
74
|
|
57
|
-
|
58
|
-
sub.on_error { |error| raise error.inspect }
|
59
|
-
|
60
|
-
sub.on_event do |event|
|
61
|
-
expect(parse_data(event)).to eql('id' => received)
|
62
|
-
received += 1
|
63
|
-
end
|
75
|
+
stream_with(20, stream)
|
64
76
|
|
77
|
+
sub = session.subscription(stream)
|
78
|
+
sub.on_event { |event| received << event }
|
65
79
|
sub.start
|
66
80
|
|
67
|
-
|
81
|
+
stream_with(50, stream)
|
68
82
|
|
69
83
|
Timeout.timeout(5) do
|
70
84
|
loop do
|
71
|
-
break if received >= 50
|
85
|
+
break if received.size >= 50
|
72
86
|
sleep(0.1)
|
73
87
|
end
|
74
88
|
end
|
89
|
+
|
90
|
+
expect(received.size).to be(50)
|
91
|
+
expect(received).to start_from(20)
|
75
92
|
end
|
76
93
|
|
77
94
|
it 'allows to make a catchup subscription' do
|
78
95
|
stream = random_stream
|
79
|
-
received =
|
96
|
+
received = []
|
80
97
|
|
81
|
-
|
82
|
-
|
83
|
-
sub = session.subscription(stream, catch_up_from: 20)
|
84
|
-
sub.on_error { |error| raise error.inspect }
|
85
|
-
|
86
|
-
sub.on_event do |event|
|
87
|
-
expect(parse_data(event)).to eql('id' => received + 20)
|
88
|
-
received += 1
|
89
|
-
end
|
98
|
+
stream_with(2100, stream)
|
90
99
|
|
100
|
+
sub = session.subscription(stream, from: 20)
|
101
|
+
sub.on_event { |event| received << event }
|
91
102
|
sub.start
|
92
103
|
|
93
104
|
Thread.new do
|
94
105
|
50.times do
|
95
|
-
|
106
|
+
stream_with(2, stream)
|
96
107
|
end
|
97
108
|
end
|
98
109
|
|
99
110
|
Timeout.timeout(5) do
|
100
111
|
loop do
|
101
|
-
break if received >=
|
112
|
+
break if received.size >= 2180
|
102
113
|
sleep(0.1)
|
103
114
|
end
|
104
115
|
end
|
116
|
+
|
117
|
+
expect(received.size).to be(2180)
|
118
|
+
expect(received).to start_from(20)
|
105
119
|
end
|
106
120
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: estore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mathieu Ravaux
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-04-
|
12
|
+
date: 2015-04-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: beefcake
|
@@ -100,17 +100,24 @@ files:
|
|
100
100
|
- Rakefile
|
101
101
|
- estore.gemspec
|
102
102
|
- lib/estore.rb
|
103
|
-
- lib/estore/
|
103
|
+
- lib/estore/commands.rb
|
104
|
+
- lib/estore/commands/append.rb
|
105
|
+
- lib/estore/commands/base.rb
|
106
|
+
- lib/estore/commands/catch_up_subscription.rb
|
107
|
+
- lib/estore/commands/ping.rb
|
108
|
+
- lib/estore/commands/promise.rb
|
109
|
+
- lib/estore/commands/read_batch.rb
|
110
|
+
- lib/estore/commands/read_forward.rb
|
111
|
+
- lib/estore/commands/subscription.rb
|
104
112
|
- lib/estore/connection.rb
|
105
113
|
- lib/estore/connection/buffer.rb
|
106
|
-
- lib/estore/connection/
|
114
|
+
- lib/estore/connection/protocol.rb
|
107
115
|
- lib/estore/connection_context.rb
|
108
116
|
- lib/estore/errors.rb
|
109
117
|
- lib/estore/message_extensions.rb
|
110
118
|
- lib/estore/messages.rb
|
111
119
|
- lib/estore/package.rb
|
112
120
|
- lib/estore/session.rb
|
113
|
-
- lib/estore/subscription.rb
|
114
121
|
- lib/estore/version.rb
|
115
122
|
- spec/integration/session_spec.rb
|
116
123
|
- spec/spec_helper.rb
|
@@ -1,87 +0,0 @@
|
|
1
|
-
class Estore
|
2
|
-
# Catch-Up Subscriptions
|
3
|
-
#
|
4
|
-
# This kind of subscription specifies a starting point, in the form of an
|
5
|
-
# event number or transaction file position. The given function will be
|
6
|
-
# called for events from the starting point until the end of the stream,
|
7
|
-
# and then for subsequently written events.
|
8
|
-
#
|
9
|
-
# For example, if a starting point of 50 is specified when a stream has 100
|
10
|
-
# events in it, the subscriber can expect to see events 51 through 100, and
|
11
|
-
# then any events subsequently written until such time as the subscription is
|
12
|
-
# dropped or closed.
|
13
|
-
class CatchUpSubscription < Subscription
|
14
|
-
MAX_READ_BATCH = 100
|
15
|
-
|
16
|
-
attr_reader :from, :caught_up
|
17
|
-
|
18
|
-
def initialize(estore, stream, from, options = {})
|
19
|
-
super(estore, stream, options)
|
20
|
-
|
21
|
-
@from = from
|
22
|
-
@caught_up = false
|
23
|
-
@mutex = Mutex.new
|
24
|
-
@queue = []
|
25
|
-
@position = from - 1
|
26
|
-
@batch_size = options[:batch_size] || 100
|
27
|
-
end
|
28
|
-
|
29
|
-
def on_catchup(&block)
|
30
|
-
@on_catchup = block if block
|
31
|
-
end
|
32
|
-
|
33
|
-
def start
|
34
|
-
subscribe
|
35
|
-
backfill
|
36
|
-
switch_to_live
|
37
|
-
call_on_catchup
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def event_appeared(event)
|
43
|
-
unless caught_up
|
44
|
-
@mutex.synchronize do
|
45
|
-
@queue.push(event) unless caught_up
|
46
|
-
end
|
47
|
-
end
|
48
|
-
dispatch(event) if caught_up
|
49
|
-
end
|
50
|
-
|
51
|
-
def switch_to_live
|
52
|
-
@mutex.synchronize do
|
53
|
-
dispatch_events(received_while_backfilling)
|
54
|
-
@queue = nil
|
55
|
-
@caught_up = true
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def backfill
|
60
|
-
loop do
|
61
|
-
events, finished = fetch_batch(@position + 1)
|
62
|
-
@mutex.synchronize do
|
63
|
-
dispatch_events(events)
|
64
|
-
end
|
65
|
-
break if finished
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def dispatch_events(events)
|
70
|
-
events.each { |e| dispatch(e) }
|
71
|
-
end
|
72
|
-
|
73
|
-
def fetch_batch(from)
|
74
|
-
prom = @estore.read(stream, from, @batch_size)
|
75
|
-
response = prom.sync
|
76
|
-
[Array(response.events), response.is_end_of_stream]
|
77
|
-
end
|
78
|
-
|
79
|
-
def received_while_backfilling
|
80
|
-
@queue.find_all { |event| event.original_event_number > @position }
|
81
|
-
end
|
82
|
-
|
83
|
-
def call_on_catchup
|
84
|
-
@on_catchup.call if @on_catchup
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
data/lib/estore/subscription.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
class Estore
|
2
|
-
# Volatile Subscriptions
|
3
|
-
#
|
4
|
-
# This kind of subscription calls a given function for events written
|
5
|
-
# after the subscription is established.
|
6
|
-
#
|
7
|
-
# For example, if a stream has 100 events in it when a subscriber connects,
|
8
|
-
# the subscriber can expect to see event number 101 onwards until the time
|
9
|
-
# the subscription is closed or dropped.
|
10
|
-
class Subscription
|
11
|
-
attr_reader :id, :stream, :resolve_link_tos, :position
|
12
|
-
|
13
|
-
def initialize(estore, stream, options = {})
|
14
|
-
@estore = estore
|
15
|
-
@stream = stream
|
16
|
-
@resolve_link_tos = options.fetch(:resolve_link_tos, true)
|
17
|
-
end
|
18
|
-
|
19
|
-
def on_error(&block)
|
20
|
-
@on_error = block if block
|
21
|
-
end
|
22
|
-
|
23
|
-
def on_event(&block)
|
24
|
-
@on_event = block if block
|
25
|
-
end
|
26
|
-
|
27
|
-
def start
|
28
|
-
subscribe
|
29
|
-
end
|
30
|
-
|
31
|
-
def stop
|
32
|
-
@estore.unsubscribe(id) if id
|
33
|
-
@id = nil
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def subscribe
|
39
|
-
prom = @estore.subscribe(stream, self, resolve_link_tos: resolve_link_tos)
|
40
|
-
@id = prom.correlation_id
|
41
|
-
prom.sync
|
42
|
-
end
|
43
|
-
|
44
|
-
def call_on_error(error)
|
45
|
-
@on_error.call(error) if @on_error
|
46
|
-
end
|
47
|
-
|
48
|
-
def dispatch(event)
|
49
|
-
@on_event.call(event) if @on_event
|
50
|
-
@position = event.original_event_number
|
51
|
-
end
|
52
|
-
|
53
|
-
def event_appeared(event)
|
54
|
-
dispatch(event)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|