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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module EventStore
2
+ VERSION = '0.0.1'
3
+ 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
@@ -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
+ }