estore 0.0.1
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 +7 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +11 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +21 -0
- data/circle.yml +3 -0
- data/estore.gemspec +28 -0
- data/lib/estore.rb +92 -0
- data/lib/estore/catchup_subscription.rb +94 -0
- data/lib/estore/connection.rb +118 -0
- data/lib/estore/connection/buffer.rb +67 -0
- data/lib/estore/connection/commands.rb +90 -0
- data/lib/estore/connection_context.rb +90 -0
- data/lib/estore/errors.rb +4 -0
- data/lib/estore/message_extensions.rb +19 -0
- data/lib/estore/messages.rb +369 -0
- data/lib/estore/package.rb +31 -0
- data/lib/estore/subscription.rb +57 -0
- data/lib/estore/version.rb +3 -0
- data/spec/db/.gitkeep +0 -0
- data/spec/eventstore_spec.rb +103 -0
- data/spec/spec_helper.rb +12 -0
- data/vendor/proto/ClientMessageDtos.proto +261 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1a58e5f588a5c8c0d2b25fedf68429869effc0dd
|
4
|
+
data.tar.gz: f59bc201d6f2c2fcc9dcc5f1d2bb825a70f8aac7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 19c2507e00eca563cb754a645cc4b6a2dde55346a1f99d6a8b82ca406b30d73128e8e2ca5ea5761733514d33464b8a3fc16e7d4ba0618b6c5fae0fa2e08716ab
|
7
|
+
data.tar.gz: 43e2a30135b4b708cd9593e41a51692de8501b4b736db77e2aaf0a281caa7af64f84de2de090192daa626ce5e3b493fce2c298a005b0e0ac97ade2fc310066df
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
File without changes
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Mathieu Ravaux
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Eventstore
|
2
|
+
|
3
|
+
[](https://gitter.im/mathieuravaux/eventstore-ruby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
|
+
|
5
|
+
Ruby client library Eventstore
|
6
|
+
|
7
|
+
Eventstore is an open-source, functional database
|
8
|
+
with Complex Event Processing in JavaScript
|
9
|
+
|
10
|
+
[](https://circleci.com/gh/mathieuravaux/eventstore-ruby)
|
11
|
+
|
12
|
+
## Install
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile and run Bundler:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'estore'
|
18
|
+
```
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install estore
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
## Contributing
|
29
|
+
|
30
|
+
1. Fork it ( https://github.com/mathieuravaux/eventstore/fork )
|
31
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
32
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
33
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
34
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
VENDORED_PROTO = 'vendor/proto/ClientMessageDtos.proto'
|
5
|
+
PROTO_URL = 'https://raw.githubusercontent.com/EventStore/EventStore/oss-v3.0.1/src/Protos/ClientAPI/ClientMessageDtos.proto'
|
6
|
+
PROTO_DIR = 'lib/estore'
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
|
10
|
+
task default: :spec
|
11
|
+
|
12
|
+
desc 'Update the protobuf messages definition'
|
13
|
+
task :proto do
|
14
|
+
system("wget -O #{VENDORED_PROTO} #{PROTO_URL}")
|
15
|
+
system("mkdir -p #{PROTO_DIR}")
|
16
|
+
beefcake_bin = Bundler.bin_path.join('protoc-gen-beefcake').to_s
|
17
|
+
if system("BEEFCAKE_NAMESPACE=Eventstore protoc --plugin=#{beefcake_bin} --beefcake_out lib/estore #{VENDORED_PROTO}")
|
18
|
+
FileUtils.mv('lib/estore/ClientMessageDtos.pb.rb', 'lib/estore/messages.rb')
|
19
|
+
system("sed -i '' 's/module Eventstore/class Eventstore/' lib/estore/messages.rb")
|
20
|
+
end
|
21
|
+
end
|
data/circle.yml
ADDED
data/estore.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'estore/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'estore'
|
8
|
+
spec.version = EventStore::VERSION
|
9
|
+
spec.authors = ['Mathieu Ravaux', 'Héctor Ramón']
|
10
|
+
spec.email = ['mathieu.ravaux@gmail.com', 'hector0193@gmail.com']
|
11
|
+
spec.summary = 'Ruby client API for the Event Store.'
|
12
|
+
spec.description = 'Event Store is an open-source, functional database with Complex Event Processing in JavaScript.'
|
13
|
+
spec.homepage = 'https://github.com/rom-eventstore/eventstore-ruby'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(/^spec\//)
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'beefcake', '~> 1.1.0.pre1'
|
22
|
+
spec.add_dependency 'promise.rb', '~> 0.6.1'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rspec'
|
27
|
+
spec.add_development_dependency 'pry'
|
28
|
+
end
|
data/lib/estore.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
# The Eventstore class is responsible for maintaining a full-duplex connection
|
4
|
+
# between the client and the Event Store server.
|
5
|
+
# EventStore is thread-safe, and it is recommended that only one instance per application is created.
|
6
|
+
#
|
7
|
+
# All operations are handled fully asynchronously, returning a promise.
|
8
|
+
# If you need to execute synchronously, simply call .sync on the returned promise.
|
9
|
+
#
|
10
|
+
# To get maximum performance from the connection, it is recommended that it be used asynchronously.
|
11
|
+
class Eventstore
|
12
|
+
attr_reader :host, :port, :connection, :context, :error_handler
|
13
|
+
def initialize(host, port = 2113)
|
14
|
+
@host = host
|
15
|
+
@port = port
|
16
|
+
@context = ConnectionContext.new
|
17
|
+
@connection = Connection.new(host, port, context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_error(error = nil, &block)
|
21
|
+
context.on_error(error, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
connection.close
|
26
|
+
end
|
27
|
+
|
28
|
+
def ping
|
29
|
+
command('Ping')
|
30
|
+
end
|
31
|
+
|
32
|
+
def new_event(event_type, data, content_type: 'json', uuid: nil)
|
33
|
+
uuid ||= SecureRandom.uuid
|
34
|
+
content_type_code = { 'json' => 1 }.fetch(content_type, 0)
|
35
|
+
NewEvent.new(
|
36
|
+
event_id: Package.encode_uuid(uuid),
|
37
|
+
event_type: event_type,
|
38
|
+
data: data,
|
39
|
+
data_content_type: content_type_code,
|
40
|
+
metadata_content_type: 1
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def write_events(stream, events)
|
45
|
+
events = Array(events)
|
46
|
+
msg = WriteEvents.new(
|
47
|
+
event_stream_id: stream,
|
48
|
+
expected_version: -2,
|
49
|
+
events: events,
|
50
|
+
require_master: true
|
51
|
+
)
|
52
|
+
command('WriteEvents', msg)
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_stream_events_forward(stream, start, max)
|
56
|
+
msg = ReadStreamEvents.new(
|
57
|
+
event_stream_id: stream,
|
58
|
+
from_event_number: start,
|
59
|
+
max_count: max,
|
60
|
+
resolve_link_tos: true,
|
61
|
+
require_master: false
|
62
|
+
)
|
63
|
+
command('ReadStreamEventsForward', msg)
|
64
|
+
end
|
65
|
+
|
66
|
+
def subscribe_to_stream(handler, stream, resolve_link_tos = false)
|
67
|
+
msg = SubscribeToStream.new(event_stream_id: stream, resolve_link_tos: resolve_link_tos)
|
68
|
+
command('SubscribeToStream', msg, handler)
|
69
|
+
end
|
70
|
+
|
71
|
+
def unsubscribe_from_stream(subscription_uuid)
|
72
|
+
msg = UnsubscribeFromStream.new
|
73
|
+
command('UnsubscribeFromStream', msg, uuid: subscription_uuid)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def command(*args)
|
79
|
+
connection.send_command(*args)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
require_relative 'estore/errors'
|
84
|
+
require_relative 'estore/package'
|
85
|
+
require_relative 'estore/messages'
|
86
|
+
require_relative 'estore/message_extensions'
|
87
|
+
require_relative 'estore/connection_context'
|
88
|
+
require_relative 'estore/connection'
|
89
|
+
require_relative 'estore/connection/buffer'
|
90
|
+
require_relative 'estore/connection/commands'
|
91
|
+
require_relative 'estore/subscription'
|
92
|
+
require_relative 'estore/catchup_subscription'
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class Eventstore
|
2
|
+
# Catch-Up Subscriptions
|
3
|
+
#
|
4
|
+
# This kind of subscription specifies a starting point, in the form of an event
|
5
|
+
# number or transaction file position. The given function will be called for events
|
6
|
+
# from the starting point until the end of the stream, and then for subsequently written events.
|
7
|
+
#
|
8
|
+
# For example, if a starting point of 50 is specified when a stream has 100 events in it,
|
9
|
+
# the subscriber can expect to see events 51 through 100, and then any events subsequently
|
10
|
+
# written until such time as the subscription is dropped or closed.
|
11
|
+
#
|
12
|
+
class CatchUpSubscription < Subscription
|
13
|
+
MAX_READ_BATCH = 100
|
14
|
+
|
15
|
+
attr_reader :from, :caught_up
|
16
|
+
|
17
|
+
def initialize(eventstore, stream, from, resolve_link_tos: true, batch_size: MAX_READ_BATCH)
|
18
|
+
super(eventstore, stream, resolve_link_tos: resolve_link_tos)
|
19
|
+
@from = from
|
20
|
+
@caught_up = false
|
21
|
+
|
22
|
+
@mutex = Mutex.new
|
23
|
+
@queue = []
|
24
|
+
@position = from
|
25
|
+
@batch_size = batch_size
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_catchup(&block)
|
29
|
+
@on_catchup = block if block
|
30
|
+
end
|
31
|
+
|
32
|
+
def start
|
33
|
+
subscribe
|
34
|
+
backfill
|
35
|
+
switch_to_live
|
36
|
+
call_on_catchup
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def event_appeared(event)
|
42
|
+
unless caught_up
|
43
|
+
@mutex.synchronize do
|
44
|
+
@queue.push(event) unless caught_up
|
45
|
+
end
|
46
|
+
end
|
47
|
+
dispatch(event) if caught_up
|
48
|
+
end
|
49
|
+
|
50
|
+
def switch_to_live
|
51
|
+
log("fn=switch_to_live id=#{@id} stream=#{stream} at=start position=#{@position} queue_size=#{@queue.size}")
|
52
|
+
@mutex.synchronize do
|
53
|
+
dispatch_events(received_while_backfilling)
|
54
|
+
@queue = nil
|
55
|
+
@caught_up = true
|
56
|
+
end
|
57
|
+
log("fn=switch_to_live id=#{@id} stream=#{stream} at=finish position=#{@position}")
|
58
|
+
end
|
59
|
+
|
60
|
+
def backfill
|
61
|
+
log("fn=backfill at=start position=#{@position}")
|
62
|
+
loop do
|
63
|
+
events, finished = fetch_batch(@position + 1)
|
64
|
+
@mutex.synchronize do
|
65
|
+
dispatch_events(events)
|
66
|
+
end
|
67
|
+
break if finished
|
68
|
+
end
|
69
|
+
log("fn=backfill at=finish position=#{@position}")
|
70
|
+
end
|
71
|
+
|
72
|
+
def dispatch_events(events)
|
73
|
+
events.each { |e| dispatch(e) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def fetch_batch(from)
|
77
|
+
prom = eventstore.read_stream_events_forward(stream, from, @batch_size)
|
78
|
+
response = prom.sync
|
79
|
+
[Array(response.events), response.is_end_of_stream]
|
80
|
+
end
|
81
|
+
|
82
|
+
def received_while_backfilling
|
83
|
+
@queue.find_all { |event| event.original_event_number > @position }
|
84
|
+
end
|
85
|
+
|
86
|
+
def call_on_catchup
|
87
|
+
@on_catchup.call if @on_catchup
|
88
|
+
end
|
89
|
+
|
90
|
+
def log(msg)
|
91
|
+
puts(msg)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
class Eventstore
|
2
|
+
# Connection owns the TCP socket, formats and sends commands over the socket.
|
3
|
+
# It also starts a background thread to read from the TCP socket and handle received packages,
|
4
|
+
# dispatching them to the calling app.
|
5
|
+
class Connection
|
6
|
+
attr_reader :host, :port, :context, :error_handler
|
7
|
+
attr_reader :buffer, :mutex
|
8
|
+
|
9
|
+
def initialize(host, port, context)
|
10
|
+
@host = host
|
11
|
+
@port = Integer(port)
|
12
|
+
@context = context
|
13
|
+
|
14
|
+
@buffer = Buffer.new(&method(:on_received_package))
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def close
|
19
|
+
@terminating = true
|
20
|
+
socket.close
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_command(command, msg = nil, handler = nil, uuid = nil)
|
24
|
+
code = COMMANDS.fetch(command)
|
25
|
+
msg.validate! if msg
|
26
|
+
|
27
|
+
correlation_id = uuid || SecureRandom.uuid
|
28
|
+
frame = Package.encode(code, correlation_id, msg)
|
29
|
+
|
30
|
+
mutex.synchronize do
|
31
|
+
promise = context.register_command(correlation_id, command, handler)
|
32
|
+
# puts "Sending #{command} command with correlation id #{correlation_id}"
|
33
|
+
# puts "Sending to socket: #{frame.length} #{frame.inspect}"
|
34
|
+
to_write = frame.to_s
|
35
|
+
socket.write(to_write)
|
36
|
+
promise
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength
|
43
|
+
def on_received_package(command, message, uuid, _flags)
|
44
|
+
# p(fn: "on_received_package", command: command)
|
45
|
+
# callback = context.received_package(uuid, command, message)
|
46
|
+
case command
|
47
|
+
when 'Pong' then context.fulfilled_command(uuid, 'Pong')
|
48
|
+
when 'HeartbeatRequestCommand' then send_command('HeartbeatResponseCommand')
|
49
|
+
when 'SubscriptionConfirmation' then context.fulfilled_command(uuid, decode(SubscriptionConfirmation, message))
|
50
|
+
when 'ReadStreamEventsForwardCompleted'
|
51
|
+
context.fulfilled_command(uuid, decode(ReadStreamEventsCompleted, message))
|
52
|
+
when 'StreamEventAppeared'
|
53
|
+
resolved_event = decode(StreamEventAppeared, message).event
|
54
|
+
context.trigger(uuid, 'event_appeared', resolved_event)
|
55
|
+
when 'WriteEventsCompleted' then on_write_events_completed(uuid, decode(WriteEventsCompleted, message))
|
56
|
+
else fail command
|
57
|
+
end
|
58
|
+
end
|
59
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize, 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
|
66
|
+
end
|
67
|
+
|
68
|
+
context.fulfilled_command(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
|
+
end
|
81
|
+
|
82
|
+
def socket
|
83
|
+
@socket || connect
|
84
|
+
end
|
85
|
+
|
86
|
+
def connect
|
87
|
+
@socket = TCPSocket.open(host, port)
|
88
|
+
Thread.new do
|
89
|
+
process_downstream
|
90
|
+
end
|
91
|
+
@socket
|
92
|
+
rescue TimeoutError, Errno::ECONNREFUSED, Errno::EHOSTDOWN,
|
93
|
+
Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ETIMEDOUT
|
94
|
+
raise CannotConnectError, "Error connecting to Eventstore on #{host.inspect}:#{port.inspect} (#{$ERROR_INFO.class})"
|
95
|
+
end
|
96
|
+
|
97
|
+
def process_downstream
|
98
|
+
loop do
|
99
|
+
buffer << socket.sysread(4096)
|
100
|
+
end
|
101
|
+
rescue IOError, EOFError
|
102
|
+
on_disconnect
|
103
|
+
rescue => error
|
104
|
+
on_exception(error)
|
105
|
+
end
|
106
|
+
|
107
|
+
def on_disconnect
|
108
|
+
return if @terminating
|
109
|
+
puts 'Eventstore disconnected'
|
110
|
+
context.on_error(DisconnectionError.new('Eventstore disconnected'))
|
111
|
+
end
|
112
|
+
|
113
|
+
def on_exception(error)
|
114
|
+
puts "process_downstream_error #{error.inspect}"
|
115
|
+
context.on_error(error)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|