estore 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Join the chat at https://gitter.im/mathieuravaux/eventstore-ruby](https://badges.gitter.im/Join%20Chat.svg)](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
|
+
[![Circle CI](https://circleci.com/gh/mathieuravaux/eventstore-ruby.svg?style=svg)](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
|