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;
}
///
/// Used to track exceptions during multi-threaded tests, volatile so that it is not locked in CPU registers.
///
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();
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();
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);
}
///
/// Creates a Database object per thread, this mimics the web context which is per HttpContext and is required for the multi-threaded test
///
internal class PerThreadDatabaseFactory : DisposableObject, IDatabaseFactory
{
private readonly ConcurrentDictionary _databases = new ConcurrentDictionary();
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());
}
}
///
/// Creates a UOW with a Database object per thread
///
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();
}
}
}
}