* 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.
404 lines
12 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|