Files
Umbraco-CMS/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs
Shannon Deminick a0e4492a35 New implementation of IDatabaseFactory, allows setting the DatabaseContext.Current at runtime
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.
2012-12-12 03:47:04 +05:00

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();
}
}
}
}