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 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
@@ -0,0 +1,18 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/bundle/
11
+ /.idea/
12
+ *.bundle
13
+ *.so
14
+ *.o
15
+ *.a
16
+ *.gem
17
+ mkmf.log
18
+ .rspec
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'lib/estore/messages.rb'
4
+ - 'vendor/**/*'
5
+ - 'spec/fixtures/**/*'
6
+
7
+ Metrics/LineLength:
8
+ Max: 140
9
+
10
+ # Style/Documentation:
11
+ # Enabled: false
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --no-private
2
+ --exclude lib/estore/messages.rb
data/CHANGELOG.md ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in estore.gemspec
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ notify:
2
+ webhooks:
3
+ - url: https://webhooks.gitter.im/e/e20bfa93dc2e11895805
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