New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimistic Locking friendly changes #86
Conversation
I collected the work from across all gems. https://github.com/RailsEventStore/aggregate_root/issues/8 It is missing required Travis changes to run rails_event_store_active_record on Mysql and Postgres
right now it is missing required Travis changes to run |
I considered using `prepend` to initialize @unpublished_events = [] instead of using the lazy load pattern: def unpublished_events @unpublished_events ||= [] end but I decided the overhead is not worth it. Although we coud use it to set version to -1 as well. I will create a separate issue for it.
@@ -1,26 +1,55 @@ | |||
if ENV['CODECLIMATE_REPO_TOKEN'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not needed anymore. We got rid of CC
t.text :data, null: false | ||
t.integer :position, null: true | ||
if ENV['DATABASE_URL'].start_with?("postgres") | ||
t.references :event, null: false, type: :uuid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually need the :uuid? Maybe :string would suffice? With :string we don't have to enable pgcrypto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what's wrong with pgcrypto?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing wrong. It's not enabled by default. Are we going to force users to enable it even though it's not really needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
Excellent job @paneq! This is quite a big PR. Could we maybe split it into smaller chunks? The checklist already contains some bits that could be extracted to separate PRs (mysql/postrgres, fix enrich_event_metadata, etc) I would also open separate PRs for the following:
Makes sense? |
event | ||
def append_to_stream(events, stream_name, expected_version) | ||
events = [*events] | ||
expected_version = case expected_version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be in its own private method. It makes the append_to_stream
very hard to reason about otherwise, also violates single responsibility principle
when :none | ||
-1 | ||
when :auto | ||
eis = EventInStream.where(stream: stream_name).order("position DESC").first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should use a repository to access this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should also be in its own private method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code we are writing here is the repository :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's the event repository, not EventInStream repository. I would make a clear distinction
expected_version | ||
end | ||
in_stream = events.flat_map.with_index do |event, index| | ||
position = if expected_version == :any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extract in its own method
else | ||
expected_version + index + 1 | ||
end | ||
Event.create!( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should use the adapter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the point of this adapter
anymore now that the repository needs to work with 2-3 ActiveRecord classes instead of 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think it makes it way harder to re-write this repository for another ORM if they are a mixed of multiple active record classes instead of a single responsibility repository which maps to one active record class
end | ||
EventInStream.import(in_stream) | ||
self | ||
rescue ActiveRecord::RecordNotUnique |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this class should know nothing about active record
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would rails_event_store_active_record/event_repository.rb
need to know nothing about active record when it actively uses active record to implement the needed features.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvm, didn't see we are in active record namespace :)
stream: "__global__", | ||
position: nil, | ||
event_id: event.event_id | ||
)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better indentation technique would be easier to read IMO
metadata: event.metadata, | ||
event_type: event.class, | ||
) | ||
[EventInStream.new( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be extracted into its own method as well
@@ -17,75 +54,75 @@ def delete_stream(stream_name) | |||
end | |||
|
|||
def has_event?(event_id) | |||
adapter.exists?(event_id: event_id) | |||
Event.exists?(id: event_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should use the adapter
end | ||
|
||
def last_stream_event(stream_name) | ||
build_event_entity(adapter.where(stream: stream_name).last) | ||
build_event_entity( | ||
EventInStream.preload(:event).where(stream: stream_name).order('position DESC, id DESC').first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should use the event in stream repository like the other comment earlier
unless start_event_id.equal?(:head) | ||
starting_event = adapter.find_by(event_id: start_event_id) | ||
stream = stream.where('id > ?', starting_event) | ||
def read_events_forward(stream_name, after_event_id, count) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comments about adapters
I agree. It's more so that I don't forget about those things than to actually include in this PR. |
well... We could add such functionality in separate PR without releasing a new version because publishing multiple events using the old code is still going to be completely broken from optimistic locking perspective but maybe a separate PR for extending API is a good idea? Dunno |
No brainer. Feel free to extract and merge. Very simple. Related mutant discussion https://twitter.com/pankowecki/status/902521997573455877 |
Ideally not, but I didn't want to write myself all the code for doing a single INSERT with multiple records. It might not be relevant if we migrate to transactions and 3 tables approach. |
@gottfrois Thanks for the comments about extracting methods etc. This is right now however totally Work In Progress in which I am trying to find out if what we are trying to achieve is possible within the constraints that I described. I am still not exactly sure and there are bigger decisions to be made (3vs2 tables), handling concurrency etc. So once I am happy with the Big decisions, I am gonna focus on refactoring the smaller parts. |
@gottfrois Also, I am trying to reach a state in which this can be merged to master, unreleased. And then everyone can easily improve the codebase with proper tests until we are satisfied with the results. And the we could do an official release. So I am gonna dismiss the review right now. But does not mean I don't agree. I just want to handle those kind of things later :) |
no hard feelings :) |
We will deal with those changes later :)
Apparently mere developers such as me don't understand it :) I.e. how is this change related to optimistic locking? Why the big number is written in binary notation? Why do we even need to overwrite Event#hash, etc |
In one race condition test I wanted to guarantee that all events are returned but the order is not known so I used
Because our Event has
These two are failing without implementing
It's easier that way to see how many bits it has. |
Wow that's a proper explanation. Much appreciated, thanks!! |
Simple test to check if #has_event? passes if different string with same content still returns true.
This changes the original logic but it makes sense imho because we now have the concept of an event belonging to multiple streams.
We now have 2 (it can be later 3) classes responsible for storing events. I don't see much point in this adapter anymore.
Regarding mutations in |
Today I realized that ideally I would want to come out with a DB schema that allows for Event Store as queue: ( #106 ) and since I don't want to keep the DB schema constantly I kind of coupled both problems together. |
Kill lovely mutations.
I am not sure if `equal` will always work, but I think it will as two identical symbols must be the same object, no matter how constructed.
1. `preload()` only has observable perfromance effects so mutating it does not benefit and I am not sure how would we kill it anyway? I was thinking about adding in build_event_entity(record) a check like raise "use preload()" unless record.association(:event).loaded? but then how do we kill mutations around this ^^ check if does not external API in any way? 2. where('id < ?', before_event) automatically substitutes id so killing a few mutations
I was wrong in bc51e52 we can use nr of DB queries to verify preloading. It its not part of linted spec but additional test of a particular implementation.
I does not matter if we shift all postions by 1,2,3 etc. We use 1 so that first event in a stream is recorded with nr 0 :)
I had to remove_index :event_store_events_in_streams, [:stream, :position] because it was used by default when fetching the records and even without explicit order the order as defined by that used index. Surprise.
Conflicts: .travis.yml
@pawelpacana @mlomnicki before we merge this I have a few questions. Let's start with most important one:
|
@paneq yep, defo makes sense 👍 |
Conflicts: aggregate_root/spec/aggregate_root_spec.rb rails_event_store_active_record/Makefile rails_event_store_active_record/rails_event_store_active_record.gemspec ruby_event_store/Makefile ruby_event_store/lib/ruby_event_store/client.rb ruby_event_store/spec/subscription_spec.rb
Our codebase relies on non-duplicated events. Right now it is not possible to publish same event twice. But if we add append_to_stream, it will be possible to have same event in many streams. Don't mutate #detect_pkey_index_violated because it has logic which work on different databases and we can't run mutant across many DBs at the same time easily (we could have many connections but not worth it imho).
@mlomnicki @pawelpacana I want to merge today, please review holistically :) The idea behind So publishing in stream |
So far I was not sure how we could introduce a 3rd table |
@@ -87,7 +97,7 @@ def enrich_event_metadata(event) | |||
metadata[:timestamp] ||= clock.() | |||
metadata.merge!(metadata_proc.call || {}) if metadata_proc | |||
|
|||
event.class.new(event_id: event.event_id, metadata: metadata, data: event.data) | |||
# event.class.new(event_id: event.event_id, metadata: metadata, data: event.data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's up with this comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing metatadata inside the instance vs new instance with new metadata. Keeping the old behavior was not possible during refactorings. A separate task would be to bring back the old behavior if still wanted, but test it properly. It was not.
end | ||
end | ||
|
||
it 'reads events different uuid object but same content' do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite get the description about different uuid object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't clear at all here. we provide different string instance containing the same UUID. This makes it possible to establish whether we need ==
or eql?
or equal?
comparison logic. So instead of using the same variable, we use different string variable but with identical content.
It started here: https://github.com/RailsEventStore/aggregate_root/issues/8
3 possible ways of writing
Number
:none
- works like-1
assumes stream empty so far, starts adding new in streamShould work well for
Auto
:auto
- assumes lock in higher layer. Will query for last N and start doing N+1, N+2... etc [New mode]Any
:any
- NULLable position in stream, order unknownEventInStream
auto-increment id.This is just a spike that I wanted to share with you.
What do you think @mpraglowski @pawelpacana @mlomnicki ?
Checklist
enrich_event_metadata
to not editmetadata
directly but rather a cloned/duped version.all
instead of__global__
and it should only write there once.AggregateRoot
can handle conflicts