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
@@ -0,0 +1,31 @@
|
|
1
|
+
class Eventstore
|
2
|
+
# Package is a length-prefixed binary frame transferred over TCP
|
3
|
+
class Package
|
4
|
+
def self.encode(code, correlation_id, msg)
|
5
|
+
command = Beefcake::Buffer.new
|
6
|
+
command << code
|
7
|
+
command << 0x0 # non authenticated
|
8
|
+
uuid_bytes = encode_uuid(correlation_id)
|
9
|
+
uuid_bytes.each_byte { |b| command << b }
|
10
|
+
msg.encode(command) if msg
|
11
|
+
|
12
|
+
prefix_with_length(command)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.prefix_with_length(command)
|
16
|
+
package = Beefcake::Buffer.new
|
17
|
+
package.append_fixed32(command.length)
|
18
|
+
package << command
|
19
|
+
package
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.encode_uuid(uuid)
|
23
|
+
uuid.scan(/[0-9a-f]{4}/).map { |x| x.to_i(16) }.pack('n*')
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.parse_uuid(bytes)
|
27
|
+
a, b, c, d, e, f, g, h = *bytes.unpack('n*').map { |n| n.to_s(16) }.map { |n| n.rjust(4, '0') }
|
28
|
+
[a, b, '-', c, '-', d, '-', e, '-', f, g, h].join('')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Eventstore
|
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 :eventstore, :stream, :resolve_link_tos, :id, :position
|
12
|
+
|
13
|
+
def initialize(eventstore, stream, resolve_link_tos: true)
|
14
|
+
@eventstore = eventstore
|
15
|
+
@stream = stream
|
16
|
+
@resolve_link_tos = resolve_link_tos
|
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
|
+
eventstore.unsubscribe_from_stream(id) if id
|
33
|
+
@id = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def subscribe
|
39
|
+
prom = eventstore.subscribe_to_stream(self, stream, 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
|
data/spec/db/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventstore do
|
4
|
+
let(:es) { Eventstore.new('localhost', 1113) }
|
5
|
+
subject { new_estore }
|
6
|
+
let(:injector) { new_estore }
|
7
|
+
|
8
|
+
def new_estore
|
9
|
+
es = Eventstore.new('localhost', 1113)
|
10
|
+
es.on_error { |error| Thread.main.raise(error) }
|
11
|
+
es
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'supports the PING command' do
|
15
|
+
Timeout.timeout(1) do
|
16
|
+
promise = es.ping
|
17
|
+
result = promise.sync
|
18
|
+
expect(result).to eql 'Pong'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def inject_event(stream)
|
23
|
+
event_type = 'TestEvent'
|
24
|
+
data = JSON.generate(at: Time.now.to_i, foo: 'bar')
|
25
|
+
event = injector.new_event(event_type, data)
|
26
|
+
# puts ">#{stream}\t\t#{event.inspect}"
|
27
|
+
prom = injector.write_events(stream, event)
|
28
|
+
prom.sync
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'dumps the content of the outlet stream from the last checkpoint' do
|
32
|
+
inject_events('outlet', 50)
|
33
|
+
events = subject.read_stream_events_forward('outlet', 1, 20).sync
|
34
|
+
expect(events).to be_kind_of(Eventstore::ReadStreamEventsCompleted)
|
35
|
+
events.events.each do |event|
|
36
|
+
expect(event).to be_kind_of(Eventstore::ResolvedIndexedEvent)
|
37
|
+
JSON.parse(event.event.data)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def inject_events_async(stream, target)
|
42
|
+
Thread.new do
|
43
|
+
begin
|
44
|
+
inject_events(stream, target)
|
45
|
+
rescue => error
|
46
|
+
puts(error.inspect)
|
47
|
+
puts(*error.backtrace)
|
48
|
+
Thread.main.raise(error)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def inject_events(stream, target)
|
54
|
+
target.times do |_i|
|
55
|
+
inject_event(stream)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'allows to make a live subscription' do
|
60
|
+
stream = "catchup-test-#{SecureRandom.uuid}"
|
61
|
+
received = 0
|
62
|
+
|
63
|
+
sub = subject.new_subscription(stream)
|
64
|
+
sub.on_event { |_event| received += 1 }
|
65
|
+
sub.on_error { |error| fail(error.inspect) }
|
66
|
+
sub.start
|
67
|
+
|
68
|
+
inject_events(stream, 50)
|
69
|
+
|
70
|
+
Timeout.timeout(20) do
|
71
|
+
loop do
|
72
|
+
break if received >= 50
|
73
|
+
sleep(0.1)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'allows to make a catch-up subscription' do
|
79
|
+
stream = "catchup-test-#{SecureRandom.uuid}"
|
80
|
+
received = 0
|
81
|
+
mutex = Mutex.new
|
82
|
+
|
83
|
+
expect(subject.ping.sync).to eql 'Pong'
|
84
|
+
|
85
|
+
# puts "stream: #{stream}"
|
86
|
+
|
87
|
+
inject_events(stream, 1220)
|
88
|
+
|
89
|
+
sub = subject.new_catchup_subscription(stream, -1)
|
90
|
+
sub.on_event { |_event| mutex.synchronize { received += 1 } }
|
91
|
+
sub.on_error { |error| fail error.inspect }
|
92
|
+
sub.start
|
93
|
+
|
94
|
+
inject_events_async(stream, 780)
|
95
|
+
|
96
|
+
Timeout.timeout(10) do
|
97
|
+
loop do
|
98
|
+
break if received >= 2000
|
99
|
+
sleep(0.1)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'estore'
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
trap 'TTIN' do
|
7
|
+
Thread.list.each do |thread|
|
8
|
+
puts "Thread TID-#{thread.object_id.to_s(36)}"
|
9
|
+
puts thread.backtrace.join("\n")
|
10
|
+
puts "\n\n\n"
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,261 @@
|
|
1
|
+
package EventStore.Client.Messages;
|
2
|
+
|
3
|
+
enum OperationResult
|
4
|
+
{
|
5
|
+
Success = 0;
|
6
|
+
PrepareTimeout = 1;
|
7
|
+
CommitTimeout = 2;
|
8
|
+
ForwardTimeout = 3;
|
9
|
+
WrongExpectedVersion = 4;
|
10
|
+
StreamDeleted = 5;
|
11
|
+
InvalidTransaction = 6;
|
12
|
+
AccessDenied = 7;
|
13
|
+
}
|
14
|
+
|
15
|
+
message NewEvent {
|
16
|
+
required bytes event_id = 1;
|
17
|
+
required string event_type = 2;
|
18
|
+
required int32 data_content_type = 3;
|
19
|
+
required int32 metadata_content_type = 4;
|
20
|
+
required bytes data = 5;
|
21
|
+
optional bytes metadata = 6;
|
22
|
+
}
|
23
|
+
|
24
|
+
message EventRecord {
|
25
|
+
required string event_stream_id = 1;
|
26
|
+
required int32 event_number = 2;
|
27
|
+
required bytes event_id = 3;
|
28
|
+
required string event_type = 4;
|
29
|
+
required int32 data_content_type = 5;
|
30
|
+
required int32 metadata_content_type = 6;
|
31
|
+
required bytes data = 7;
|
32
|
+
optional bytes metadata = 8;
|
33
|
+
optional int64 created = 9;
|
34
|
+
optional int64 created_epoch = 10;
|
35
|
+
}
|
36
|
+
|
37
|
+
message ResolvedIndexedEvent {
|
38
|
+
required EventRecord event = 1;
|
39
|
+
optional EventRecord link = 2;
|
40
|
+
}
|
41
|
+
|
42
|
+
message ResolvedEvent {
|
43
|
+
required EventRecord event = 1;
|
44
|
+
optional EventRecord link = 2;
|
45
|
+
required int64 commit_position = 3;
|
46
|
+
required int64 prepare_position = 4;
|
47
|
+
}
|
48
|
+
|
49
|
+
message WriteEvents {
|
50
|
+
required string event_stream_id = 1;
|
51
|
+
required int32 expected_version = 2;
|
52
|
+
repeated NewEvent events = 3;
|
53
|
+
required bool require_master = 4;
|
54
|
+
}
|
55
|
+
|
56
|
+
message WriteEventsCompleted {
|
57
|
+
required OperationResult result = 1;
|
58
|
+
optional string message = 2;
|
59
|
+
required int32 first_event_number = 3;
|
60
|
+
required int32 last_event_number = 4;
|
61
|
+
optional int64 prepare_position = 5;
|
62
|
+
optional int64 commit_position = 6;
|
63
|
+
}
|
64
|
+
|
65
|
+
message DeleteStream {
|
66
|
+
required string event_stream_id = 1;
|
67
|
+
required int32 expected_version = 2;
|
68
|
+
required bool require_master = 3;
|
69
|
+
optional bool hard_delete = 4;
|
70
|
+
}
|
71
|
+
|
72
|
+
message DeleteStreamCompleted {
|
73
|
+
required OperationResult result = 1;
|
74
|
+
optional string message = 2;
|
75
|
+
optional int64 prepare_position = 3;
|
76
|
+
optional int64 commit_position = 4;
|
77
|
+
}
|
78
|
+
|
79
|
+
message TransactionStart {
|
80
|
+
required string event_stream_id = 1;
|
81
|
+
required int32 expected_version = 2;
|
82
|
+
required bool require_master = 3;
|
83
|
+
}
|
84
|
+
|
85
|
+
message TransactionStartCompleted {
|
86
|
+
required int64 transaction_id = 1;
|
87
|
+
required OperationResult result = 2;
|
88
|
+
optional string message = 3;
|
89
|
+
}
|
90
|
+
|
91
|
+
message TransactionWrite {
|
92
|
+
required int64 transaction_id = 1;
|
93
|
+
repeated NewEvent events = 2;
|
94
|
+
required bool require_master = 3;
|
95
|
+
}
|
96
|
+
|
97
|
+
message TransactionWriteCompleted {
|
98
|
+
required int64 transaction_id = 1;
|
99
|
+
required OperationResult result = 2;
|
100
|
+
optional string message = 3;
|
101
|
+
}
|
102
|
+
|
103
|
+
message TransactionCommit {
|
104
|
+
required int64 transaction_id = 1;
|
105
|
+
required bool require_master = 2;
|
106
|
+
}
|
107
|
+
|
108
|
+
message TransactionCommitCompleted {
|
109
|
+
required int64 transaction_id = 1;
|
110
|
+
required OperationResult result = 2;
|
111
|
+
optional string message = 3;
|
112
|
+
required int32 first_event_number = 4;
|
113
|
+
required int32 last_event_number = 5;
|
114
|
+
optional int64 prepare_position = 6;
|
115
|
+
optional int64 commit_position = 7;
|
116
|
+
}
|
117
|
+
|
118
|
+
message ReadEvent {
|
119
|
+
required string event_stream_id = 1;
|
120
|
+
required int32 event_number = 2;
|
121
|
+
required bool resolve_link_tos = 3;
|
122
|
+
required bool require_master = 4;
|
123
|
+
}
|
124
|
+
|
125
|
+
message ReadEventCompleted {
|
126
|
+
|
127
|
+
enum ReadEventResult {
|
128
|
+
Success = 0;
|
129
|
+
NotFound = 1;
|
130
|
+
NoStream = 2;
|
131
|
+
StreamDeleted = 3;
|
132
|
+
Error = 4;
|
133
|
+
AccessDenied = 5;
|
134
|
+
}
|
135
|
+
|
136
|
+
required ReadEventResult result = 1;
|
137
|
+
required ResolvedIndexedEvent event = 2;
|
138
|
+
|
139
|
+
optional string error = 3;
|
140
|
+
}
|
141
|
+
|
142
|
+
message ReadStreamEvents {
|
143
|
+
required string event_stream_id = 1;
|
144
|
+
required int32 from_event_number = 2;
|
145
|
+
required int32 max_count = 3;
|
146
|
+
required bool resolve_link_tos = 4;
|
147
|
+
required bool require_master = 5;
|
148
|
+
}
|
149
|
+
|
150
|
+
message ReadStreamEventsCompleted {
|
151
|
+
|
152
|
+
enum ReadStreamResult {
|
153
|
+
Success = 0;
|
154
|
+
NoStream = 1;
|
155
|
+
StreamDeleted = 2;
|
156
|
+
NotModified = 3;
|
157
|
+
Error = 4;
|
158
|
+
AccessDenied = 5;
|
159
|
+
}
|
160
|
+
|
161
|
+
repeated ResolvedIndexedEvent events = 1;
|
162
|
+
required ReadStreamResult result = 2;
|
163
|
+
required int32 next_event_number = 3;
|
164
|
+
required int32 last_event_number = 4;
|
165
|
+
required bool is_end_of_stream = 5;
|
166
|
+
required int64 last_commit_position = 6;
|
167
|
+
|
168
|
+
optional string error = 7;
|
169
|
+
}
|
170
|
+
|
171
|
+
message ReadAllEvents {
|
172
|
+
required int64 commit_position = 1;
|
173
|
+
required int64 prepare_position = 2;
|
174
|
+
required int32 max_count = 3;
|
175
|
+
required bool resolve_link_tos = 4;
|
176
|
+
required bool require_master = 5;
|
177
|
+
}
|
178
|
+
|
179
|
+
message ReadAllEventsCompleted {
|
180
|
+
|
181
|
+
enum ReadAllResult {
|
182
|
+
Success = 0;
|
183
|
+
NotModified = 1;
|
184
|
+
Error = 2;
|
185
|
+
AccessDenied = 3;
|
186
|
+
}
|
187
|
+
|
188
|
+
required int64 commit_position = 1;
|
189
|
+
required int64 prepare_position = 2;
|
190
|
+
repeated ResolvedEvent events = 3;
|
191
|
+
required int64 next_commit_position = 4;
|
192
|
+
required int64 next_prepare_position = 5;
|
193
|
+
|
194
|
+
optional ReadAllResult result = 6 [default = Success];
|
195
|
+
optional string error = 7;
|
196
|
+
}
|
197
|
+
|
198
|
+
message SubscribeToStream {
|
199
|
+
required string event_stream_id = 1;
|
200
|
+
required bool resolve_link_tos = 2;
|
201
|
+
}
|
202
|
+
|
203
|
+
message SubscriptionConfirmation {
|
204
|
+
required int64 last_commit_position = 1;
|
205
|
+
optional int32 last_event_number = 2;
|
206
|
+
}
|
207
|
+
|
208
|
+
message StreamEventAppeared {
|
209
|
+
required ResolvedEvent event = 1;
|
210
|
+
}
|
211
|
+
|
212
|
+
message UnsubscribeFromStream {
|
213
|
+
}
|
214
|
+
|
215
|
+
message SubscriptionDropped {
|
216
|
+
|
217
|
+
enum SubscriptionDropReason {
|
218
|
+
Unsubscribed = 0;
|
219
|
+
AccessDenied = 1;
|
220
|
+
}
|
221
|
+
|
222
|
+
optional SubscriptionDropReason reason = 1 [default = Unsubscribed];
|
223
|
+
}
|
224
|
+
|
225
|
+
message NotHandled {
|
226
|
+
|
227
|
+
enum NotHandledReason {
|
228
|
+
NotReady = 0;
|
229
|
+
TooBusy = 1;
|
230
|
+
NotMaster = 2;
|
231
|
+
}
|
232
|
+
|
233
|
+
required NotHandledReason reason = 1;
|
234
|
+
optional bytes additional_info = 2;
|
235
|
+
|
236
|
+
message MasterInfo {
|
237
|
+
required string external_tcp_address = 1;
|
238
|
+
required int32 external_tcp_port = 2;
|
239
|
+
required string external_http_address = 3;
|
240
|
+
required int32 external_http_port = 4;
|
241
|
+
optional string external_secure_tcp_address = 5;
|
242
|
+
optional int32 external_secure_tcp_port = 6;
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
message ScavengeDatabase {
|
247
|
+
}
|
248
|
+
|
249
|
+
message ScavengeDatabaseCompleted {
|
250
|
+
|
251
|
+
enum ScavengeResult {
|
252
|
+
Success = 0;
|
253
|
+
InProgress = 1;
|
254
|
+
Failed = 2;
|
255
|
+
}
|
256
|
+
|
257
|
+
required ScavengeResult result = 1;
|
258
|
+
optional string error = 2;
|
259
|
+
required int32 total_time_ms = 3;
|
260
|
+
required int64 total_space_saved = 4;
|
261
|
+
}
|