Skip to content
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

[release/9.0] Acquire the migrations database lock in a transaction. #34489

Merged
merged 4 commits into from
Sep 3, 2024

Conversation

AndriySvyryd
Copy link
Member

@AndriySvyryd AndriySvyryd commented Aug 21, 2024

Fixes #34439

Description

The migrations database lock introduced in #34115 doesn't work with providers that use a transaction-based lock implementation.

With this the lock can be acquired inside the transaction and all migrations are executed in a single transaction (when possible) to allow them to be covered by a single lock.

Additionally, the non-transaction based locks will be reacquired when retrying on a transient failure.

Customer impact

Currently, the migrations database lock does not work on some providers. And they aren't reacquired when retrying on a transient failure for the other providers.

How found

Reported by a provider maintainer.

Regression

No, new feature.

Testing

Tests added.

Risk

Medium. It's a breaking change for providers from rc1, but it's unlikely that the existing implementations were working correctly.

@AndriySvyryd AndriySvyryd requested a review from roji August 21, 2024 05:45
@AndriySvyryd
Copy link
Member Author

@roji Let me know if this is an acceptable design

Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndriySvyryd thanks, overall looks to me... There are some slight things I don't understand about flowing the state across Migrator/MigrationCommandExecutor, but that may just be the draft state of this PR. Otherwise see comments below.

I'm personally not seeing the value of the separation of Migrator vs. MigrationCommandExecutor - the separation may actually add more complexity/cognitive load rather than simplifying (especially now that transaction management and locking are also managed in Migrator...). Something to consider for the future...

@AndriySvyryd
Copy link
Member Author

I'm personally not seeing the value of the separation of Migrator vs. MigrationCommandExecutor - the separation may actually add more complexity/cognitive load rather than simplifying (especially now that transaction management and locking are also managed in Migrator...). Something to consider for the future...

This is mainly to reduce code duplication as it's also called from RelationalDatabaseCreator.CreateTables. However, the lock isn't taken there, since the migrations history table shouldn't be created.

@roji
Copy link
Member

roji commented Aug 21, 2024

I'm personally not seeing the value of the separation of Migrator vs. MigrationCommandExecutor - the separation may actually add more complexity/cognitive load rather than simplifying (especially now that transaction management and locking are also managed in Migrator...). Something to consider for the future...

This is mainly to reduce code duplication as it's also called from RelationalDatabaseCreator.CreateTables. However, the lock isn't taken there, since the migrations history table shouldn't be created.

OK. This is academic at the moment in any case, I'm not actually proposing we do anything for 9.0...

@AndriySvyryd AndriySvyryd changed the title Acquire the migrations database lock in a transaction. [release/9.0] Acquire the migrations database lock in a transaction. Aug 24, 2024
Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's getting closer @AndriySvyryd but I can still see some trouble (or stuff I don't understand), take a look.

src/EFCore.Relational/Migrations/Internal/Migrator.cs Outdated Show resolved Hide resolved
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public abstract LockReleaseBehavior LockReleaseBehavior { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming: I'd love to just call this LockingScope, but it's true that for Explicit we still take it ourselves, so it seems to indeed apply only to the release part...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dotnet/efteam API review question

Copy link
Member

@roji roji Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am also OK with the current naming if we don't get around to discussing this (same with the other naming proposals below).

Copy link
Member Author

@AndriySvyryd AndriySvyryd Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. We won't have any meetings for this, so if anyone else in @dotnet/efteam has an opinion - speak up

/// <see langword="null"/> if there's no current transaction.
/// </param>
/// <returns>An object that can be disposed to release the lock.</returns>
IMigrationsDatabaseLock Refresh(bool connectionReopened, bool? transactionRestarted)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it might belong in the callsites, rather than as part of the lock implementation - but I'm not sure... The behavior here seems universal for the various LockReleaseBehavior values, i.e. I don't see an implementation of IMigrationsDatabaseLock ever needing to change it - do you have a specific case in mind?

If we're not sure, it may be better to not prematurely expose this hook before we have a request, or at least flag it as pubternal...

Otherwise, we may just want to call this Reacquire() instead of Refresh(), for consistency (or ReacquireIfNeeded())

Copy link
Member Author

@AndriySvyryd AndriySvyryd Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason it's here is because I don't want to expose HistoryRepository to the callsites as that would allow them to acquire new locks.

The secondary benefit is having an escape hatch in case there's a behavior that can't be currently expressed in LockReleaseBehavior. Any request for this would be too late to incorporate into 9.0.0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dotnet/efteam API Review

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason it's here is because I don't want to expose HistoryRepository to the callsites as that would allow them to acquire new locks.

OK, that may be a bit over-defensive for very low-level code that we maintain (or possibly a provider).

The secondary benefit is having an escape hatch in case there's a behavior that can't be currently expressed in LockReleaseBehavior. Any request for this would be too late to incorporate into 9.0.0

I'm a bit doubtful about the extent to which actual custom logic could be implemented in the current design... Simply because it's not really possible to override Migrator and MigrationCommandExecutor behavior which determines exactly where the lock gets taken/released...

Anyway, I'm OK with the design as-is. I'd maybe slightly prefer putting pubternal/experimental on this as if feels like we may want to refactor/improve the design later, but I'll defer to you.

/// <see langword="null"/> if there's no current transaction.
/// </param>
/// <returns>An object that can be disposed to release the lock.</returns>
IMigrationsDatabaseLock Refresh(bool connectionReopened, bool? transactionRestarted)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: transactionStarted rather than restarted?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this name accurately conveys the tri-value aspect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dotnet/efteam API Review

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this name accurately conveys the tri-value aspect.

Maybe, but transactionRestarted is also inaccurate for the case where we're just starting out and there nothing to re-start.

But never mind, this isn't important and it's a bit late for minor naming discussions...

@AndriySvyryd AndriySvyryd force-pushed the Issue34439 branch 2 times, most recently from 378c56a to e27f07a Compare August 30, 2024 04:54
@AndriySvyryd AndriySvyryd enabled auto-merge (squash) September 3, 2024 00:15
@AndriySvyryd AndriySvyryd merged commit 972a50a into release/9.0 Sep 3, 2024
7 checks passed
@AndriySvyryd AndriySvyryd deleted the Issue34439 branch September 3, 2024 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants