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