estore 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ffaed9c9b3874fa4950a194ebc95a84474f5891
4
- data.tar.gz: aa3d8f02bba1e085557a7fc0567b619d28b97467
3
+ metadata.gz: 18db1ad6f67a5c73ecd75dbe6fc80f5f03b717f2
4
+ data.tar.gz: 47b240aa7ce8fddbe969103718a42a1668575f8d
5
5
  SHA512:
6
- metadata.gz: 1dc0acb4b9d8c53e5c58547cbdf610343c221646444f44341b0365d082e327f32c2c8234ac4efe1b8d7ee4faa3670b4c0abd4add84182cd5668085aa61de584e
7
- data.tar.gz: 329da382ffb6ae45ad9c0033598232a2b757d0461a7071b569f479b1c3b75fc48e1fdb918b48eb7672b170732ef8659f5550b2ccabac3ed6cb48c807810f74db
6
+ metadata.gz: b5d4d2f91f7e7c2296684ac876e11b9c3959db6988e5b48cf0a8e5888723cd048acf68b14ebe159b0e9c2d4494030d2179912ef1777c6a4b878b5c85fbebcb60
7
+ data.tar.gz: abaeaa10b0ec1c4d623bddb40550cee7f8d8afe2eddf36f19baea5092548cd5a74b6dd8098329f6d07a5b2626c634e57ae45d3bb846d79868f57dd8e9bb6c035
@@ -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 = spec.summary
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
@@ -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/commands'
10
- require 'estore/subscription'
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,16 @@
1
+ module Estore
2
+ module Commands
3
+ class Ping
4
+ include Command
5
+
6
+ def call
7
+ write('Ping')
8
+ promise
9
+ end
10
+
11
+ def handle(*)
12
+ promise.fulfill('Pong')
13
+ end
14
+ end
15
+ end
16
+ 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
@@ -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
- attr_reader :host, :port, :context, :buffer, :mutex
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 send_command(command, msg = nil, handler = nil, uuid = nil)
22
- code = COMMANDS.fetch(command)
23
+ def write(uuid, command, msg = nil)
23
24
  msg.validate! if msg
24
25
 
25
- correlation_id = uuid || SecureRandom.uuid
26
- frame = Package.encode(code, correlation_id, msg)
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
- # 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))
36
+ def on_received_package(message, type, uuid, _flags)
37
+ if type == 'HeartbeatRequestCommand'
38
+ write(SecureRandom.uuid, 'HeartbeatResponseCommand')
54
39
  else
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
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 #{error.inspect}"
116
- context.on_error(error)
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
- consume_available_packages
21
+ consume_packages
23
22
  end
24
23
 
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
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(command, message, Package.parse_uuid(uuid_bytes), flags)
47
+ @handler.call(message, command, Package.parse_uuid(uuid_bytes), flags)
58
48
  end
59
49
 
60
50
  def parse(pkg)
@@ -1,92 +1,28 @@
1
1
  require 'promise'
2
2
 
3
3
  module Estore
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
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
- @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
8
+ @commands = {}
55
9
  end
56
10
 
57
- def rejected_command(uuid, error)
58
- prom = nil
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 on_error(error = nil, &block)
74
- if block
75
- @error_handler = block
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
- 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
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
@@ -1,4 +1,4 @@
1
1
  module Estore
2
- class CannotConnectError < RuntimeError; end
3
- class DisconnectionError < RuntimeError; end
2
+ CannotConnectError = Class.new(RuntimeError)
3
+ DisconnectionError = Class.new(RuntimeError)
4
4
  end
@@ -31,70 +31,44 @@ module Estore
31
31
  end
32
32
 
33
33
  def ping
34
- command('Ping')
34
+ command(Commands::Ping)
35
35
  end
36
36
 
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
- )
37
+ def read(stream, options = {})
38
+ from = options[:from] || 0
39
+ limit = options[:limit]
45
40
 
46
- command('ReadStreamEventsForward', msg)
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 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
- )
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 subscribe(stream, handler, options = {})
61
- msg = SubscribeToStream.new(
62
- event_stream_id: stream,
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
- command('SubscribeToStream', msg, handler)
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[:catch_up_from]
71
- CatchUpSubscription.new(self, stream, options[:catch_up_from], options)
61
+ if options[:from]
62
+ command(Commands::CatchUpSubscription, stream, options[:from], options)
72
63
  else
73
- Subscription.new(self, stream, options)
64
+ command(Commands::Subscription, stream, options)
74
65
  end
75
66
  end
76
67
 
77
68
  private
78
69
 
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)
70
+ def command(command, *args)
71
+ command.new(connection, *args)
98
72
  end
99
73
  end
100
74
  end
@@ -1,3 +1,3 @@
1
1
  module Estore
2
- VERSION = '0.0.4'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -33,74 +33,88 @@ describe Estore::Session do
33
33
  "test-#{SecureRandom.uuid}"
34
34
  end
35
35
 
36
- def populate(count, stream = nil)
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
- it 'reads events from a stream' do
43
- stream = populate(20)
44
- read = session.read(stream, 0, 20).sync
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
- expect(read.events.size).to be(20)
50
+ it 'reads all the events from a stream' do
51
+ events = session.read(stream_with(200)).sync
47
52
 
48
- read.events.each_with_index do |event, index|
49
- expect(parse_data(event)).to eql('id' => index)
50
- end
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 = 0
73
+ received = []
56
74
 
57
- sub = session.subscription(stream)
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
- populate(50, stream)
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 = 0
96
+ received = []
80
97
 
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
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
- populate(2, stream)
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 >= 130
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
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: An Event Store driver for Ruby
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/catchup_subscription.rb
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/commands.rb
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
@@ -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