sandthorn_sequel_projection 0.0.1

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