estore 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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'