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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +3 -0
  3. data/.gitignore +23 -0
  4. data/.rspec +2 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +9 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +130 -0
  11. data/Rakefile +16 -0
  12. data/lib/sandthorn_sequel_projection.rb +57 -0
  13. data/lib/sandthorn_sequel_projection/cursor.rb +39 -0
  14. data/lib/sandthorn_sequel_projection/errors.rb +14 -0
  15. data/lib/sandthorn_sequel_projection/event_handler.rb +49 -0
  16. data/lib/sandthorn_sequel_projection/event_handler_collection.rb +27 -0
  17. data/lib/sandthorn_sequel_projection/lock.rb +83 -0
  18. data/lib/sandthorn_sequel_projection/manifest.rb +10 -0
  19. data/lib/sandthorn_sequel_projection/processed_events_tracker.rb +94 -0
  20. data/lib/sandthorn_sequel_projection/projection.rb +65 -0
  21. data/lib/sandthorn_sequel_projection/runner.rb +62 -0
  22. data/lib/sandthorn_sequel_projection/tasks.rb +14 -0
  23. data/lib/sandthorn_sequel_projection/utilities.rb +5 -0
  24. data/lib/sandthorn_sequel_projection/utilities/core_extensions/array_wrap.rb +13 -0
  25. data/lib/sandthorn_sequel_projection/utilities/null_proc.rb +9 -0
  26. data/lib/sandthorn_sequel_projection/version.rb +3 -0
  27. data/sandthorn_sequel_projection.gemspec +37 -0
  28. data/spec/cursor_spec.rb +44 -0
  29. data/spec/event_handler_collection_spec.rb +39 -0
  30. data/spec/event_handler_spec.rb +82 -0
  31. data/spec/integration/projection_spec.rb +126 -0
  32. data/spec/lock_spec.rb +141 -0
  33. data/spec/processed_events_tracker_spec.rb +79 -0
  34. data/spec/projection_spec.rb +91 -0
  35. data/spec/runner_spec.rb +32 -0
  36. data/spec/spec_helper.rb +23 -0
  37. data/spec/support/mock_event_store.rb +39 -0
  38. data/spec/test_data/event_data.json +1876 -0
  39. data/spec/utilities/null_proc_spec.rb +14 -0
  40. metadata +262 -0
@@ -0,0 +1,10 @@
1
+ module SandthornSequelProjection
2
+ class Manifest
3
+
4
+ attr_reader :projections
5
+
6
+ def initialize(*projections)
7
+ @projections = Array.wrap(projections).flatten
8
+ end
9
+ end
10
+ end
@@ -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,5 @@
1
+ module Utilities
2
+ end
3
+
4
+ require "sandthorn_sequel_projection/utilities/null_proc"
5
+ require "sandthorn_sequel_projection/utilities/core_extensions/array_wrap"
@@ -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,9 @@
1
+ module SandthornSequelProjection
2
+ module Utilities
3
+ class NullProc < Proc
4
+ def self.new
5
+ super() { |*| }
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module SandthornSequelProjection
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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