estore 0.0.1

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