sandthorn_sequel_projection 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/.autotest +3 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +130 -0
- data/Rakefile +16 -0
- data/lib/sandthorn_sequel_projection.rb +57 -0
- data/lib/sandthorn_sequel_projection/cursor.rb +39 -0
- data/lib/sandthorn_sequel_projection/errors.rb +14 -0
- data/lib/sandthorn_sequel_projection/event_handler.rb +49 -0
- data/lib/sandthorn_sequel_projection/event_handler_collection.rb +27 -0
- data/lib/sandthorn_sequel_projection/lock.rb +83 -0
- data/lib/sandthorn_sequel_projection/manifest.rb +10 -0
- data/lib/sandthorn_sequel_projection/processed_events_tracker.rb +94 -0
- data/lib/sandthorn_sequel_projection/projection.rb +65 -0
- data/lib/sandthorn_sequel_projection/runner.rb +62 -0
- data/lib/sandthorn_sequel_projection/tasks.rb +14 -0
- data/lib/sandthorn_sequel_projection/utilities.rb +5 -0
- data/lib/sandthorn_sequel_projection/utilities/core_extensions/array_wrap.rb +13 -0
- data/lib/sandthorn_sequel_projection/utilities/null_proc.rb +9 -0
- data/lib/sandthorn_sequel_projection/version.rb +3 -0
- data/sandthorn_sequel_projection.gemspec +37 -0
- data/spec/cursor_spec.rb +44 -0
- data/spec/event_handler_collection_spec.rb +39 -0
- data/spec/event_handler_spec.rb +82 -0
- data/spec/integration/projection_spec.rb +126 -0
- data/spec/lock_spec.rb +141 -0
- data/spec/processed_events_tracker_spec.rb +79 -0
- data/spec/projection_spec.rb +91 -0
- data/spec/runner_spec.rb +32 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/mock_event_store.rb +39 -0
- data/spec/test_data/event_data.json +1876 -0
- data/spec/utilities/null_proc_spec.rb +14 -0
- metadata +262 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d9e616e9522822b606892aacf8e5b123747b90c7
|
4
|
+
data.tar.gz: 1655704ac3c4573906381d6aa3186df2f3ac7a02
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8ac17d0d63990bd20f59e1bae9e79a61938796c03ea5446938a0fc2e3691d5ca4fa67f980458dd136668819a7dde801e472c3296c92bb553eff8cde279626519
|
7
|
+
data.tar.gz: 539c42725038075dc4f094d125ddf818a31cd4cc15ad3c20def9e7838ecab8e296bd964ffea61733a487ddac3a922ecdb767a3d30efe891c09fc8073225b9ba6
|
data/.autotest
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.idea
|
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
sandthorn_sequel_projection
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Lars Krantz
|
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,130 @@
|
|
1
|
+
# SandthornSequelProjection
|
2
|
+
|
3
|
+
A DSL and some convenience utilities for creating projections based on event data.
|
4
|
+
Uses the Sequel gem for storage.
|
5
|
+
|
6
|
+
Main points:
|
7
|
+
|
8
|
+
- DSL for registering event handlers that listen to filtered event streams
|
9
|
+
- Event handlers receive one event at a time
|
10
|
+
- Planned: projection manifests are used to declare dependent projections. This information can be used to execute
|
11
|
+
non-dependent projections in parallel
|
12
|
+
|
13
|
+
Requirements on event handling:
|
14
|
+
|
15
|
+
- It must be possible to define event handlers such that the execution order is defined
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'sandthorn_sequel_projection'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install sandthorn_sequel_projection
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### 1. Configure
|
34
|
+
|
35
|
+
The default configuration expects that the Sandthorn gem is present and uses it to fetch events.
|
36
|
+
|
37
|
+
SandthornSequelProjection uses Sequel to connected to a database. Configure it like this:
|
38
|
+
|
39
|
+
SandthornSequelProjection.configure do |thorn|
|
40
|
+
thorn.db_connection = Sequel.sqlite
|
41
|
+
end
|
42
|
+
|
43
|
+
### 2. Define projections
|
44
|
+
|
45
|
+
class MyProjection < SandthornSequelProjection::Projection
|
46
|
+
|
47
|
+
# Start by defining any needed migrations.
|
48
|
+
# See SimpleMigrator::Migratable for details
|
49
|
+
migration("MyProjection20150215-1") do |db_connection|
|
50
|
+
db_connection.create_table?(:my_table) do
|
51
|
+
primary_key :id
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Event handlers will be executed in the order they were defined
|
56
|
+
# The key is the name of the method to be executed. Filters are defined in the value.
|
57
|
+
# Handlers with only a symbol will execute for all events.
|
58
|
+
define_event_handlers do |handlers|
|
59
|
+
handlers.add new_user: { aggregate_type: MyAggregates::User, event_name: :new }
|
60
|
+
handlers.add foo_changed: { aggregate_types: [MyAggregates::User, MyAggregates::Foo] }
|
61
|
+
handlers.add :wildcard
|
62
|
+
end
|
63
|
+
|
64
|
+
def new_users(event)
|
65
|
+
# handle new user events, one at a time
|
66
|
+
end
|
67
|
+
|
68
|
+
def foo_changed(event)
|
69
|
+
# handle the events defined in the foo_changed-listener, one at a time
|
70
|
+
end
|
71
|
+
|
72
|
+
def wildcard(event)
|
73
|
+
# Will receive all events
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
### 3. Create a manifest
|
78
|
+
|
79
|
+
Manifests are used to define the order in which projections are run.
|
80
|
+
|
81
|
+
manifest = SandthornSequelProjections::Manifest.create
|
82
|
+
[
|
83
|
+
MyProjection,
|
84
|
+
MyProjections::SomeOtherProjection
|
85
|
+
]
|
86
|
+
|
87
|
+
### 4. Run the projections
|
88
|
+
|
89
|
+
Create a runner and give it the manifest. Run your projections.
|
90
|
+
|
91
|
+
runner = SandthornSequelProjection::Runner.new(manifest)
|
92
|
+
runner.run
|
93
|
+
|
94
|
+
The runner runs migrations for all of the projections and then
|
95
|
+
polls the event store for changes and passes new events to the projections.
|
96
|
+
|
97
|
+
## Plans for the future
|
98
|
+
|
99
|
+
The projection manifest should define dependent projections, similar to how Rake tasks are defined.
|
100
|
+
In this way, we could identify the most efficient way of splitting up projections over multiple
|
101
|
+
threads.
|
102
|
+
|
103
|
+
For example:
|
104
|
+
|
105
|
+
SandthornSequelProjection.manifest do
|
106
|
+
projection my_dependent_projection: [:projection_foo, :projection_bar]
|
107
|
+
projection my_other_dependent_projection: [:my_dependent_projection, :projection_bar]
|
108
|
+
projection my_third_dependent_projection: :projection_qux
|
109
|
+
end
|
110
|
+
|
111
|
+
This manifest would create two independent branches:
|
112
|
+
|
113
|
+
:projection_foo :projection_bar :projection_qux
|
114
|
+
\ / |
|
115
|
+
:my_dependent_projection :my_third_dependent_projection
|
116
|
+
|
|
117
|
+
|
|
118
|
+
:my_other_dependent_projection
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
## Contributing
|
125
|
+
|
126
|
+
1. Fork it ( https://github.com/[my-github-username]/sandthorn_sequel_projection/fork )
|
127
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
128
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
129
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
130
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
task :console do
|
10
|
+
require 'awesome_print'
|
11
|
+
require 'pry'
|
12
|
+
require 'bundler'
|
13
|
+
require 'sandthorn_sequel_projection'
|
14
|
+
ARGV.clear
|
15
|
+
Pry.start
|
16
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "sequel"
|
2
|
+
require "sandthorn_event_filter"
|
3
|
+
require "simple_migrator"
|
4
|
+
require "sandthorn"
|
5
|
+
|
6
|
+
require "sandthorn_sequel_projection/errors"
|
7
|
+
require "sandthorn_sequel_projection/version"
|
8
|
+
require "sandthorn_sequel_projection/utilities"
|
9
|
+
require "sandthorn_sequel_projection/cursor"
|
10
|
+
require "sandthorn_sequel_projection/event_handler"
|
11
|
+
require "sandthorn_sequel_projection/event_handler_collection"
|
12
|
+
require "sandthorn_sequel_projection/projection"
|
13
|
+
require "sandthorn_sequel_projection/lock"
|
14
|
+
require "sandthorn_sequel_projection/processed_events_tracker"
|
15
|
+
require "sandthorn_sequel_projection/manifest"
|
16
|
+
require "sandthorn_sequel_projection/runner"
|
17
|
+
|
18
|
+
module SandthornSequelProjection
|
19
|
+
|
20
|
+
class << self
|
21
|
+
require 'delegate'
|
22
|
+
extend Forwardable
|
23
|
+
|
24
|
+
def_delegators :configuration, :batch_size, :event_stores
|
25
|
+
|
26
|
+
attr_accessor :configuration
|
27
|
+
|
28
|
+
def configure
|
29
|
+
@configuration ||= Configuration.default
|
30
|
+
yield(configuration) if block_given?
|
31
|
+
start
|
32
|
+
end
|
33
|
+
|
34
|
+
def start
|
35
|
+
ProcessedEventsTracker.migrate!(configuration.db_connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_event_store(name)
|
39
|
+
Sandthorn.find_event_store(name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Configuration
|
44
|
+
|
45
|
+
attr_accessor :db_connection, :event_stores, :projections_folder, :batch_size
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
yield(self) if block_given?
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.default
|
52
|
+
self.new do |c|
|
53
|
+
c.batch_size = 40
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SandthornSequelProjection
|
2
|
+
class Cursor
|
3
|
+
|
4
|
+
attr_reader :last_sequence_number, :batch_size
|
5
|
+
|
6
|
+
def initialize(
|
7
|
+
after_sequence_number: 0,
|
8
|
+
event_store: SandthornSequelProjection.default_event_store,
|
9
|
+
batch_size: SandthornSequelProjection.batch_size)
|
10
|
+
@last_sequence_number = after_sequence_number
|
11
|
+
@batch_size = batch_size
|
12
|
+
@event_store = event_store
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_batch
|
16
|
+
events = get_events
|
17
|
+
events.tap do |events|
|
18
|
+
if last_event = events.last
|
19
|
+
@last_sequence_number = last_event[:sequence_number]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def get_events
|
27
|
+
wrap(get_event_array)
|
28
|
+
end
|
29
|
+
|
30
|
+
def wrap(events)
|
31
|
+
SandthornEventFilter.filter(events)
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_event_array
|
35
|
+
@event_store.get_events(after_sequence_number: last_sequence_number, take: batch_size)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SandthornSequelProjection
|
2
|
+
|
3
|
+
class SandthornSequelProjectionError < StandardError; end
|
4
|
+
|
5
|
+
class MigrationError < SandthornSequelProjectionError
|
6
|
+
def initialize(error)
|
7
|
+
super(error.message)
|
8
|
+
@error = error
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class InvalidEventStoreError < SandthornSequelProjectionError; end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module SandthornSequelProjection
|
2
|
+
class EventHandler
|
3
|
+
|
4
|
+
attr_reader :projection, :filter, :message
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@filter = SandthornEventFilter::Filter.new
|
8
|
+
parse_options(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle(target, event)
|
12
|
+
if filter.match?(event)
|
13
|
+
call_handler(target, event)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse_options(options)
|
20
|
+
if options.is_a? Symbol
|
21
|
+
set_method(options)
|
22
|
+
elsif options.is_a? Hash
|
23
|
+
method_name = options.keys.first
|
24
|
+
set_method(method_name)
|
25
|
+
construct_filter(options[method_name])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def call_handler(target, event)
|
30
|
+
target.send(message, event)
|
31
|
+
end
|
32
|
+
|
33
|
+
def construct_filter(options)
|
34
|
+
types, event_names = extract_filter_options(options)
|
35
|
+
@filter = @filter.extract(types: types) if types.any?
|
36
|
+
@filter = @filter.extract(events: event_names) if types.any?
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_filter_options(options)
|
40
|
+
types = Array.wrap(options[:aggregate_type] || options[:aggregate_types])
|
41
|
+
events = Array.wrap(options[:event_name] || options[:event_names])
|
42
|
+
[types, events]
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_method(message)
|
46
|
+
@message = message
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
class EventHandlerCollection
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :handlers, :length, :each, :first
|
7
|
+
|
8
|
+
attr_reader :handlers
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@handlers = Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def define(handler_data)
|
15
|
+
@handlers << EventHandler.new(handler_data)
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle(projection, events)
|
19
|
+
events.each do |event|
|
20
|
+
handlers.each do |handler|
|
21
|
+
handler.handle(projection, event)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
module SandthornSequelProjection
|
3
|
+
class Lock
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :db_connection, :identifier
|
7
|
+
|
8
|
+
def_delegators :db_connection, :transaction
|
9
|
+
|
10
|
+
DEFAULT_TIMEOUT = 3*60 # 3 minutes
|
11
|
+
DEFAULT_LOCK_COLUMN = :locked_at
|
12
|
+
|
13
|
+
def initialize(identifier, db_connection = nil, table_name = nil)
|
14
|
+
@identifier = identifier.to_s
|
15
|
+
@db_connection = db_connection || SandthornDriverSequel.configuration.db_connection
|
16
|
+
@table_name = table_name || ProcessedEventsTracker::DEFAULT_TABLE_NAME
|
17
|
+
end
|
18
|
+
|
19
|
+
def locked?
|
20
|
+
!unlocked?
|
21
|
+
end
|
22
|
+
|
23
|
+
def unlocked?
|
24
|
+
locked_at.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def expired?
|
28
|
+
locked_at && (Time.now - locked_at > timeout)
|
29
|
+
end
|
30
|
+
|
31
|
+
def timeout
|
32
|
+
DEFAULT_TIMEOUT
|
33
|
+
end
|
34
|
+
|
35
|
+
def acquire
|
36
|
+
if attempt_lock
|
37
|
+
begin
|
38
|
+
yield
|
39
|
+
ensure
|
40
|
+
release
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def release
|
46
|
+
set_lock(nil)
|
47
|
+
end
|
48
|
+
|
49
|
+
def attempt_lock
|
50
|
+
transaction do
|
51
|
+
if unlocked? || expired?
|
52
|
+
lock
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def lock_column_name
|
58
|
+
DEFAULT_LOCK_COLUMN
|
59
|
+
end
|
60
|
+
|
61
|
+
def lock
|
62
|
+
set_lock(Time.now)
|
63
|
+
end
|
64
|
+
|
65
|
+
def db_row
|
66
|
+
table.where(identifier: @identifier)
|
67
|
+
end
|
68
|
+
|
69
|
+
def table
|
70
|
+
db_connection[@table_name]
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_lock(value)
|
74
|
+
db_row.update(lock_column_name => value)
|
75
|
+
end
|
76
|
+
|
77
|
+
def locked_at
|
78
|
+
if row = db_row.first
|
79
|
+
row[lock_column_name]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|