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