event_sourcery-postgres 0.9.1 → 1.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 +4 -4
- data/CHANGELOG.md +27 -1
- data/README.md +2 -4
- data/event_sourcery-postgres.gemspec +18 -12
- data/lib/event_sourcery/postgres/config.rb +3 -0
- data/lib/event_sourcery/postgres/event_store.rb +15 -9
- data/lib/event_sourcery/postgres/optimised_event_poll_waiter.rb +4 -2
- data/lib/event_sourcery/postgres/projector.rb +4 -0
- data/lib/event_sourcery/postgres/queue_with_interval_callback.rb +11 -2
- data/lib/event_sourcery/postgres/reactor.rb +14 -9
- data/lib/event_sourcery/postgres/schema.rb +100 -91
- data/lib/event_sourcery/postgres/table_owner.rb +7 -5
- data/lib/event_sourcery/postgres/tracker.rb +21 -21
- data/lib/event_sourcery/postgres/version.rb +3 -1
- data/lib/event_sourcery/postgres.rb +3 -0
- metadata +47 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0257d9a0f70fe078196251fc6de2944278cb2ddf7dda70e2125ce1217d62af6
|
|
4
|
+
data.tar.gz: 4ff91be144b82ec02988d7993a7094618e002ffce96ec7dfde1cde054fb15668
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5cd37ea3284ab95e970e704bbabdb14c2160dd3a7612a48927fc99e15a72b074d8c704c0239a75256a158bf468df93c110dcaa0d0ce46da4f871ceb1fd0390c9
|
|
7
|
+
data.tar.gz: d9e5f9281bcc91bb364e1dd607065199b97f144118841183807ce78a3a7a0ab4997fc6e12a625d0f15aaf3b15076d769a03b56362e47201906539ed29d8fefb4
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
[Unreleased]: https://github.com/envato/event_sourcery-postgres/compare/v1.0.1...HEAD
|
|
11
|
+
|
|
12
|
+
## [1.0.1] - 2026-01-17
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Resolve issues as identified by RuboCop ([#85]).
|
|
17
|
+
|
|
18
|
+
[1.0.0]: https://github.com/envato/event_sourcery-postgres/compare/v1.0.0...v1.0.1
|
|
19
|
+
[#85]: https://github.com/envato/event_sourcery-postgres/pull/85
|
|
20
|
+
|
|
21
|
+
## [1.0.0] - 2025-12-28
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Resolve issues as identified by RuboCop ([#78], [#82], [#83]).
|
|
26
|
+
- Minor fixups in gem metadata ([#79]).
|
|
27
|
+
- Remove support for older Ruby versions: Ruby 2.6 or greater is now required ([#80]).
|
|
28
|
+
|
|
29
|
+
[1.0.0]: https://github.com/envato/event_sourcery-postgres/compare/v0.9.1...v1.0.0
|
|
30
|
+
[#78]: https://github.com/envato/event_sourcery-postgres/pull/78
|
|
31
|
+
[#79]: https://github.com/envato/event_sourcery-postgres/pull/79
|
|
32
|
+
[#80]: https://github.com/envato/event_sourcery-postgres/pull/80
|
|
33
|
+
[#82]: https://github.com/envato/event_sourcery-postgres/pull/82
|
|
34
|
+
[#83]: https://github.com/envato/event_sourcery-postgres/pull/83
|
|
35
|
+
|
|
9
36
|
## [0.9.1] - 2022-01-20
|
|
10
37
|
|
|
11
38
|
### Changed
|
|
@@ -117,7 +144,6 @@ or when the loop stops
|
|
|
117
144
|
- Postgres related configuration is through `EventSourcery::Postgres.configure`
|
|
118
145
|
instead of `EventSourcery.configure`.
|
|
119
146
|
|
|
120
|
-
[Unreleased]: https://github.com/envato/event_sourcery-postgres/compare/v0.9.1...HEAD
|
|
121
147
|
[0.9.1]: https://github.com/envato/event_sourcery-postgres/compare/v0.9.0...v0.9.1
|
|
122
148
|
[0.9.0]: https://github.com/envato/event_sourcery-postgres/compare/v0.8.1...v0.9.0
|
|
123
149
|
[0.8.1]: https://github.com/envato/event_sourcery-postgres/compare/v0.8.0...v0.8.1
|
data/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# EventSourcery::Postgres
|
|
2
2
|
|
|
3
|
-
[](https://github.com/envato/event_sourcery-postgres/actions/workflows/test.yml)
|
|
4
4
|
|
|
5
5
|
## Development Status
|
|
6
6
|
|
|
7
|
-
EventSourcery is
|
|
8
|
-
haven't finalized the API yet and things are still moving rapidly. Until we
|
|
9
|
-
release a 1.0 things may change without first being deprecated.
|
|
7
|
+
EventSourcery::Postgres is in production use at [Envato](http://envato.com).
|
|
10
8
|
|
|
11
9
|
## Installation
|
|
12
10
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
5
|
require 'event_sourcery/postgres/version'
|
|
5
6
|
|
|
@@ -12,27 +13,32 @@ Gem::Specification.new do |spec|
|
|
|
12
13
|
|
|
13
14
|
spec.summary = 'Postgres event store for use with EventSourcery'
|
|
14
15
|
spec.homepage = 'https://github.com/envato/event_sourcery-postgres'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
|
|
15
18
|
spec.metadata = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
'allowed_push_host' => 'https://rubygems.org',
|
|
20
|
+
'bug_tracker_uri' => "#{spec.homepage}/issues",
|
|
21
|
+
'changelog_uri' => "#{spec.homepage}/blob/HEAD/CHANGELOG.md",
|
|
22
|
+
'documentation_uri' => "https://www.rubydoc.info/gems/#{spec.name}/#{spec.version}",
|
|
23
|
+
'source_code_uri' => "#{spec.homepage}/tree/v#{spec.version}"
|
|
24
|
+
}
|
|
20
25
|
|
|
21
|
-
spec.files
|
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
22
27
|
f.match(%r{^(\.|bin/|Gemfile|Rakefile|script/|spec/)})
|
|
23
28
|
end
|
|
24
29
|
spec.bindir = 'exe'
|
|
25
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
26
31
|
spec.require_paths = ['lib']
|
|
27
32
|
|
|
28
|
-
spec.required_ruby_version = '>= 2.
|
|
33
|
+
spec.required_ruby_version = '>= 2.6.0'
|
|
29
34
|
|
|
30
|
-
spec.add_dependency 'sequel', '>= 4.38'
|
|
31
|
-
spec.add_dependency 'pg'
|
|
32
35
|
spec.add_dependency 'event_sourcery', '>= 0.14.0'
|
|
36
|
+
spec.add_dependency 'pg'
|
|
37
|
+
spec.add_dependency 'sequel', '>= 4.38'
|
|
38
|
+
spec.add_development_dependency 'benchmark-ips'
|
|
33
39
|
spec.add_development_dependency 'bundler'
|
|
40
|
+
spec.add_development_dependency 'pry'
|
|
34
41
|
spec.add_development_dependency 'rake', '~> 13'
|
|
35
42
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
36
|
-
spec.add_development_dependency '
|
|
37
|
-
spec.add_development_dependency 'benchmark-ips'
|
|
43
|
+
spec.add_development_dependency 'rubocop', '~> 1'
|
|
38
44
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
5
|
+
# PostgreSQL implementation of an event store for persisting and retrieving domain events.
|
|
3
6
|
class EventStore
|
|
4
7
|
include EventSourcery::EventStore::EachByRange
|
|
5
8
|
|
|
@@ -32,6 +35,7 @@ module EventSourcery
|
|
|
32
35
|
events = Array(event_or_events)
|
|
33
36
|
aggregate_ids = events.map(&:aggregate_id).uniq
|
|
34
37
|
raise AtomicWriteToMultipleAggregatesNotSupported unless aggregate_ids.count == 1
|
|
38
|
+
|
|
35
39
|
sql = write_events_sql(aggregate_ids.first, events, expected_version)
|
|
36
40
|
@db_connection.run(sql)
|
|
37
41
|
log_events_saved(events)
|
|
@@ -40,9 +44,9 @@ module EventSourcery
|
|
|
40
44
|
rescue Sequel::DatabaseError => e
|
|
41
45
|
if e.message =~ /Concurrency conflict/
|
|
42
46
|
raise ConcurrencyError, "expected version was not #{expected_version}. Error: #{e.message}"
|
|
43
|
-
else
|
|
44
|
-
raise
|
|
45
47
|
end
|
|
48
|
+
|
|
49
|
+
raise
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
# Get the next set of events from the given event id. You can
|
|
@@ -55,10 +59,11 @@ module EventSourcery
|
|
|
55
59
|
#
|
|
56
60
|
# @return [Array] array of found events
|
|
57
61
|
def get_next_from(id, event_types: nil, limit: 1000)
|
|
58
|
-
query =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
query =
|
|
63
|
+
events_table
|
|
64
|
+
.order(:id)
|
|
65
|
+
.where(Sequel.lit('id >= ?', id))
|
|
66
|
+
.limit(limit)
|
|
62
67
|
query = query.where(type: event_types) if event_types
|
|
63
68
|
query.map { |event_row| build_event(event_row) }
|
|
64
69
|
end
|
|
@@ -96,7 +101,7 @@ module EventSourcery
|
|
|
96
101
|
# @param event_types the event_types to subscribe to, default all.
|
|
97
102
|
# @param after_listen the after listen call back block. default nil.
|
|
98
103
|
# @param subscription_master the subscription master block
|
|
99
|
-
def subscribe(from_id:, event_types: nil, after_listen: nil,
|
|
104
|
+
def subscribe(from_id:, subscription_master:, event_types: nil, after_listen: nil, &block)
|
|
100
105
|
poll_waiter = OptimisedEventPollWaiter.new(db_connection: @db_connection, after_listen: after_listen)
|
|
101
106
|
args = {
|
|
102
107
|
poll_waiter: poll_waiter,
|
|
@@ -157,9 +162,10 @@ module EventSourcery
|
|
|
157
162
|
|
|
158
163
|
def to_sql_literal(value)
|
|
159
164
|
return 'null' unless value
|
|
160
|
-
|
|
165
|
+
|
|
166
|
+
wrapped_value = if value.is_a?(Time)
|
|
161
167
|
value.iso8601(6)
|
|
162
|
-
elsif Hash
|
|
168
|
+
elsif value.is_a?(Hash)
|
|
163
169
|
Sequel.pg_json(value)
|
|
164
170
|
else
|
|
165
171
|
value
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
3
5
|
# Optimise poll interval with Postgres listen/notify
|
|
@@ -11,7 +13,7 @@ module EventSourcery
|
|
|
11
13
|
@after_listen = after_listen
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
def poll(after_listen: proc {
|
|
16
|
+
def poll(after_listen: proc {}, &block)
|
|
15
17
|
@events_queue.callback = proc do
|
|
16
18
|
ensure_listen_thread_alive!
|
|
17
19
|
block.call
|
|
@@ -52,7 +54,7 @@ module EventSourcery
|
|
|
52
54
|
after_listen_callback = if after_listen
|
|
53
55
|
proc do
|
|
54
56
|
after_listen.call
|
|
55
|
-
@after_listen
|
|
57
|
+
@after_listen&.call
|
|
56
58
|
end
|
|
57
59
|
else
|
|
58
60
|
@after_listen
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
5
|
+
# Mixin providing projection capabilities for processing events into read models.
|
|
3
6
|
module Projector
|
|
4
7
|
def self.included(base)
|
|
5
8
|
base.include(EventProcessing::EventStreamProcessor)
|
|
@@ -15,6 +18,7 @@ module EventSourcery
|
|
|
15
18
|
end
|
|
16
19
|
end
|
|
17
20
|
|
|
21
|
+
# Instance methods for projector event processing and tracking.
|
|
18
22
|
module InstanceMethods
|
|
19
23
|
def initialize(tracker: EventSourcery::Postgres.config.event_tracker,
|
|
20
24
|
db_connection: EventSourcery::Postgres.config.projections_database,
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
5
|
+
# Queue that invokes a callback at regular intervals when no items are available.
|
|
3
6
|
class QueueWithIntervalCallback < ::Queue
|
|
4
7
|
attr_accessor :callback
|
|
5
8
|
|
|
6
|
-
def initialize(
|
|
9
|
+
def initialize(
|
|
10
|
+
callback: proc {},
|
|
11
|
+
callback_interval: EventSourcery::Postgres.config.callback_interval_if_no_new_events,
|
|
12
|
+
poll_interval: 0.1
|
|
13
|
+
)
|
|
7
14
|
@callback = callback
|
|
8
15
|
@callback_interval = callback_interval
|
|
9
16
|
@poll_interval = poll_interval
|
|
10
17
|
super()
|
|
11
18
|
end
|
|
12
19
|
|
|
13
|
-
def pop(non_block_without_callback = false)
|
|
20
|
+
def pop(non_block_without_callback = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
14
21
|
return super if non_block_without_callback
|
|
22
|
+
|
|
15
23
|
pop_with_interval_callback
|
|
16
24
|
end
|
|
17
25
|
|
|
@@ -21,6 +29,7 @@ module EventSourcery
|
|
|
21
29
|
time = Time.now
|
|
22
30
|
loop do
|
|
23
31
|
return pop(true) unless empty?
|
|
32
|
+
|
|
24
33
|
if @callback_interval && Time.now > time + @callback_interval
|
|
25
34
|
@callback.call
|
|
26
35
|
time = Time.now
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
5
|
+
# Mixin providing reactor capabilities for processing events and emitting new events in response.
|
|
3
6
|
module Reactor
|
|
4
7
|
UndeclaredEventEmissionError = Class.new(StandardError)
|
|
5
8
|
|
|
@@ -10,6 +13,7 @@ module EventSourcery
|
|
|
10
13
|
base.include(InstanceMethods)
|
|
11
14
|
end
|
|
12
15
|
|
|
16
|
+
# Class methods for declaring and querying emitted event types.
|
|
13
17
|
module ClassMethods
|
|
14
18
|
# Assign the types of events this reactor can emit.
|
|
15
19
|
#
|
|
@@ -20,7 +24,7 @@ module EventSourcery
|
|
|
20
24
|
|
|
21
25
|
# @return [Array] an array of the types of events this reactor can emit
|
|
22
26
|
def emit_events
|
|
23
|
-
@emits_event_types ||= []
|
|
27
|
+
@emits_event_types ||= [] # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
# This will tell you if this reactor emits any type of event.
|
|
@@ -39,6 +43,7 @@ module EventSourcery
|
|
|
39
43
|
end
|
|
40
44
|
end
|
|
41
45
|
|
|
46
|
+
# Instance methods for reactor initialisation and event emission.
|
|
42
47
|
module InstanceMethods
|
|
43
48
|
def initialize(tracker: EventSourcery::Postgres.config.event_tracker,
|
|
44
49
|
db_connection: EventSourcery::Postgres.config.projections_database,
|
|
@@ -48,11 +53,10 @@ module EventSourcery
|
|
|
48
53
|
@event_source = event_source
|
|
49
54
|
@event_sink = event_sink
|
|
50
55
|
@db_connection = db_connection
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end
|
|
56
|
+
return unless self.class.emits_events?
|
|
57
|
+
return unless event_sink.nil? || event_source.nil?
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, 'An event sink and source is required for processors that emit events'
|
|
56
60
|
end
|
|
57
61
|
end
|
|
58
62
|
|
|
@@ -61,19 +65,20 @@ module EventSourcery
|
|
|
61
65
|
attr_reader :event_sink, :event_source
|
|
62
66
|
|
|
63
67
|
def emit_event(event_or_hash, &block)
|
|
64
|
-
event = if Event
|
|
68
|
+
event = if event_or_hash.is_a?(Event)
|
|
65
69
|
event_or_hash
|
|
66
70
|
else
|
|
67
71
|
Event.new(event_or_hash)
|
|
68
72
|
end
|
|
69
73
|
raise UndeclaredEventEmissionError unless self.class.emits_event?(event.class)
|
|
74
|
+
|
|
70
75
|
event = event.with(causation_id: _event.uuid, correlation_id: _event.correlation_id)
|
|
71
76
|
invoke_action_and_emit_event(event, block)
|
|
72
|
-
EventSourcery.logger.debug { "[#{
|
|
77
|
+
EventSourcery.logger.debug { "[#{processor_name}] Emitted event: #{event.inspect}" }
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
def invoke_action_and_emit_event(event, action)
|
|
76
|
-
action
|
|
81
|
+
action&.call(event.body)
|
|
77
82
|
event_sink.sink(event)
|
|
78
83
|
end
|
|
79
84
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
5
|
+
# Schema management for creating event store and projector tables in PostgreSQL.
|
|
3
6
|
module Schema
|
|
4
7
|
module_function
|
|
5
8
|
|
|
@@ -15,7 +18,12 @@ module EventSourcery
|
|
|
15
18
|
write_events_function_name: EventSourcery::Postgres.config.write_events_function_name)
|
|
16
19
|
create_events(db: db, table_name: events_table_name)
|
|
17
20
|
create_aggregates(db: db, table_name: aggregates_table_name)
|
|
18
|
-
create_or_update_functions(
|
|
21
|
+
create_or_update_functions(
|
|
22
|
+
db: db,
|
|
23
|
+
events_table_name: events_table_name,
|
|
24
|
+
function_name: write_events_function_name,
|
|
25
|
+
aggregates_table_name: aggregates_table_name
|
|
26
|
+
)
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
# Create the events table. Needs the database and the table name.
|
|
@@ -35,8 +43,9 @@ module EventSourcery
|
|
|
35
43
|
column :version, :bigint, null: false
|
|
36
44
|
column :correlation_id, :uuid
|
|
37
45
|
column :causation_id, :uuid
|
|
38
|
-
column :created_at, :'timestamp without time zone', null: false,
|
|
39
|
-
|
|
46
|
+
column :created_at, :'timestamp without time zone', null: false,
|
|
47
|
+
default: Sequel.lit("(now() at time zone 'utc')")
|
|
48
|
+
index %i[aggregate_id version], unique: true
|
|
40
49
|
index :uuid, unique: true
|
|
41
50
|
index :type
|
|
42
51
|
index :correlation_id
|
|
@@ -70,96 +79,96 @@ module EventSourcery
|
|
|
70
79
|
function_name: EventSourcery::Postgres.config.write_events_function_name,
|
|
71
80
|
events_table_name: EventSourcery::Postgres.config.events_table_name,
|
|
72
81
|
aggregates_table_name: EventSourcery::Postgres.config.aggregates_table_name)
|
|
73
|
-
db.run
|
|
74
|
-
create or replace function #{function_name}(_aggregateId uuid,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
declare
|
|
84
|
-
currentVersion int;
|
|
85
|
-
body json;
|
|
86
|
-
eventVersion int;
|
|
87
|
-
eventId text;
|
|
88
|
-
index int;
|
|
89
|
-
newVersion int;
|
|
90
|
-
numEvents int;
|
|
91
|
-
createdAt timestamp without time zone;
|
|
92
|
-
begin
|
|
93
|
-
numEvents := array_length(_bodies, 1);
|
|
94
|
-
select version into currentVersion from #{aggregates_table_name} where aggregate_id = _aggregateId;
|
|
95
|
-
if not found then
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
else
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end if;
|
|
121
|
-
index := 1;
|
|
122
|
-
eventVersion := currentVersion + 1;
|
|
123
|
-
if _lockTable then
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
end if;
|
|
133
|
-
foreach body IN ARRAY(_bodies)
|
|
134
|
-
loop
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
82
|
+
db.run <<~SQL
|
|
83
|
+
create or replace function #{function_name}(_aggregateId uuid,
|
|
84
|
+
_eventTypes varchar[],
|
|
85
|
+
_expectedVersion int,
|
|
86
|
+
_bodies json[],
|
|
87
|
+
_createdAtTimes timestamp without time zone[],
|
|
88
|
+
_eventUUIDs uuid[],
|
|
89
|
+
_correlationIds uuid[],
|
|
90
|
+
_causationIds uuid[],
|
|
91
|
+
_lockTable boolean) returns void as $$
|
|
92
|
+
declare
|
|
93
|
+
currentVersion int;
|
|
94
|
+
body json;
|
|
95
|
+
eventVersion int;
|
|
96
|
+
eventId text;
|
|
97
|
+
index int;
|
|
98
|
+
newVersion int;
|
|
99
|
+
numEvents int;
|
|
100
|
+
createdAt timestamp without time zone;
|
|
101
|
+
begin
|
|
102
|
+
numEvents := array_length(_bodies, 1);
|
|
103
|
+
select version into currentVersion from #{aggregates_table_name} where aggregate_id = _aggregateId;
|
|
104
|
+
if not found then
|
|
105
|
+
-- when we have no existing version for this aggregate
|
|
106
|
+
if _expectedVersion = 0 or _expectedVersion is null then
|
|
107
|
+
-- set the version to 1 if expected version is null or 0
|
|
108
|
+
insert into #{aggregates_table_name}(aggregate_id, version) values(_aggregateId, numEvents);
|
|
109
|
+
currentVersion := 0;
|
|
110
|
+
else
|
|
111
|
+
raise 'Concurrency conflict. Current version: 0, expected version: %', _expectedVersion;
|
|
112
|
+
end if;
|
|
113
|
+
else
|
|
114
|
+
if _expectedVersion is null then
|
|
115
|
+
-- automatically increment the version
|
|
116
|
+
update #{aggregates_table_name} set version = version + numEvents where aggregate_id = _aggregateId returning version into newVersion;
|
|
117
|
+
currentVersion := newVersion - numEvents;
|
|
118
|
+
else
|
|
119
|
+
-- increment the version if it's at our expected version
|
|
120
|
+
update #{aggregates_table_name} set version = version + numEvents where aggregate_id = _aggregateId and version = _expectedVersion;
|
|
121
|
+
if not found then
|
|
122
|
+
-- version was not at expected_version, raise an error.
|
|
123
|
+
-- currentVersion may not equal what it did in the database when the
|
|
124
|
+
-- above update statement is executed (it may have been incremented by another
|
|
125
|
+
-- process)
|
|
126
|
+
raise 'Concurrency conflict. Last known current version: %, expected version: %', currentVersion, _expectedVersion;
|
|
127
|
+
end if;
|
|
128
|
+
end if;
|
|
129
|
+
end if;
|
|
130
|
+
index := 1;
|
|
131
|
+
eventVersion := currentVersion + 1;
|
|
132
|
+
if _lockTable then
|
|
133
|
+
-- Ensure this transaction is the only one writing events to guarantee
|
|
134
|
+
-- linear growth of sequence IDs.
|
|
135
|
+
-- Any value that won't conflict with other advisory locks will work.
|
|
136
|
+
-- The Postgres tracker currently obtains an advisory lock using it's
|
|
137
|
+
-- integer row ID, so values 1 to the number of ESP's in the system would
|
|
138
|
+
-- be taken if the tracker is running in the same database as your
|
|
139
|
+
-- projections.
|
|
140
|
+
perform pg_advisory_xact_lock(-1);
|
|
141
|
+
end if;
|
|
142
|
+
foreach body IN ARRAY(_bodies)
|
|
143
|
+
loop
|
|
144
|
+
if _createdAtTimes[index] is not null then
|
|
145
|
+
createdAt := _createdAtTimes[index];
|
|
146
|
+
else
|
|
147
|
+
createdAt := now() at time zone 'utc';
|
|
148
|
+
end if;
|
|
140
149
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
insert into #{events_table_name}
|
|
151
|
+
(uuid, aggregate_id, type, body, version, correlation_id, causation_id, created_at)
|
|
152
|
+
values
|
|
153
|
+
(
|
|
154
|
+
_eventUUIDs[index],
|
|
155
|
+
_aggregateId,
|
|
156
|
+
_eventTypes[index],
|
|
157
|
+
body,
|
|
158
|
+
eventVersion,
|
|
159
|
+
_correlationIds[index],
|
|
160
|
+
_causationIds[index],
|
|
161
|
+
createdAt
|
|
162
|
+
)
|
|
163
|
+
returning id into eventId;
|
|
155
164
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
end loop;
|
|
159
|
-
perform pg_notify('new_event', eventId);
|
|
160
|
-
end;
|
|
161
|
-
$$ language plpgsql;
|
|
162
|
-
SQL
|
|
165
|
+
eventVersion := eventVersion + 1;
|
|
166
|
+
index := index + 1;
|
|
167
|
+
end loop;
|
|
168
|
+
perform pg_notify('new_event', eventId);
|
|
169
|
+
end;
|
|
170
|
+
$$ language plpgsql;
|
|
171
|
+
SQL
|
|
163
172
|
end
|
|
164
173
|
|
|
165
174
|
# Create the projector tracker table. Needs the database and the table name.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
5
|
+
# Mixin providing table management capabilities for projectors and reactors.
|
|
3
6
|
module TableOwner
|
|
4
7
|
DefaultTableError = Class.new(StandardError)
|
|
5
8
|
NoSuchTableError = Class.new(StandardError)
|
|
@@ -8,6 +11,7 @@ module EventSourcery
|
|
|
8
11
|
base.extend(ClassMethods)
|
|
9
12
|
end
|
|
10
13
|
|
|
14
|
+
# Class methods for defining and managing database tables.
|
|
11
15
|
module ClassMethods
|
|
12
16
|
# Hash of the tables and their corresponding blocks.
|
|
13
17
|
#
|
|
@@ -36,11 +40,9 @@ module EventSourcery
|
|
|
36
40
|
|
|
37
41
|
# Reset by dropping each table.
|
|
38
42
|
def reset
|
|
39
|
-
self.class.tables.
|
|
43
|
+
self.class.tables.each_key do |table_name|
|
|
40
44
|
prefixed_name = table_name_prefixed(table_name)
|
|
41
|
-
if @db_connection.table_exists?(prefixed_name)
|
|
42
|
-
@db_connection.drop_table(prefixed_name, cascade: true)
|
|
43
|
-
end
|
|
45
|
+
@db_connection.drop_table(prefixed_name, cascade: true) if @db_connection.table_exists?(prefixed_name)
|
|
44
46
|
end
|
|
45
47
|
super if defined?(super)
|
|
46
48
|
setup
|
|
@@ -49,7 +51,7 @@ module EventSourcery
|
|
|
49
51
|
# This will truncate all the tables and reset the tracker back to 0,
|
|
50
52
|
# done as a transaction.
|
|
51
53
|
def truncate
|
|
52
|
-
self.class.tables.
|
|
54
|
+
self.class.tables.each_key do |table_name|
|
|
53
55
|
@db_connection.transaction do
|
|
54
56
|
prefixed_name = table_name_prefixed(table_name)
|
|
55
57
|
@db_connection[prefixed_name].truncate
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module EventSourcery
|
|
2
4
|
module Postgres
|
|
3
5
|
# This will set up a persisted event id tracker for processors.
|
|
4
6
|
class Tracker
|
|
5
|
-
|
|
6
7
|
def initialize(db_connection = EventSourcery::Postgres.config.projections_database,
|
|
7
8
|
table_name: EventSourcery::Postgres.config.tracker_table_name,
|
|
8
9
|
obtain_processor_lock: true)
|
|
@@ -19,16 +20,14 @@ module EventSourcery
|
|
|
19
20
|
def setup(processor_name = nil)
|
|
20
21
|
create_table_if_not_exists if EventSourcery::Postgres.config.auto_create_projector_tracker
|
|
21
22
|
|
|
22
|
-
unless tracker_table_exists?
|
|
23
|
-
raise UnableToLockProcessorError, 'Projector tracker table does not exist'
|
|
24
|
-
end
|
|
23
|
+
raise UnableToLockProcessorError, 'Projector tracker table does not exist' unless tracker_table_exists?
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
return unless processor_name
|
|
26
|
+
|
|
27
|
+
create_track_entry_if_not_exists(processor_name)
|
|
28
|
+
return unless @obtain_processor_lock
|
|
29
|
+
|
|
30
|
+
obtain_global_lock_on_processor(processor_name)
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
# This will updated the tracker table to the given event id value
|
|
@@ -37,9 +36,9 @@ module EventSourcery
|
|
|
37
36
|
# @param processor_name the name of the processor to update
|
|
38
37
|
# @param event_id the event id number to update to
|
|
39
38
|
def processed_event(processor_name, event_id)
|
|
40
|
-
table
|
|
41
|
-
where(name: processor_name.to_s)
|
|
42
|
-
update(last_processed_event_id: event_id)
|
|
39
|
+
table
|
|
40
|
+
.where(name: processor_name.to_s)
|
|
41
|
+
.update(last_processed_event_id: event_id)
|
|
43
42
|
true
|
|
44
43
|
end
|
|
45
44
|
|
|
@@ -82,17 +81,18 @@ module EventSourcery
|
|
|
82
81
|
private
|
|
83
82
|
|
|
84
83
|
def obtain_global_lock_on_processor(processor_name)
|
|
85
|
-
lock_obtained = @db_connection.fetch("select pg_try_advisory_lock(#{@track_entry_id})")
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
lock_obtained = @db_connection.fetch("select pg_try_advisory_lock(#{@track_entry_id})")
|
|
85
|
+
.to_a.first[:pg_try_advisory_lock]
|
|
86
|
+
return unless lock_obtained == false
|
|
87
|
+
|
|
88
|
+
raise UnableToLockProcessorError, "Unable to get a lock on #{processor_name} #{@track_entry_id}"
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def create_table_if_not_exists
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
return if tracker_table_exists?
|
|
93
|
+
|
|
94
|
+
EventSourcery.logger.info { "Projector tracker missing - attempting to create 'projector_tracker' table" }
|
|
95
|
+
EventSourcery::Postgres::Schema.create_projector_tracker(db: @db_connection, table_name: @table_name)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def create_track_entry_if_not_exists(processor_name)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'sequel'
|
|
2
4
|
|
|
3
5
|
Sequel.default_timezone = :utc
|
|
@@ -15,6 +17,7 @@ require 'event_sourcery/postgres/reactor'
|
|
|
15
17
|
require 'event_sourcery/postgres/tracker'
|
|
16
18
|
|
|
17
19
|
module EventSourcery
|
|
20
|
+
# PostgreSQL adapter for EventSourcery providing event store and projection capabilities.
|
|
18
21
|
module Postgres
|
|
19
22
|
def self.configure
|
|
20
23
|
yield config
|
metadata
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: event_sourcery-postgres
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Envato
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: event_sourcery
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 0.14.0
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
25
|
+
version: 0.14.0
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: pg
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -39,19 +38,33 @@ dependencies:
|
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '0'
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
41
|
+
name: sequel
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
44
43
|
requirements:
|
|
45
44
|
- - ">="
|
|
46
45
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
46
|
+
version: '4.38'
|
|
48
47
|
type: :runtime
|
|
49
48
|
prerelease: false
|
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
50
|
requirements:
|
|
52
51
|
- - ">="
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
53
|
+
version: '4.38'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: benchmark-ips
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
55
68
|
- !ruby/object:Gem::Dependency
|
|
56
69
|
name: bundler
|
|
57
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -67,62 +80,61 @@ dependencies:
|
|
|
67
80
|
- !ruby/object:Gem::Version
|
|
68
81
|
version: '0'
|
|
69
82
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
83
|
+
name: pry
|
|
71
84
|
requirement: !ruby/object:Gem::Requirement
|
|
72
85
|
requirements:
|
|
73
|
-
- - "
|
|
86
|
+
- - ">="
|
|
74
87
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
88
|
+
version: '0'
|
|
76
89
|
type: :development
|
|
77
90
|
prerelease: false
|
|
78
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
92
|
requirements:
|
|
80
|
-
- - "
|
|
93
|
+
- - ">="
|
|
81
94
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
95
|
+
version: '0'
|
|
83
96
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
97
|
+
name: rake
|
|
85
98
|
requirement: !ruby/object:Gem::Requirement
|
|
86
99
|
requirements:
|
|
87
100
|
- - "~>"
|
|
88
101
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
102
|
+
version: '13'
|
|
90
103
|
type: :development
|
|
91
104
|
prerelease: false
|
|
92
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
106
|
requirements:
|
|
94
107
|
- - "~>"
|
|
95
108
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
109
|
+
version: '13'
|
|
97
110
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
111
|
+
name: rspec
|
|
99
112
|
requirement: !ruby/object:Gem::Requirement
|
|
100
113
|
requirements:
|
|
101
|
-
- - "
|
|
114
|
+
- - "~>"
|
|
102
115
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '0'
|
|
116
|
+
version: '3.0'
|
|
104
117
|
type: :development
|
|
105
118
|
prerelease: false
|
|
106
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
120
|
requirements:
|
|
108
|
-
- - "
|
|
121
|
+
- - "~>"
|
|
109
122
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '0'
|
|
123
|
+
version: '3.0'
|
|
111
124
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
125
|
+
name: rubocop
|
|
113
126
|
requirement: !ruby/object:Gem::Requirement
|
|
114
127
|
requirements:
|
|
115
|
-
- - "
|
|
128
|
+
- - "~>"
|
|
116
129
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '
|
|
130
|
+
version: '1'
|
|
118
131
|
type: :development
|
|
119
132
|
prerelease: false
|
|
120
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
134
|
requirements:
|
|
122
|
-
- - "
|
|
135
|
+
- - "~>"
|
|
123
136
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '
|
|
125
|
-
description:
|
|
137
|
+
version: '1'
|
|
126
138
|
email:
|
|
127
139
|
- rubygems@envato.com
|
|
128
140
|
executables: []
|
|
@@ -146,12 +158,14 @@ files:
|
|
|
146
158
|
- lib/event_sourcery/postgres/tracker.rb
|
|
147
159
|
- lib/event_sourcery/postgres/version.rb
|
|
148
160
|
homepage: https://github.com/envato/event_sourcery-postgres
|
|
149
|
-
licenses:
|
|
161
|
+
licenses:
|
|
162
|
+
- MIT
|
|
150
163
|
metadata:
|
|
164
|
+
allowed_push_host: https://rubygems.org
|
|
151
165
|
bug_tracker_uri: https://github.com/envato/event_sourcery-postgres/issues
|
|
152
166
|
changelog_uri: https://github.com/envato/event_sourcery-postgres/blob/HEAD/CHANGELOG.md
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
documentation_uri: https://www.rubydoc.info/gems/event_sourcery-postgres/1.0.1
|
|
168
|
+
source_code_uri: https://github.com/envato/event_sourcery-postgres/tree/v1.0.1
|
|
155
169
|
rdoc_options: []
|
|
156
170
|
require_paths:
|
|
157
171
|
- lib
|
|
@@ -159,15 +173,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
159
173
|
requirements:
|
|
160
174
|
- - ">="
|
|
161
175
|
- !ruby/object:Gem::Version
|
|
162
|
-
version: 2.
|
|
176
|
+
version: 2.6.0
|
|
163
177
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
178
|
requirements:
|
|
165
179
|
- - ">="
|
|
166
180
|
- !ruby/object:Gem::Version
|
|
167
181
|
version: '0'
|
|
168
182
|
requirements: []
|
|
169
|
-
rubygems_version:
|
|
170
|
-
signing_key:
|
|
183
|
+
rubygems_version: 4.0.4
|
|
171
184
|
specification_version: 4
|
|
172
185
|
summary: Postgres event store for use with EventSourcery
|
|
173
186
|
test_files: []
|