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
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
class ProcessedEventsTracker
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegator self, :table_name
|
8
|
+
def_delegators :db_connection, :transaction
|
9
|
+
|
10
|
+
attr_reader :db_connection, :identifier, :lock, :event_store
|
11
|
+
|
12
|
+
DEFAULT_TABLE_NAME = :processed_events_trackers
|
13
|
+
|
14
|
+
def initialize(identifier:, event_store:, db_connection: nil)
|
15
|
+
@identifier = identifier.to_s
|
16
|
+
@event_store = event_store
|
17
|
+
@db_connection = db_connection || SandthornSequelProjection.configuration.db_connection
|
18
|
+
@lock = Lock.new(identifier, @db_connection)
|
19
|
+
ensure_row
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_lock
|
23
|
+
@lock.acquire do
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_events(&block)
|
29
|
+
with_lock do
|
30
|
+
cursor = Cursor.new(after_sequence_number: last_processed_sequence_number, event_store: event_store)
|
31
|
+
events = cursor.get_batch
|
32
|
+
transaction do
|
33
|
+
until(events.empty?)
|
34
|
+
block.call(events)
|
35
|
+
events = cursor.get_batch
|
36
|
+
end
|
37
|
+
write_sequence_number(cursor.last_sequence_number)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def last_processed_sequence_number
|
43
|
+
row[:last_processed_sequence_number]
|
44
|
+
end
|
45
|
+
|
46
|
+
def table
|
47
|
+
db_connection[table_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
def row_exists?
|
51
|
+
!row.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
def row
|
55
|
+
table.where(identifier: identifier).first
|
56
|
+
end
|
57
|
+
|
58
|
+
def reset
|
59
|
+
with_lock do
|
60
|
+
write_sequence_number(0)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.table_name
|
65
|
+
DEFAULT_TABLE_NAME
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.migrate!(db_connection)
|
69
|
+
db_connection.create_table?(table_name) do
|
70
|
+
String :identifier
|
71
|
+
Integer :last_processed_sequence_number, default: 0
|
72
|
+
DateTime :locked_at, null: true
|
73
|
+
end
|
74
|
+
db_connection.add_index(table_name, :identifier, unique: true)
|
75
|
+
rescue Exception => e
|
76
|
+
raise Errors::MigrationError, e
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def write_sequence_number(number)
|
82
|
+
table.where(identifier: identifier).update(last_processed_sequence_number: number)
|
83
|
+
end
|
84
|
+
|
85
|
+
def ensure_row
|
86
|
+
create_row unless row_exists?
|
87
|
+
end
|
88
|
+
|
89
|
+
def create_row
|
90
|
+
table.insert(identifier: identifier)
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
class Projection
|
5
|
+
extend Forwardable
|
6
|
+
include SimpleMigrator::Migratable
|
7
|
+
|
8
|
+
def_delegators self, :identifier, :event_store
|
9
|
+
def_delegators :tracker, :last_processed_sequence_number
|
10
|
+
|
11
|
+
attr_reader :db_connection, :event_handlers, :tracker
|
12
|
+
|
13
|
+
def initialize(db_connection = nil)
|
14
|
+
@db_connection = db_connection || SandthornSequelProjection.configuration.db_connection
|
15
|
+
@tracker = ProcessedEventsTracker.new(
|
16
|
+
identifier: identifier,
|
17
|
+
db_connection: @db_connection,
|
18
|
+
event_store: event_store)
|
19
|
+
@event_handlers = self.class.event_handlers
|
20
|
+
end
|
21
|
+
|
22
|
+
def update!
|
23
|
+
tracker.process_events do |batch|
|
24
|
+
event_handlers.handle(self, batch)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def migrator
|
29
|
+
SimpleMigrator.migrator(db_connection)
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
attr_reader :event_store_name
|
34
|
+
|
35
|
+
def event_store(event_store = nil)
|
36
|
+
if event_store
|
37
|
+
@event_store_name = event_store
|
38
|
+
else
|
39
|
+
find_event_store
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_event_store
|
44
|
+
SandthornSequelProjection.find_event_store(@event_store_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :event_store_name
|
48
|
+
attr_accessor :event_handlers
|
49
|
+
|
50
|
+
def define_event_handlers
|
51
|
+
@event_handlers ||= EventHandlerCollection.new
|
52
|
+
yield(@event_handlers)
|
53
|
+
end
|
54
|
+
|
55
|
+
def identifier
|
56
|
+
self.name.gsub(/::/, '_').
|
57
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
58
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
59
|
+
tr("-", "_").
|
60
|
+
downcase
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module SandthornSequelProjection
|
2
|
+
class Runner
|
3
|
+
|
4
|
+
DEFAULT_INTERVAL = 0.5
|
5
|
+
|
6
|
+
attr_reader :manifest, :interval
|
7
|
+
|
8
|
+
def initialize(manifest, interval = DEFAULT_INTERVAL)
|
9
|
+
@manifest = manifest
|
10
|
+
@interval = interval
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(infinite = true)
|
14
|
+
@projections = manifest.projections.map do |projection_class|
|
15
|
+
projection_class.new(db_connection)
|
16
|
+
end
|
17
|
+
migrate!
|
18
|
+
if infinite
|
19
|
+
start_loop
|
20
|
+
else
|
21
|
+
loop_once
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def start_loop
|
28
|
+
while true
|
29
|
+
loop_once
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def loop_once
|
34
|
+
@projections.each do |projection|
|
35
|
+
projection.update!
|
36
|
+
end
|
37
|
+
sleep(interval)
|
38
|
+
end
|
39
|
+
|
40
|
+
def db_connection
|
41
|
+
SandthornSequelProjection.configuration.db_connection
|
42
|
+
end
|
43
|
+
|
44
|
+
def migrate!
|
45
|
+
@projections.each(&:migrate!)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
class CircularQueue < Queue
|
51
|
+
|
52
|
+
# = CircularQueue
|
53
|
+
# Automatically pushes popped elements to the back of the queue
|
54
|
+
# In other words, can never be emptied by use of pop
|
55
|
+
def pop
|
56
|
+
super.tap do |el|
|
57
|
+
self << el
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rake/dsl_definition'
|
2
|
+
|
3
|
+
module SandthornSequelProjections
|
4
|
+
class RakeTasks
|
5
|
+
include Rake::DSL if defined? Rake::DSL
|
6
|
+
def install
|
7
|
+
namespace :sandthorn_projections do
|
8
|
+
task :init do
|
9
|
+
SandthornSequelProjections.start
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copied from ActiveSupport
|
2
|
+
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/array/wrap.rb
|
3
|
+
class Array
|
4
|
+
def self.wrap(object)
|
5
|
+
if object.nil?
|
6
|
+
[]
|
7
|
+
elsif object.respond_to?(:to_ary)
|
8
|
+
object.to_ary || [object]
|
9
|
+
else
|
10
|
+
[object]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sandthorn_sequel_projection/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sandthorn_sequel_projection"
|
8
|
+
spec.version = SandthornSequelProjection::VERSION
|
9
|
+
spec.authors = ["Lars Krantz"]
|
10
|
+
spec.email = ["lars.krantz@alaz.se"]
|
11
|
+
spec.summary = %q{Helps creating sql projections from sandthorn events}
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.required_ruby_version = ">= 2.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "sandthorn_driver_sequel"
|
27
|
+
spec.add_development_dependency "awesome_print"
|
28
|
+
spec.add_development_dependency "pry"
|
29
|
+
spec.add_development_dependency "sqlite3"
|
30
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
31
|
+
|
32
|
+
spec.add_runtime_dependency "sandthorn", "~> 0.6"
|
33
|
+
spec.add_runtime_dependency "sandthorn_event_filter", "~> 0.0.4"
|
34
|
+
spec.add_runtime_dependency "sequel"
|
35
|
+
spec.add_runtime_dependency "simple_migrator"
|
36
|
+
|
37
|
+
end
|
data/spec/cursor_spec.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module SandthornSequelProjection
|
2
|
+
describe Cursor do
|
3
|
+
|
4
|
+
let(:first_page) { [ { sequence_number: 1 } ] }
|
5
|
+
let(:second_page) { [ { sequence_number: 2 } ] }
|
6
|
+
let(:third_page) { [] }
|
7
|
+
let(:event_store) do
|
8
|
+
store = Object.new
|
9
|
+
env = self
|
10
|
+
store.tap do |store|
|
11
|
+
store.define_singleton_method(:get_events) do |after_sequence_number: 0, take: 1|
|
12
|
+
case after_sequence_number
|
13
|
+
when 0
|
14
|
+
env.first_page
|
15
|
+
when 1
|
16
|
+
env.second_page
|
17
|
+
else
|
18
|
+
env.third_page
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
let(:cursor) { Cursor.new(after_sequence_number: 0, batch_size: 1, event_store: event_store) }
|
24
|
+
|
25
|
+
it "has the correct starting sequence number and batch size" do
|
26
|
+
expect(cursor.batch_size).to eq(1)
|
27
|
+
expect(cursor.last_sequence_number).to eq(0)
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#get_batch" do
|
31
|
+
describe "sequential calls" do
|
32
|
+
it "returns pages until empty and keeps track of seen sequence numbers" do
|
33
|
+
expect(cursor.get_batch.events).to eq(first_page)
|
34
|
+
expect(cursor.last_sequence_number).to eq(first_page.last[:sequence_number])
|
35
|
+
expect(cursor.get_batch.events).to eq(second_page)
|
36
|
+
expect(cursor.last_sequence_number).to eq(second_page.last[:sequence_number])
|
37
|
+
expect(cursor.get_batch.events).to eq(third_page)
|
38
|
+
expect(cursor.last_sequence_number).to eq(second_page.last[:sequence_number])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
describe EventHandlerCollection do
|
5
|
+
let(:collection) { EventHandlerCollection.new }
|
6
|
+
let(:projection) { Object.new }
|
7
|
+
|
8
|
+
describe "#add" do
|
9
|
+
it "adds the handler to the collection" do
|
10
|
+
handler_data = :foo
|
11
|
+
collection.define(handler_data)
|
12
|
+
expect(collection.handlers.length).to eq(1)
|
13
|
+
handler = collection.handlers.first
|
14
|
+
expect(handler.message).to eq(:foo)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#handle" do
|
19
|
+
it "calls handle on each handler for every event, in order" do
|
20
|
+
events = [1,2]
|
21
|
+
handler1 = :foo
|
22
|
+
handler2 = :bar
|
23
|
+
handlers = [handler1, handler2]
|
24
|
+
handlers.each do |handler|
|
25
|
+
collection.define(handler)
|
26
|
+
end
|
27
|
+
collection.handlers.each do |handler|
|
28
|
+
expect(handler).to receive(:handle).ordered.with(projection, 1).once
|
29
|
+
end
|
30
|
+
collection.handlers.each do |handler|
|
31
|
+
expect(handler).to receive(:handle).ordered.with(projection, 2).once
|
32
|
+
end
|
33
|
+
collection.handle(projection, events)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module SandthornSequelProjection
|
4
|
+
describe EventHandler do
|
5
|
+
|
6
|
+
class TestProjection
|
7
|
+
def foo(event); event; end
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:projection) { TestProjection.new }
|
11
|
+
let(:simple_handler) { EventHandler.new(:foo)}
|
12
|
+
|
13
|
+
describe "::initialize" do
|
14
|
+
|
15
|
+
context "when given just a symbol" do
|
16
|
+
|
17
|
+
it "creates a handler with an empty filter" do
|
18
|
+
expect(simple_handler.filter).to be_empty
|
19
|
+
end
|
20
|
+
|
21
|
+
it "creates a handler with the proper method handle" do
|
22
|
+
expect(simple_handler.message).to eq(:foo)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when given filters" do
|
28
|
+
context "and given singular keywords" do
|
29
|
+
let(:handler) { EventHandler.new(foo: { aggregate_type: "FooBar", event_name: "new" })}
|
30
|
+
let(:extractor) { handler.filter.matchers.matchers.first }
|
31
|
+
let(:matching_event) { { aggregate_type: "FooBar", event_name: "new" } }
|
32
|
+
|
33
|
+
it "creates a handler with an extract filter" do
|
34
|
+
expect(extractor).to be_a_kind_of(SandthornEventFilter::Matchers::Extract)
|
35
|
+
expect(extractor).to match(matching_event)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when given plural keywords" do
|
40
|
+
let(:handler) { EventHandler.new(foo: { aggregate_types: ["FooBar"], event_names: ["new"] })}
|
41
|
+
let(:extractor) { handler.filter.matchers.matchers.first }
|
42
|
+
let(:matching_event) { { aggregate_type: "FooBar", event_name: "new" } }
|
43
|
+
|
44
|
+
it "creates the correct handler" do
|
45
|
+
expect(extractor).to be_a_kind_of(SandthornEventFilter::Matchers::Extract)
|
46
|
+
expect(extractor).to match(matching_event)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#handle" do
|
53
|
+
|
54
|
+
let(:event) { {foo: :bar} }
|
55
|
+
|
56
|
+
context "when given just a symbol" do
|
57
|
+
it "always calls the handler method" do
|
58
|
+
input = {foo: :bar}
|
59
|
+
expect(simple_handler.handle(projection, event)).to eq(event)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when given a symbol and filter" do
|
64
|
+
let(:handler) { EventHandler.new(foo: { aggregate_type: "FooBar", event_name: "new" })}
|
65
|
+
context "when the filter matches" do
|
66
|
+
it "calls the method" do
|
67
|
+
allow(handler.filter).to receive(:match?) { true }
|
68
|
+
expect(handler.handle(projection, event)).to eq(event)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "when the filter doesn't match" do
|
73
|
+
it "doesn't call the method" do
|
74
|
+
allow(handler.filter).to receive(:match?) { false }
|
75
|
+
expect(handler.handle(projection, event)).to be_falsey
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|