with a custom IDatabaseFactory (mostly for testing). Changes AuditTrail to internal so the public way is just with the 'Audit' class. Fixed ThreadSafetyServiceTests which was failing with the new AuditTrail stuff because of the Database instances, this is not solved with the new PerThreadDatabaseFactory for the unit test. Created new 'UmbracoDatabase' object which inherits from the PetaPoco one so that we can future proof the implementation as we might want some custom logic on there. Now the IDatabaseFactory returns an UmbracoDatabase instead of just Database.
251 lines
7.5 KiB
C#
251 lines
7.5 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using NUnit.Framework;
|
|
using Umbraco.Core;
|
|
using Umbraco.Core.Models;
|
|
using Umbraco.Core.Persistence;
|
|
using Umbraco.Core.Persistence.UnitOfWork;
|
|
using Umbraco.Core.Publishing;
|
|
using Umbraco.Core.Services;
|
|
using Umbraco.Tests.TestHelpers;
|
|
using Umbraco.Tests.TestHelpers.Entities;
|
|
|
|
namespace Umbraco.Tests.Services
|
|
{
|
|
[TestFixture]
|
|
public class ThreadSafetyServiceTest : BaseDatabaseFactoryTest
|
|
{
|
|
private PerThreadPetaPocoUnitOfWorkProvider _uowProvider;
|
|
private PerThreadDatabaseFactory _dbFactory;
|
|
|
|
[SetUp]
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
//we need to use our own custom IDatabaseFactory for the DatabaseContext because we MUST ensure that
|
|
//a Database instance is created per thread, whereas the default implementation which will work in an HttpContext
|
|
//threading environment, or a single apartment threading environment will not work for this test because
|
|
//it is multi-threaded.
|
|
_dbFactory = new PerThreadDatabaseFactory();
|
|
//assign the custom factory to the new context and assign that to 'Current'
|
|
DatabaseContext.Current = new DatabaseContext(_dbFactory);
|
|
//overwrite the local object
|
|
DatabaseContext = DatabaseContext.Current;
|
|
|
|
//here we are going to override the ServiceContext because normally with our test cases we use a
|
|
//global Database object but this is NOT how it should work in the web world or in any multi threaded scenario.
|
|
//we need a new Database object for each thread.
|
|
_uowProvider = new PerThreadPetaPocoUnitOfWorkProvider(_dbFactory);
|
|
ServiceContext = new ServiceContext(_uowProvider, new FileUnitOfWorkProvider(), new PublishingStrategy());
|
|
|
|
CreateTestData();
|
|
}
|
|
|
|
[TearDown]
|
|
public override void TearDown()
|
|
{
|
|
_error = null;
|
|
|
|
//dispose!
|
|
_dbFactory.Dispose();
|
|
_uowProvider.Dispose();
|
|
|
|
base.TearDown();
|
|
|
|
ServiceContext = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to track exceptions during multi-threaded tests, volatile so that it is not locked in CPU registers.
|
|
/// </summary>
|
|
private volatile Exception _error = null;
|
|
|
|
private const int MaxThreadCount = 20;
|
|
|
|
[Test]
|
|
public void Ensure_All_Threads_Execute_Successfully_Content_Service()
|
|
{
|
|
//we will mimick the ServiceContext in that each repository in a service (i.e. ContentService) is a singleton
|
|
var contentService = (ContentService)ServiceContext.ContentService;
|
|
|
|
var threads = new List<Thread>();
|
|
|
|
Debug.WriteLine("Starting test...");
|
|
|
|
for (var i = 0; i < MaxThreadCount; i++)
|
|
{
|
|
var t = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
Debug.WriteLine("Created content on thread: " + Thread.CurrentThread.ManagedThreadId);
|
|
|
|
//create 2 content items
|
|
|
|
var content1 = contentService.CreateContent(-1, "umbTextpage", 0);
|
|
content1.Name = "test" + Guid.NewGuid();
|
|
Debug.WriteLine("Saving content1 on thread: " + Thread.CurrentThread.ManagedThreadId);
|
|
contentService.Save(content1);
|
|
|
|
Thread.Sleep(100); //quick pause for maximum overlap!
|
|
|
|
var content2 = contentService.CreateContent(-1, "umbTextpage", 0);
|
|
content2.Name = "test" + Guid.NewGuid();
|
|
Debug.WriteLine("Saving content2 on thread: " + Thread.CurrentThread.ManagedThreadId);
|
|
contentService.Save(content2);
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
_error = e;
|
|
}
|
|
});
|
|
threads.Add(t);
|
|
}
|
|
|
|
//start all threads
|
|
threads.ForEach(x => x.Start());
|
|
|
|
//wait for all to complete
|
|
threads.ForEach(x => x.Join());
|
|
|
|
//kill them all
|
|
threads.ForEach(x => x.Abort());
|
|
|
|
if (_error == null)
|
|
{
|
|
//now look up all items, there should be 40!
|
|
var items = contentService.GetRootContent();
|
|
Assert.AreEqual(40, items.Count());
|
|
}
|
|
else
|
|
{
|
|
Assert.Fail("ERROR! " + _error);
|
|
}
|
|
|
|
}
|
|
|
|
[Test]
|
|
public void Ensure_All_Threads_Execute_Successfully_Media_Service()
|
|
{
|
|
//we will mimick the ServiceContext in that each repository in a service (i.e. ContentService) is a singleton
|
|
var mediaService = (MediaService)ServiceContext.MediaService;
|
|
|
|
var threads = new List<Thread>();
|
|
|
|
Debug.WriteLine("Starting test...");
|
|
|
|
for (var i = 0; i < MaxThreadCount; i++)
|
|
{
|
|
var t = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
var folderMediaType = ServiceContext.ContentTypeService.GetMediaType(1031);
|
|
|
|
Debug.WriteLine("Created content on thread: " + Thread.CurrentThread.ManagedThreadId);
|
|
|
|
//create 2 content items
|
|
|
|
var folder1 = MockedMedia.CreateMediaFolder(folderMediaType, -1);
|
|
folder1.Name = "test" + Guid.NewGuid();
|
|
Debug.WriteLine("Saving folder1 on thread: " + Thread.CurrentThread.ManagedThreadId);
|
|
mediaService.Save(folder1, 0);
|
|
|
|
Thread.Sleep(100); //quick pause for maximum overlap!
|
|
|
|
var folder2 = MockedMedia.CreateMediaFolder(folderMediaType, -1);
|
|
folder2.Name = "test" + Guid.NewGuid();
|
|
Debug.WriteLine("Saving folder2 on thread: " + Thread.CurrentThread.ManagedThreadId);
|
|
mediaService.Save(folder2, 0);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_error = e;
|
|
}
|
|
});
|
|
threads.Add(t);
|
|
}
|
|
|
|
//start all threads
|
|
threads.ForEach(x => x.Start());
|
|
|
|
//wait for all to complete
|
|
threads.ForEach(x => x.Join());
|
|
|
|
//kill them all
|
|
threads.ForEach(x => x.Abort());
|
|
|
|
if (_error == null)
|
|
{
|
|
//now look up all items, there should be 40!
|
|
var items = mediaService.GetRootMedia();
|
|
Assert.AreEqual(40, items.Count());
|
|
}
|
|
else
|
|
{
|
|
Assert.Fail("ERROR! " + _error);
|
|
}
|
|
|
|
}
|
|
|
|
public void CreateTestData()
|
|
{
|
|
//Create and Save ContentType "umbTextpage" -> 1045
|
|
ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage", "Textpage");
|
|
contentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522");
|
|
ServiceContext.ContentTypeService.Save(contentType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Database object per thread, this mimics the web context which is per HttpContext and is required for the multi-threaded test
|
|
/// </summary>
|
|
internal class PerThreadDatabaseFactory : DisposableObject, IDatabaseFactory
|
|
{
|
|
private readonly ConcurrentDictionary<int, UmbracoDatabase> _databases = new ConcurrentDictionary<int, UmbracoDatabase>();
|
|
|
|
public UmbracoDatabase CreateDatabase()
|
|
{
|
|
var db = _databases.GetOrAdd(Thread.CurrentThread.ManagedThreadId, i => new UmbracoDatabase(Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName));
|
|
return db;
|
|
}
|
|
|
|
protected override void DisposeResources()
|
|
{
|
|
//dispose the databases
|
|
_databases.ForEach(x => x.Value.Dispose());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a UOW with a Database object per thread
|
|
/// </summary>
|
|
internal class PerThreadPetaPocoUnitOfWorkProvider : DisposableObject, IDatabaseUnitOfWorkProvider
|
|
{
|
|
private readonly PerThreadDatabaseFactory _dbFactory;
|
|
|
|
public PerThreadPetaPocoUnitOfWorkProvider(PerThreadDatabaseFactory dbFactory)
|
|
{
|
|
_dbFactory = dbFactory;
|
|
}
|
|
|
|
public IDatabaseUnitOfWork GetUnitOfWork()
|
|
{
|
|
//Create or get a database instance for this thread.
|
|
var db = _dbFactory.CreateDatabase();
|
|
return new PetaPocoUnitOfWork(db);
|
|
}
|
|
|
|
protected override void DisposeResources()
|
|
{
|
|
//dispose the databases
|
|
_dbFactory.Dispose();
|
|
}
|
|
}
|
|
|
|
}
|
|
} |