estore 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/estore/catchup_subscription.rb +87 -0
- data/lib/estore/connection/buffer.rb +20 -10
- data/lib/estore/connection/{protocol.rb → commands.rb} +0 -0
- data/lib/estore/connection.rb +54 -18
- data/lib/estore/connection_context.rb +75 -11
- data/lib/estore/errors.rb +2 -2
- data/lib/estore/session.rb +47 -21
- data/lib/estore/subscription.rb +57 -0
- data/lib/estore/version.rb +1 -1
- data/lib/estore.rb +3 -2
- data/spec/integration/session_spec.rb +30 -44
- metadata +4 -11
- data/lib/estore/commands/append.rb +0 -56
- data/lib/estore/commands/base.rb +0 -51
- data/lib/estore/commands/catch_up_subscription.rb +0 -60
- data/lib/estore/commands/ping.rb +0 -16
- data/lib/estore/commands/promise.rb +0 -19
- data/lib/estore/commands/read_batch.rb +0 -25
- data/lib/estore/commands/read_forward.rb +0 -39
- data/lib/estore/commands/subscription.rb +0 -53
- data/lib/estore/commands.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ffaed9c9b3874fa4950a194ebc95a84474f5891
|
4
|
+
data.tar.gz: aa3d8f02bba1e085557a7fc0567b619d28b97467
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1dc0acb4b9d8c53e5c58547cbdf610343c221646444f44341b0365d082e327f32c2c8234ac4efe1b8d7ee4faa3670b4c0abd4add84182cd5668085aa61de584e
|
7
|
+
data.tar.gz: 329da382ffb6ae45ad9c0033598232a2b757d0461a7071b569f479b1c3b75fc48e1fdb918b48eb7672b170732ef8659f5550b2ccabac3ed6cb48c807810f74db
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module 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
|
@@ -4,6 +4,7 @@ module Estore
|
|
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
|
7
8
|
def initialize(&block)
|
8
9
|
@mutex = Mutex.new
|
9
10
|
@buffer = ''.force_encoding('BINARY')
|
@@ -14,37 +15,46 @@ module Estore
|
|
14
15
|
bytes = bytes.force_encoding('BINARY') if
|
15
16
|
bytes.respond_to? :force_encoding
|
16
17
|
|
17
|
-
|
18
|
+
mutex.synchronize do
|
18
19
|
@buffer << bytes
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
+
consume_available_packages
|
22
23
|
end
|
23
24
|
|
24
|
-
def
|
25
|
-
while
|
25
|
+
def consume_available_packages
|
26
|
+
while consume_package
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def consume_package
|
31
|
+
pkg = read_package
|
32
|
+
if pkg
|
26
33
|
handle(pkg)
|
27
34
|
discard_bytes(pkg)
|
35
|
+
true
|
36
|
+
else
|
37
|
+
false
|
28
38
|
end
|
29
39
|
end
|
30
40
|
|
31
41
|
def read_package
|
32
|
-
return nil if
|
33
|
-
package_length =
|
34
|
-
bytes =
|
42
|
+
return nil if buffer.length < 4
|
43
|
+
package_length = buffer[0...4].unpack('l<').first
|
44
|
+
bytes = buffer[4...(4 + package_length)].dup
|
35
45
|
bytes if bytes.bytesize >= package_length
|
36
46
|
end
|
37
47
|
|
38
48
|
def discard_bytes(pkg)
|
39
|
-
|
40
|
-
@buffer =
|
49
|
+
mutex.synchronize do
|
50
|
+
@buffer = buffer[(4 + pkg.bytesize)..-1]
|
41
51
|
end
|
42
52
|
end
|
43
53
|
|
44
54
|
def handle(pkg)
|
45
55
|
code, flags, uuid_bytes, message = parse(pkg)
|
46
56
|
command = Estore::Connection.command_name(code)
|
47
|
-
|
57
|
+
handler.call(command, message, Package.parse_uuid(uuid_bytes), flags)
|
48
58
|
end
|
49
59
|
|
50
60
|
def parse(pkg)
|
File without changes
|
data/lib/estore/connection.rb
CHANGED
@@ -3,9 +3,7 @@ module Estore
|
|
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
|
-
|
7
|
-
|
8
|
-
delegate [:register, :remove] => :@context
|
6
|
+
attr_reader :host, :port, :context, :buffer, :mutex
|
9
7
|
|
10
8
|
def initialize(host, port, context)
|
11
9
|
@host = host
|
@@ -20,25 +18,65 @@ module Estore
|
|
20
18
|
socket.close
|
21
19
|
end
|
22
20
|
|
23
|
-
def
|
21
|
+
def send_command(command, msg = nil, handler = nil, uuid = nil)
|
22
|
+
code = COMMANDS.fetch(command)
|
24
23
|
msg.validate! if msg
|
25
24
|
|
26
|
-
|
27
|
-
frame = Package.encode(code,
|
25
|
+
correlation_id = uuid || SecureRandom.uuid
|
26
|
+
frame = Package.encode(code, correlation_id, msg)
|
28
27
|
|
29
|
-
|
28
|
+
mutex.synchronize do
|
29
|
+
promise = context.register_command(correlation_id, command, handler)
|
30
30
|
socket.write(frame.to_s)
|
31
|
+
promise
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
35
|
private
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
|
38
|
+
# rubocop:disable Metrics/MethodLength
|
39
|
+
def on_received_package(command, message, uuid, _flags)
|
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))
|
39
54
|
else
|
40
|
-
|
55
|
+
raise command
|
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
|
41
66
|
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
|
42
80
|
end
|
43
81
|
|
44
82
|
def socket
|
@@ -46,7 +84,7 @@ module Estore
|
|
46
84
|
end
|
47
85
|
|
48
86
|
def connect
|
49
|
-
@socket = TCPSocket.open(
|
87
|
+
@socket = TCPSocket.open(host, port)
|
50
88
|
Thread.new do
|
51
89
|
process_downstream
|
52
90
|
end
|
@@ -54,12 +92,12 @@ module Estore
|
|
54
92
|
rescue TimeoutError, Errno::ECONNREFUSED, Errno::EHOSTDOWN,
|
55
93
|
Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ETIMEDOUT
|
56
94
|
raise CannotConnectError, "Error connecting to Eventstore on "\
|
57
|
-
"#{
|
95
|
+
"#{host.inspect}:#{port.inspect} (#{$ERROR_INFO.class})"
|
58
96
|
end
|
59
97
|
|
60
98
|
def process_downstream
|
61
99
|
loop do
|
62
|
-
|
100
|
+
buffer << socket.sysread(4096)
|
63
101
|
end
|
64
102
|
rescue IOError, EOFError
|
65
103
|
on_disconnect
|
@@ -74,10 +112,8 @@ module Estore
|
|
74
112
|
end
|
75
113
|
|
76
114
|
def on_exception(error)
|
77
|
-
puts "process_downstream_error"
|
78
|
-
|
79
|
-
puts error.backtrace
|
80
|
-
@context.on_error(error)
|
115
|
+
puts "process_downstream_error #{error.inspect}"
|
116
|
+
context.on_error(error)
|
81
117
|
end
|
82
118
|
end
|
83
119
|
end
|
@@ -1,28 +1,92 @@
|
|
1
1
|
require 'promise'
|
2
2
|
|
3
3
|
module Estore
|
4
|
-
#
|
4
|
+
# Extension of a Ruby implementation of the Promises/A+ spec
|
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
|
5
25
|
class ConnectionContext
|
26
|
+
attr_reader :mutex, :requests, :targets
|
6
27
|
def initialize
|
7
28
|
@mutex = Mutex.new
|
8
|
-
@
|
29
|
+
@requests = {}
|
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
|
9
55
|
end
|
10
56
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
57
|
+
def rejected_command(uuid, error)
|
58
|
+
prom = nil
|
59
|
+
|
60
|
+
mutex.synchronize do
|
61
|
+
prom = requests.delete(uuid)
|
14
62
|
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)
|
15
71
|
end
|
16
72
|
|
17
|
-
def
|
18
|
-
|
19
|
-
@
|
73
|
+
def on_error(error = nil, &block)
|
74
|
+
if block
|
75
|
+
@error_handler = block
|
76
|
+
else
|
77
|
+
@error_handler.call(error) if @error_handler
|
20
78
|
end
|
21
79
|
end
|
22
80
|
|
23
|
-
|
24
|
-
|
25
|
-
|
81
|
+
private
|
82
|
+
|
83
|
+
def promise(uuid, target = nil)
|
84
|
+
prom = Promise.new(uuid)
|
85
|
+
mutex.synchronize do
|
86
|
+
requests[uuid] = prom
|
87
|
+
targets[uuid] = target
|
88
|
+
end
|
89
|
+
prom
|
26
90
|
end
|
27
91
|
end
|
28
92
|
end
|
data/lib/estore/errors.rb
CHANGED
data/lib/estore/session.rb
CHANGED
@@ -31,44 +31,70 @@ module Estore
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def ping
|
34
|
-
command(
|
34
|
+
command('Ping')
|
35
35
|
end
|
36
36
|
|
37
|
-
def read(stream,
|
38
|
-
|
39
|
-
|
37
|
+
def read(stream, start, limit)
|
38
|
+
msg = ReadStreamEvents.new(
|
39
|
+
event_stream_id: stream,
|
40
|
+
from_event_number: start,
|
41
|
+
max_count: limit,
|
42
|
+
resolve_link_tos: true,
|
43
|
+
require_master: false
|
44
|
+
)
|
40
45
|
|
41
|
-
|
42
|
-
read_batch(stream, from, limit)
|
43
|
-
else
|
44
|
-
read_forward(stream, from)
|
45
|
-
end
|
46
|
+
command('ReadStreamEventsForward', msg)
|
46
47
|
end
|
47
48
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
49
|
+
def append(stream, events, options = {})
|
50
|
+
msg = WriteEvents.new(
|
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
|
+
)
|
51
56
|
|
52
|
-
|
53
|
-
command(Commands::ReadForward, stream, from, batch_size, &block).call
|
57
|
+
command('WriteEvents', msg)
|
54
58
|
end
|
55
59
|
|
56
|
-
def
|
57
|
-
|
60
|
+
def subscribe(stream, handler, options = {})
|
61
|
+
msg = SubscribeToStream.new(
|
62
|
+
event_stream_id: stream,
|
63
|
+
resolve_link_tos: options[:resolve_link_tos]
|
64
|
+
)
|
65
|
+
|
66
|
+
command('SubscribeToStream', msg, handler)
|
58
67
|
end
|
59
68
|
|
60
69
|
def subscription(stream, options = {})
|
61
|
-
if options[:
|
62
|
-
|
70
|
+
if options[:catch_up_from]
|
71
|
+
CatchUpSubscription.new(self, stream, options[:catch_up_from], options)
|
63
72
|
else
|
64
|
-
|
73
|
+
Subscription.new(self, stream, options)
|
65
74
|
end
|
66
75
|
end
|
67
76
|
|
68
77
|
private
|
69
78
|
|
70
|
-
|
71
|
-
|
79
|
+
CONTENT_TYPES = {
|
80
|
+
json: 1
|
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)
|
72
98
|
end
|
73
99
|
end
|
74
100
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module 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
|
data/lib/estore/version.rb
CHANGED
data/lib/estore.rb
CHANGED
@@ -6,5 +6,6 @@ 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/
|
9
|
+
require 'estore/connection/commands'
|
10
|
+
require 'estore/subscription'
|
11
|
+
require 'estore/catchup_subscription'
|
@@ -33,88 +33,74 @@ describe Estore::Session do
|
|
33
33
|
"test-#{SecureRandom.uuid}"
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
36
|
+
def populate(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
|
-
|
45
|
-
expect(parse_data(event)).to eql('id' => index + start)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
it 'reads all the events from a stream' do
|
51
|
-
events = session.read(stream_with(200)).sync
|
52
|
-
|
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
|
42
|
+
it 'reads events from a stream' do
|
43
|
+
stream = populate(20)
|
44
|
+
read = session.read(stream, 0, 20).sync
|
59
45
|
|
60
|
-
expect(events.size).to be(
|
61
|
-
expect(events).to start_from(20)
|
62
|
-
end
|
46
|
+
expect(read.events.size).to be(20)
|
63
47
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
expect(events.size).to be(15)
|
68
|
-
expect(events).to start_from(10)
|
48
|
+
read.events.each_with_index do |event, index|
|
49
|
+
expect(parse_data(event)).to eql('id' => index)
|
50
|
+
end
|
69
51
|
end
|
70
52
|
|
71
53
|
it 'allows to make a live subscription' do
|
72
54
|
stream = random_stream
|
73
|
-
received =
|
74
|
-
|
75
|
-
stream_with(20, stream)
|
55
|
+
received = 0
|
76
56
|
|
77
57
|
sub = session.subscription(stream)
|
78
|
-
sub.
|
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
|
64
|
+
|
79
65
|
sub.start
|
80
66
|
|
81
|
-
|
67
|
+
populate(50, stream)
|
82
68
|
|
83
69
|
Timeout.timeout(5) do
|
84
70
|
loop do
|
85
|
-
break if received
|
71
|
+
break if received >= 50
|
86
72
|
sleep(0.1)
|
87
73
|
end
|
88
74
|
end
|
89
|
-
|
90
|
-
expect(received.size).to be(50)
|
91
|
-
expect(received).to start_from(20)
|
92
75
|
end
|
93
76
|
|
94
77
|
it 'allows to make a catchup subscription' do
|
95
78
|
stream = random_stream
|
96
|
-
received =
|
79
|
+
received = 0
|
97
80
|
|
98
|
-
|
81
|
+
populate(50, stream)
|
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
|
99
90
|
|
100
|
-
sub = session.subscription(stream, from: 20)
|
101
|
-
sub.on_event { |event| received << event }
|
102
91
|
sub.start
|
103
92
|
|
104
93
|
Thread.new do
|
105
94
|
50.times do
|
106
|
-
|
95
|
+
populate(2, stream)
|
107
96
|
end
|
108
97
|
end
|
109
98
|
|
110
99
|
Timeout.timeout(5) do
|
111
100
|
loop do
|
112
|
-
break if received
|
101
|
+
break if received >= 130
|
113
102
|
sleep(0.1)
|
114
103
|
end
|
115
104
|
end
|
116
|
-
|
117
|
-
expect(received.size).to be(2180)
|
118
|
-
expect(received).to start_from(20)
|
119
105
|
end
|
120
106
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mathieu Ravaux
|
@@ -100,24 +100,17 @@ files:
|
|
100
100
|
- Rakefile
|
101
101
|
- estore.gemspec
|
102
102
|
- lib/estore.rb
|
103
|
-
- lib/estore/
|
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
|
103
|
+
- lib/estore/catchup_subscription.rb
|
112
104
|
- lib/estore/connection.rb
|
113
105
|
- lib/estore/connection/buffer.rb
|
114
|
-
- lib/estore/connection/
|
106
|
+
- lib/estore/connection/commands.rb
|
115
107
|
- lib/estore/connection_context.rb
|
116
108
|
- lib/estore/errors.rb
|
117
109
|
- lib/estore/message_extensions.rb
|
118
110
|
- lib/estore/messages.rb
|
119
111
|
- lib/estore/package.rb
|
120
112
|
- lib/estore/session.rb
|
113
|
+
- lib/estore/subscription.rb
|
121
114
|
- lib/estore/version.rb
|
122
115
|
- spec/integration/session_spec.rb
|
123
116
|
- spec/spec_helper.rb
|
@@ -1,56 +0,0 @@
|
|
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
|
data/lib/estore/commands/base.rb
DELETED
@@ -1,51 +0,0 @@
|
|
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
|
@@ -1,60 +0,0 @@
|
|
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
|
data/lib/estore/commands/ping.rb
DELETED
@@ -1,19 +0,0 @@
|
|
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
|
@@ -1,25 +0,0 @@
|
|
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
|
@@ -1,39 +0,0 @@
|
|
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
|
@@ -1,53 +0,0 @@
|
|
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/commands.rb
DELETED
@@ -1,8 +0,0 @@
|
|
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'
|