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
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