Files
Umbraco-CMS/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/Scoping/EFCoreLockTests.cs
Andy Butland 825f791d01 Remove the non-controversial, straightforward obsoleted constructs for Umbraco 16 (#18661)
* Removed obsoletes from IConfigManipulator.

* Removed obsolete models builder extensions.

* Removed the obsolete ContentDashboardSettings.

* Removed the obsolete InstallMissingDatabase setting on GlobalSettings.

* Removed obsolete NuCache settings.

* Removed obsolete RuntimeMinificationSettings.

* Removed obsolete health check constant.

* Removed obsolete icon constant.

* Removed obsolete telemetry constant.

* Removed obsolete property and constructor on UmbracoBuilder.

* Removed obsolete constructor on AuditNotificationsHandler.

* Removed obsolete constructor on HTTP header health checks.

* Removed obsolete constructor on MediaFileManager.

* Removed obsolete GetDefaultFileContent on ViewHelper.

* Remove obsoleted methods on embed providers.

* Fix tests.

* Removed obsolete constructors on BlockEditorDataConverter.

* Removed obsolete SeedCacheDuration property on CacheSettings.

* Removed obsolete PublishCulture on ContentRepositoryExtensions.

* Removed obsolete MonitorLock.

* Removed obsolete synchronous HasSavedValues from IDataTypeUsageService and IDataTypeUsageRepository.

* Removed obsolete HasSavedPropertyValues from IPropertyTypeUsageService and IPropertyTypeUsageRepository.

* Removed obsolete methods in ITrackedReferencesService and ITrackedReferencesRepository.

* Removed obsolete DateValueEditor constructors.

* Removed obsolete GetAutomaticRelationTypesAliases.

* Removed obsolete constructor on TextOnlyValueEditor.

* Removed obsolete constructors on RegexValidator and RequiredValidator.

* Removed obsolete constructs on SliderValueConverter and TagsValueConverter.

* Removed obsolete GetContentType methods from IPublishedCache.

* Removed ContentFinderByIdPath.

* Removed obsolete constructor on DefaultMediaUrlProvider.

* Removed obsolete constructor on Domain.

* Removed obsolete constructor on PublishedRequest.

* Removed obsolete methods on CheckPermissions.

* Removed obsolete GetUserId from IBackOfficeSecurity.

* Removed obsolete methods on LegacyPasswordSecurity.

* Removed obsolete constructors on AuditService.

* Removed obsolete methods on IContentEditingService.

* Remove obsolete constructors and methods on ContentService/IContentService.

* Removed obsolete constructor in ContentTypeEditingService.

* Removed obsolete constructor in MediaTypeEditingService.

* Removed obsolete constructor in MemberTypeEditingService.

* Removed obsolete constructor in ContentTypeService.

* Removed obsolete constructors in ContentTypeServiceBase.

* Removed obsolete constructors and methods in ContentVersionService.

* Removed obsolete constructor in DataTypeUsageService.

* Removed obsolete constructor in DomainService.

* Removed obsolete constructor in FileService.

* Removes obsolete AttemptMove from IContentService.

* Removes obsolete SetPreventCleanup from IContentVersionService.

* Removes obsolete GetReferences from IDataTypeService.

* Removed obsolete SetConsentLevel from IMetricsConsentService.

* Removed obsolete methods from IPackageDataInstallation.

* Removed obsolete methods from IPackagingService.

* Removed obsolete methods on ITwoFactorLoginService.
Removed obsolete ITemporaryMediaService.

* Removed obsolete constructor from MediaService, MemberTypeService and MediaTypeService.

* More obsolete constructors.

* Removed obsoleted overloads on IPropertyValidationService.

* Fixed build for tests.

* Removed obsolete constructor for PublicAccessService, UserService and RelationService.

* Removed GetDefaultMemberType.

* Removed obsolete user group functionality from IUserService.

* Removed obsolete extension methods on IUserService.

* Removed obsolete method from ITelemetryService.

* Removed obsolete UdiParserServiceConnectors.

* Removed obsolete method on ICookieManager.

* Removed obsolete DynamicContext.

* Removed obsolete XmlHelper.

* Fixed failing integration tests.

* Removed obsoletes in Umbraco.Cms.Api.Common

* Removed obsoletes in Umbraco.Cms.Api.Delivery

* Removed obsoletes in Umbraco.Cms.Api.Management

* Removed obsoletes in Umbraco.Examine.Lucene

* Removed obsoletes in Umbraco.Infrastructure

* Fix failing delivery API contract integration test.

* Made integration tests internal.

* Removed obsoletes from web projects.

* Fix build.

* Removed Twitter OEmbed provider

* Removed obsolete constructor on PublishedDataType.

* Removed obsolete constructors on PublishedCacheBase.

* Removed the obsolete PropertyEditorTagsExtensions.

* Removed obsoletion properties on configuration response  models (#18697)

* Removed obsolete methods from server-side models.

* Update client-side types and sdk.

* Update client-side files.

* Removed obsoletion of Utf8ToAsciiConverter.ToAsciiString overload. (#18694)

* Removed obsolete method in UserService. (#18710)

* Removed obsoleted group alias keys from being publicly available. (#18682)

* Removed unneceessary ApiVersion attribute.

* Clean-up obsoletions on MemberService (#18703)

* Removed obsoleted method on MemberService, added future obsoletion to interface and updated all callers.

* Removed obsoletion on member service method that's not obsolete on the interface.
2025-03-21 17:02:31 +00:00

404 lines
12 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DistributedLocking;
using Umbraco.Cms.Core.DistributedLocking.Exceptions;
using Umbraco.Cms.Persistence.EFCore.Locking;
using Umbraco.Cms.Persistence.EFCore.Scoping;
using Umbraco.Cms.Persistence.Sqlite.Interceptors;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
using Umbraco.Cms.Tests.Integration.Umbraco.Persistence.EFCore.DbContext;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Persistence.EFCore.Scoping;
[TestFixture]
[Timeout(60000)]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)]
internal sealed class EFCoreLockTests : UmbracoIntegrationTest
{
private IEFCoreScopeProvider<TestUmbracoDbContext> EFScopeProvider =>
GetRequiredService<IEFCoreScopeProvider<TestUmbracoDbContext>>();
protected override void ConfigureTestServices(IServiceCollection services)
{
// SQLite + retry policy makes tests fail, we retry before throwing distributed locking timeout.
services.RemoveAll(x => !x.IsKeyedService && x.ImplementationType == typeof(SqliteAddRetryPolicyInterceptor));
// Remove all locking implementations to ensure we only use EFCoreDistributedLockingMechanisms
services.RemoveAll(x => x.ServiceType == typeof(IDistributedLockingMechanism));
services.AddSingleton<IDistributedLockingMechanism, SqliteEFCoreDistributedLockingMechanism<TestUmbracoDbContext>>();
services.AddSingleton<IDistributedLockingMechanism, SqlServerEFCoreDistributedLockingMechanism<TestUmbracoDbContext>>();
}
[SetUp]
protected async Task SetUp()
{
// create a few lock objects
using var scope = EFScopeProvider.CreateScope();
await scope.ExecuteWithContextAsync<Task>(async database =>
{
database.UmbracoLocks.Add(new UmbracoLock { Id = 1, Name = "Lock.1" });
database.UmbracoLocks.Add(new UmbracoLock { Id = 2, Name = "Lock.2" });
database.UmbracoLocks.Add(new UmbracoLock { Id = 3, Name = "Lock.3" });
await database.SaveChangesAsync();
});
scope.Complete();
}
[Test]
public void SingleEagerReadLockTest()
{
using var scope = EFScopeProvider.CreateScope();
scope.Locks.EagerReadLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
[Test]
public void SingleReadLockTest()
{
using var scope = EFScopeProvider.CreateScope();
scope.Locks.ReadLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
[Test]
public void SingleWriteLockTest()
{
using var scope = EFScopeProvider.CreateScope();
scope.Locks.WriteLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
[Test]
public void SingleEagerWriteLockTest()
{
using var scope = EFScopeProvider.CreateScope();
scope.Locks.EagerWriteLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
[Test]
public void Can_Reacquire_Read_Lock()
{
using (var scope = EFScopeProvider.CreateScope())
{
scope.Locks.EagerReadLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
using (var scope = EFScopeProvider.CreateScope())
{
scope.Locks.EagerReadLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
}
[Test]
public void Can_Reacquire_Write_Lock()
{
using (var scope = EFScopeProvider.CreateScope())
{
scope.Locks.EagerWriteLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
using (var scope = EFScopeProvider.CreateScope())
{
scope.Locks.EagerWriteLock(scope.InstanceId, Constants.Locks.Servers);
scope.Complete();
}
}
[Test]
public void ConcurrentReadersTest()
{
if (BaseTestDatabase.IsSqlite())
{
Assert.Ignore(
"This test doesn't work with Microsoft.Data.Sqlite in EFCore as we no longer use deferred transactions");
return;
}
const int threadCount = 8;
var threads = new Thread[threadCount];
var exceptions = new Exception[threadCount];
Lock locker = new();
var acquired = 0;
var m2 = new ManualResetEventSlim(false);
var m1 = new ManualResetEventSlim(false);
for (var i = 0; i < threadCount; i++)
{
var ic = i; // capture
threads[i] = new Thread(() =>
{
using (var scope = EFScopeProvider.CreateScope())
{
try
{
scope.Locks.EagerReadLock(scope.InstanceId, Constants.Locks.Servers);
lock (locker)
{
acquired++;
if (acquired == threadCount)
{
m2.Set();
}
}
m1.Wait();
lock (locker)
{
acquired--;
}
}
catch (Exception e)
{
exceptions[ic] = e;
}
scope.Complete();
}
});
}
// ensure that current scope does not leak into starting threads
using (ExecutionContext.SuppressFlow())
{
foreach (var thread in threads)
{
thread.Start();
}
}
m2.Wait();
// all threads have locked in parallel
var maxAcquired = acquired;
m1.Set();
foreach (var thread in threads)
{
thread.Join();
}
Assert.AreEqual(threadCount, maxAcquired);
Assert.AreEqual(0, acquired);
for (var i = 0; i < threadCount; i++)
{
Assert.IsNull(exceptions[i]);
}
}
[Test]
public void ConcurrentWritersTest()
{
if (BaseTestDatabase.IsSqlite())
{
Assert.Ignore(
"This test doesn't work with Microsoft.Data.Sqlite in EFCore as we no longer use deferred transactions");
return;
}
const int threadCount = 3;
var threads = new Thread[threadCount];
var exceptions = new Exception[threadCount];
var locker = new object();
var acquired = 0;
int triedAcquiringWriteLock = 0;
var entered = 0;
var ms = new AutoResetEvent[threadCount];
for (var i = 0; i < threadCount; i++)
{
ms[i] = new AutoResetEvent(false);
}
var m1 = new ManualResetEventSlim(false);
var m2 = new ManualResetEventSlim(false);
for (var i = 0; i < threadCount; i++)
{
var ic = i; // capture
threads[i] = new Thread(() =>
{
using (var scope = EFScopeProvider.CreateScope())
{
try
{
lock (locker)
{
entered++;
if (entered == threadCount)
{
m1.Set();
}
}
ms[ic].WaitOne();
lock (locker)
{
triedAcquiringWriteLock++;
if (triedAcquiringWriteLock == threadCount)
{
m2.Set();
}
}
scope.Locks.EagerWriteLock(scope.InstanceId, Constants.Locks.Servers);
lock (locker)
{
acquired++;
}
ms[ic].WaitOne();
lock (locker)
{
acquired--;
}
}
catch (Exception e)
{
exceptions[ic] = e;
}
scope.Complete();
}
});
}
// ensure that current scope does not leak into starting threads
using (ExecutionContext.SuppressFlow())
{
foreach (var thread in threads)
{
thread.Start();
}
}
m1.Wait();
// all threads have entered
ms[0].Set(); // let 0 go
// TODO: This timing is flaky
Thread.Sleep(1000);
for (var i = 1; i < threadCount; i++)
{
ms[i].Set(); // let others go
}
m2.Wait();
// only 1 thread has locked
Assert.AreEqual(1, acquired);
for (var i = 0; i < threadCount; i++)
{
ms[i].Set(); // let all go
}
foreach (var thread in threads)
{
thread.Join();
}
Assert.AreEqual(0, acquired);
for (var i = 0; i < threadCount; i++)
{
Assert.IsNull(exceptions[i]);
}
}
[Retry(10)] // TODO make this test non-flaky.
[Test]
public void DeadLockTest()
{
if (BaseTestDatabase.IsSqlite())
{
Assert.Ignore("This test doesn't work with Microsoft.Data.Sqlite - SELECT * FROM sys.dm_tran_locks;");
return;
}
Exception e1 = null, e2 = null;
AutoResetEvent ev1 = new(false), ev2 = new(false);
// testing:
// two threads will each obtain exclusive write locks over two
// identical lock objects deadlock each other
var thread1 = new Thread(() => DeadLockTestThread(1, 2, ev1, ev2, ref e1));
var thread2 = new Thread(() => DeadLockTestThread(2, 1, ev2, ev1, ref e2));
// ensure that current scope does not leak into starting threads
using (ExecutionContext.SuppressFlow())
{
thread1.Start();
thread2.Start();
}
ev2.Set();
thread1.Join();
thread2.Join();
Assert.IsNotNull(e1);
if (e1 != null)
{
AssertIsDistributedLockingTimeoutException(e1);
}
// the assertion below depends on timing conditions - on a fast enough environment,
// thread1 dies (deadlock) and frees thread2, which succeeds - however on a slow
// environment (CI) both threads can end up dying due to deadlock - so, cannot test
// that e2 is null - but if it's not, can test that it's a timeout
//
//Assert.IsNull(e2);
if (e2 != null)
{
AssertIsDistributedLockingTimeoutException(e2);
}
}
private void AssertIsDistributedLockingTimeoutException(Exception e)
{
var sqlException = e as DistributedLockingTimeoutException;
Assert.IsNotNull(sqlException);
}
private void DeadLockTestThread(int id1, int id2, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception)
{
using var scope = EFScopeProvider.CreateScope();
try
{
otherEv.WaitOne();
Console.WriteLine($"[{id1}] WAIT {id1}");
scope.Locks.EagerWriteLock(scope.InstanceId, id1);
Console.WriteLine($"[{id1}] GRANT {id1}");
myEv.Set();
if (id1 == 1)
{
otherEv.WaitOne();
}
else
{
Thread.Sleep(5200); // wait for deadlock...
}
Console.WriteLine($"[{id1}] WAIT {id2}");
scope.Locks.EagerWriteLock(scope.InstanceId, id2);
Console.WriteLine($"[{id1}] GRANT {id2}");
}
catch (Exception e)
{
exception = e;
}
finally
{
scope.Complete();
}
}
}