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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1eaa9c5801e991ca2643e6412176c10144e2dcb7
4
- data.tar.gz: 61800929c52882a40cb9fd234a0238902d7e66b0
3
+ metadata.gz: 4ffaed9c9b3874fa4950a194ebc95a84474f5891
4
+ data.tar.gz: aa3d8f02bba1e085557a7fc0567b619d28b97467
5
5
  SHA512:
6
- metadata.gz: a5231c8eb6a0e0713b71b2f80c9640892202cee0aaed8f224b975dad02739b9c3f90c2feec3bdc33302a67e80fd895e3ec11b03f0cc546f4eab43359231fd278
7
- data.tar.gz: 9da5606ca43d26bcac0ce1e860ae290b162de59d49691107a7fd92dd83ab643ffd1a726ff8cc1369b8c94633b1ddf4ba516153e36579add0b5c66ce6dd9d9cfa
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
- @mutex.synchronize do
18
+ mutex.synchronize do
18
19
  @buffer << bytes
19
20
  end
20
21
 
21
- consume_packages
22
+ consume_available_packages
22
23
  end
23
24
 
24
- def consume_packages
25
- while (pkg = read_package)
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 @buffer.length < 4
33
- package_length = @buffer[0...4].unpack('l<').first
34
- bytes = @buffer[4...(4 + package_length)].dup
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
- @mutex.synchronize do
40
- @buffer = @buffer[(4 + pkg.bytesize)..-1]
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
- @handler.call(message, command, Package.parse_uuid(uuid_bytes), flags)
57
+ handler.call(command, message, Package.parse_uuid(uuid_bytes), flags)
48
58
  end
49
59
 
50
60
  def parse(pkg)
File without changes
@@ -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
- extend Forwardable
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 write(uuid, command, msg = nil)
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
- code = COMMANDS.fetch(command)
27
- frame = Package.encode(code, uuid, msg)
25
+ correlation_id = uuid || SecureRandom.uuid
26
+ frame = Package.encode(code, correlation_id, msg)
28
27
 
29
- @mutex.synchronize do
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
- def on_received_package(message, type, uuid, _flags)
37
- if type == 'HeartbeatRequestCommand'
38
- write(SecureRandom.uuid, 'HeartbeatResponseCommand')
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
- @context.dispatch(uuid, message, type)
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(@host, @port)
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
- "#{@host.inspect}:#{@port.inspect} (#{$ERROR_INFO.class})"
95
+ "#{host.inspect}:#{port.inspect} (#{$ERROR_INFO.class})"
58
96
  end
59
97
 
60
98
  def process_downstream
61
99
  loop do
62
- @buffer << socket.sysread(4096)
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
- puts error.message
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
- # Registry storing handlers for the pending commands
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
- @commands = {}
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 register(command)
12
- @mutex.synchronize do
13
- @commands[command.uuid] = command
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 remove(command)
18
- @mutex.synchronize do
19
- @commands.delete(command.uuid)
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
- def dispatch(uuid, message, type)
24
- command = @commands[uuid]
25
- command.handle(message, type) if command
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
@@ -1,4 +1,4 @@
1
1
  module Estore
2
- CannotConnectError = Class.new(RuntimeError)
3
- DisconnectionError = Class.new(RuntimeError)
2
+ class CannotConnectError < RuntimeError; end
3
+ class DisconnectionError < RuntimeError; end
4
4
  end
@@ -31,44 +31,70 @@ module Estore
31
31
  end
32
32
 
33
33
  def ping
34
- command(Commands::Ping)
34
+ command('Ping')
35
35
  end
36
36
 
37
- def read(stream, options = {})
38
- from = options[:from] || 0
39
- limit = options[:limit]
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
- if limit
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 read_batch(stream, from, limit)
49
- command(Commands::ReadBatch, stream, from, limit).call
50
- end
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
- def read_forward(stream, from, batch_size = nil, &block)
53
- command(Commands::ReadForward, stream, from, batch_size, &block).call
57
+ command('WriteEvents', msg)
54
58
  end
55
59
 
56
- def append(stream, events, options = {})
57
- command(Commands::Append, stream, events, options).call
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[:from]
62
- command(Commands::CatchUpSubscription, stream, options[:from], options)
70
+ if options[:catch_up_from]
71
+ CatchUpSubscription.new(self, stream, options[:catch_up_from], options)
63
72
  else
64
- command(Commands::Subscription, stream, options)
73
+ Subscription.new(self, stream, options)
65
74
  end
66
75
  end
67
76
 
68
77
  private
69
78
 
70
- def command(command, *args)
71
- command.new(connection, *args)
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
@@ -1,3 +1,3 @@
1
1
  module Estore
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.4'
3
3
  end
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/protocol'
10
- require 'estore/commands'
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 stream_with(count, stream = nil)
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
- 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
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(80)
61
- expect(events).to start_from(20)
62
- end
46
+ expect(read.events.size).to be(20)
63
47
 
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)
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.on_event { |event| received << event }
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
- stream_with(50, stream)
67
+ populate(50, stream)
82
68
 
83
69
  Timeout.timeout(5) do
84
70
  loop do
85
- break if received.size >= 50
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
- stream_with(2100, stream)
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
- stream_with(2, stream)
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.size >= 2180
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.3
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/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
103
+ - lib/estore/catchup_subscription.rb
112
104
  - lib/estore/connection.rb
113
105
  - lib/estore/connection/buffer.rb
114
- - lib/estore/connection/protocol.rb
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
@@ -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
@@ -1,16 +0,0 @@
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
@@ -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
@@ -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'