estore 0.0.3 → 0.0.4
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 +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'
|