Files
Umbraco-CMS/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs
2013-01-27 13:32:04 -01:00

265 lines
8.2 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;
using umbraco.editorControls.tinyMCE3;
using umbraco.interfaces;
namespace Umbraco.Tests.Services
{
[TestFixture, RequiresSTA]
public class ThreadSafetyServiceTest : BaseDatabaseFactoryTest
{
private PerThreadPetaPocoUnitOfWorkProvider _uowProvider;
private PerThreadDatabaseFactory _dbFactory;
[SetUp]
public override void Initialize()
{
//NOTE The DataTypesResolver is only necessary because we are using the Save method in the MediaService
//this ensures its reset
PluginManager.Current = new PluginManager();
//for testing, we'll specify which assemblies are scanned for the PluginTypeResolver
PluginManager.Current.AssembliesToScan = new[]
{
typeof(IDataType).Assembly,
typeof(tinyMCE3dataType).Assembly
};
DataTypesResolver.Current = new DataTypesResolver(
() => PluginManager.Current.ResolveDataTypes());
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();
//overwrite the local object
ApplicationContext.DatabaseContext = new DatabaseContext(_dbFactory);
//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);
ApplicationContext.Services = new ServiceContext(_uowProvider, new FileUnitOfWorkProvider(), new PublishingStrategy());
CreateTestData();
}
[TearDown]
public override void TearDown()
{
//reset the app context
DataTypesResolver.Reset();
_error = null;
//dispose!
_dbFactory.Dispose();
_uowProvider.Dispose();
base.TearDown();
}
/// <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
string name1 = "test" + Guid.NewGuid();
var content1 = contentService.CreateContent(name1, -1, "umbTextpage", 0);
Debug.WriteLine("Saving content1 on thread: " + Thread.CurrentThread.ManagedThreadId);
contentService.Save(content1);
Thread.Sleep(100); //quick pause for maximum overlap!
string name2 = "test" + Guid.NewGuid();
var content2 = contentService.CreateContent(name2, -1, "umbTextpage", 0);
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
{
Debug.WriteLine("Created content on thread: " + Thread.CurrentThread.ManagedThreadId);
//create 2 content items
string name1 = "test" + Guid.NewGuid();
var folder1 = mediaService.CreateMedia(name1, -1, "Folder", 0);
Debug.WriteLine("Saving folder1 on thread: " + Thread.CurrentThread.ManagedThreadId);
mediaService.Save(folder1, 0);
Thread.Sleep(100); //quick pause for maximum overlap!
string name = "test" + Guid.NewGuid();
var folder2 = mediaService.CreateMedia(name, -1, "Folder", 0);
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();
}
}
}
}