From fc404d7aecd69406aa11a4b4991db5ace24ceed1 Mon Sep 17 00:00:00 2001 From: Chad Gilbert Date: Tue, 30 Apr 2019 21:23:53 -0400 Subject: [PATCH 1/2] Add workarounds for web farms to FAQ --- articles/faq.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/articles/faq.md b/articles/faq.md index 8474390..02089da 100644 --- a/articles/faq.md +++ b/articles/faq.md @@ -17,3 +17,89 @@ Possible reasons: ## What are the supported databases? [!include[Supported databases](../snippets/supported-databases.md)] + +## How can I run migrations safely from multiple application servers? + +Many server-side applications are load balanced and run multiple instances of the same application simultaneously from multiple web servers. In such a scenarios, if you choose to run migrations in-process (as opposed to an external migration runner), then there is an added risk of multiple processes trying to run migrations at the same time. + +FluentMigrator does not automatically handle this scenario because the default transactional behavior is not enough to guarantee that only a single process can be running migrations at any given time. There are, however, some workarounds. + +### Database-Dependent Application Locking + +This style of solution depends upon MaintenanceMigrations. Two Maintenance Migrations are created: One with `BeforeAll` to atomically acquire a lock, and one `AfterAll` stage to release the lock. + +This example is for **SQL Server 2008 and above** and uses [`sp_getapplock`](https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-getapplock-transact-sql) to aquire a named lock before all migrations are run, and [`sp_releaseapplock`](https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-releaseapplock-transact-sql) to release the lock after all migrations are finished. + +```c# +[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)] +public class DbMigrationLockBefore : Migration +{ + public override void Down() + { + throw new NotImplementedException("Down migrations are not supported for sp_getapplock"); + } + + public override void Up() + { + Execute.Sql(@" + DECLARE @result INT + EXEC @result = sp_getapplock 'MyApp', 'Exclusive', 'Session' + + IF @result < 0 + BEGIN + DECLARE @msg NVARCHAR(1000) = 'Received error code ' + CAST(@result AS VARCHAR(10)) + ' from sp_getapplock during migrations'; + THROW 99999, @msg, 1; + END + "); + } +} + +[Maintenance(MigrationStage.AfterAll, TransactionBehavior.None)] +public class DbMigrationUnlockAfter : Migration +{ + public override void Down() + { + throw new NotImplementedException("Down migrations are not supported for sp_releaseapplock"); + } + + public override void Up() + { + Execute.Sql("EXEC sp_releaseapplock 'MyApp', 'Session'"); + } +} +``` + +In the above SQL Server example, we need to use `TransactionBehavior.None` on the Maintenance Migration while specifying the `@LockOwner` parameter to `Session`, which means that the locking behavior applies to the entire Session rather than a single transaction. + +While the above is specific to SQL Server, similar concepts may available in other database providers. + +* PostgreSQL has [Advisory Locks](https://www.postgresql.org/docs/10/explicit-locking.html#ADVISORY-LOCKS) +* SQL Anywhere has [Schema Locks](http://infocenter.sybase.com/help/topic/com.sybase.help.sqlanywhere.12.0.1/dbusage/transact-s-3443172.html) +* Oracle has [DBMS_LOCK.ALLOCATE_UNIQUE](https://docs.oracle.com/cd/A91202_01/901_doc/appdev.901/a89852/dbms_l2a.htm) +* DB2 has [LOCK TABLESPACE](https://www.ibm.com/support/knowledgecenter/en/SSEPEK_11.0.0/perf/src/tpc/db2z_lockmode.html) (with the caveat that every table in your migration is in the same tablespace) + +### External Distributed Lock + +If your database doesn't provide a means of acquiring an exclusive lock for migrations, it is still possible to achieve this functionality by using an external service for acquiring a distributed lock. + +For example, Redis provides a way to perform [Distributed locks](https://redis.io/topics/distlock) so that different processes can operate on a shared resource in a mutually exclusive way. This scenario can be baked into a `BeforeAll`/`AfterAll` pair of Maintenance Migrations, as demonstrated above, to acquire an exclusive lock for the duration of the migration running. + +As an alternative to Maintenance Migrations, which are by necessity specified in different classes, you could simply wrap the `MigrateUp()` call in code that acquires and releases the lock. Consider this pseudo-code which relies on [RedLock.net](https://github.com/samcook/RedLock.net): + +```c# +async RunMigrationsWithDistributedLock(IMigrationRunner runner) +{ + var resource = "my-app-migrations"; + var expiry = TimeSpan.FromMinutes(5); + + using (var redLock = await redlockFactory.CreateLockAsync(resource, expiry)) // there are also non async Create() methods + { + // make sure we got the lock + if (redLock.IsAcquired) + { + runner.MigrateUp(); + } + } + // the lock is automatically released at the end of the using block +} +``` From e5d44f2259d8029f501598dc528454b7ce3c88a0 Mon Sep 17 00:00:00 2001 From: Chad Gilbert Date: Tue, 30 Apr 2019 21:50:31 -0400 Subject: [PATCH 2/2] Update quickstart with web farm in-process caveat --- articles/quickstart.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/articles/quickstart.md b/articles/quickstart.md index da2c501..2d00469 100644 --- a/articles/quickstart.md +++ b/articles/quickstart.md @@ -44,10 +44,15 @@ This will create a table named `Log` with the columns `Id`, and `Text`. You have two options to execute your migration: -* Using an in-process runner (preferred when running from a single process) -* Using an out-of-process runner (for some corporate requirements or when running from multiple processes) +* Using an in-process runner (preferred) +* Using an out-of-process runner (for some corporate requirements) -## [In-Process (preferred when running from a single process)](#tab/runner-in-process) +## [In-Process (preferred)](#tab/runner-in-process) + +> [!NOTE] +> If you are potentially running migrations from multiple application servers, such as a load balanced set of web servers, +> you will need to acquire a distributed and exclusive lock, either by database-dependent means or through the use of an +> external distributed lock coordinator. [See the FAQ for more information](xref:faq#how-can-i-run-migrations-safely-from-multiple-application-servers). Change your `Program.cs` to the following code: @@ -56,7 +61,7 @@ Change your `Program.cs` to the following code: As you can see, instantiating the [migration runner](xref:FluentMigrator.Runner.IMigrationRunner) (in `UpdateDatabase`) becomes very simple and updating the database is straight-forward. -## [Out-of-process (for some corporate requirements or when running from multiple processes)](#tab/runner-dotnet-fm) +## [Out-of-process (for some corporate requirements)](#tab/runner-dotnet-fm) > [!IMPORTANT] > You need at least the .NET Core 2.1 preview 2 SDK for this tool.