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
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
@@ -0,0 +1,3 @@
1
+ Autotest.add_hook :initialize do |at|
2
+ %w{.git spec/db}.each {|exception| at.add_exception(exception)}
3
+ end
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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
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
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 2.0.0
5
+ addons:
6
+ codeclimate:
7
+ repo_token: d57433c34e6540ec1ede44f03df0843c49e8f9b83bdf6ce7a767e9be0c9c6fd7
8
+ after_script:
9
+ - cat lcov.info | codeclimate
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sandthorn_sequel_projection.gemspec
4
+ gemspec
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