estore 0.0.4 → 0.1.0
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/estore.gemspec +2 -2
- 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 +18 -54
- data/lib/estore/connection/buffer.rb +10 -20
- data/lib/estore/connection/{commands.rb → protocol.rb} +0 -0
- data/lib/estore/connection_context.rb +11 -75
- data/lib/estore/errors.rb +2 -2
- data/lib/estore/session.rb +21 -47
- data/lib/estore/version.rb +1 -1
- data/spec/integration/session_spec.rb +44 -30
- metadata +16 -9
- 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: 18db1ad6f67a5c73ecd75dbe6fc80f5f03b717f2
|
4
|
+
data.tar.gz: 47b240aa7ce8fddbe969103718a42a1668575f8d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b5d4d2f91f7e7c2296684ac876e11b9c3959db6988e5b48cf0a8e5888723cd048acf68b14ebe159b0e9c2d4494030d2179912ef1777c6a4b878b5c85fbebcb60
|
7
|
+
data.tar.gz: abaeaa10b0ec1c4d623bddb40550cee7f8d8afe2eddf36f19baea5092548cd5a74b6dd8098329f6d07a5b2626c634e57ae45d3bb846d79868f57dd8e9bb6c035
|
data/estore.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ['Mathieu Ravaux', 'Héctor Ramón']
|
10
10
|
spec.email = ['mathieu.ravaux@gmail.com', 'hector0193@gmail.com']
|
11
11
|
spec.summary = 'An Event Store driver for Ruby'
|
12
|
-
spec.description =
|
12
|
+
spec.description = 'TCP driver to read and write events to Event Store'
|
13
13
|
spec.homepage = 'https://github.com/rom-eventstore/estore'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
@@ -22,6 +22,6 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_dependency 'promise.rb', '~> 0.6.1'
|
23
23
|
|
24
24
|
spec.add_development_dependency 'bundler'
|
25
|
-
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
26
|
spec.add_development_dependency 'rubocop', '~> 0.28.0'
|
27
27
|
end
|
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
@@ -3,7 +3,9 @@ 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
|
-
|
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 @@ module 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 @@ module 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 @@ module 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 @@ module 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
|
@@ -4,7 +4,6 @@ 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
|
8
7
|
def initialize(&block)
|
9
8
|
@mutex = Mutex.new
|
10
9
|
@buffer = ''.force_encoding('BINARY')
|
@@ -15,46 +14,37 @@ module 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)
|
File without changes
|
@@ -1,92 +1,28 @@
|
|
1
1
|
require 'promise'
|
2
2
|
|
3
3
|
module Estore
|
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
|
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
data/lib/estore/session.rb
CHANGED
@@ -31,70 +31,44 @@ module Estore
|
|
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
@@ -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.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mathieu Ravaux
|
@@ -57,16 +57,16 @@ dependencies:
|
|
57
57
|
name: rake
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- - "
|
60
|
+
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '0'
|
62
|
+
version: '10.0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- - "
|
67
|
+
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '0'
|
69
|
+
version: '10.0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rubocop
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,7 +81,7 @@ dependencies:
|
|
81
81
|
- - "~>"
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: 0.28.0
|
84
|
-
description:
|
84
|
+
description: TCP driver to read and write events to Event Store
|
85
85
|
email:
|
86
86
|
- mathieu.ravaux@gmail.com
|
87
87
|
- hector0193@gmail.com
|
@@ -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
|
-
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
|
data/lib/estore/subscription.rb
DELETED
@@ -1,57 +0,0 @@
|
|
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
|