Files
Umbraco-CMS/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs

1637 lines
70 KiB
C#
Raw Normal View History

2016-05-27 14:26:28 +02:00
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
2016-05-27 14:26:28 +02:00
using System.Linq;
using System.Web;
2016-05-27 14:26:28 +02:00
using CSharpTest.Net.Collections;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
2016-06-30 10:30:43 +02:00
using Umbraco.Core.IO;
2016-05-27 14:26:28 +02:00
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence;
2017-12-28 09:06:33 +01:00
using Umbraco.Core.Persistence.Dtos;
2016-05-27 14:26:28 +02:00
using Umbraco.Core.Persistence.Repositories;
2017-12-07 16:45:25 +01:00
using Umbraco.Core.Persistence.Repositories.Implement;
2017-05-12 14:49:44 +02:00
using Umbraco.Core.Scoping;
2016-05-27 14:26:28 +02:00
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
2017-12-28 09:18:09 +01:00
using Umbraco.Core.Services.Implement;
2019-03-05 11:15:30 +01:00
using Umbraco.Core.Strings;
2016-05-27 14:26:28 +02:00
using Umbraco.Web.Cache;
2016-06-30 10:30:43 +02:00
using Umbraco.Web.Install;
2016-05-27 14:26:28 +02:00
using Umbraco.Web.PublishedCache.NuCache.DataSource;
using Umbraco.Web.Routing;
2019-02-13 13:28:11 +01:00
using File = System.IO.File;
2016-05-27 14:26:28 +02:00
namespace Umbraco.Web.PublishedCache.NuCache
{
2017-10-31 12:48:24 +01:00
class PublishedSnapshotService : PublishedSnapshotServiceBase
2016-05-27 14:26:28 +02:00
{
private readonly ServiceContext _serviceContext;
2017-10-17 17:43:15 +02:00
private readonly IPublishedContentTypeFactory _publishedContentTypeFactory;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
2017-05-12 14:49:44 +02:00
private readonly IScopeProvider _scopeProvider;
private readonly IDataSource _dataSource;
2016-05-27 14:26:28 +02:00
private readonly ILogger _logger;
2017-12-12 15:04:13 +01:00
private readonly IDocumentRepository _documentRepository;
private readonly IMediaRepository _mediaRepository;
private readonly IMemberRepository _memberRepository;
private readonly IGlobalSettings _globalSettings;
private readonly ISiteDomainHelper _siteDomainHelper;
private readonly IEntityXmlSerializer _entitySerializer;
2018-04-30 21:29:49 +02:00
private readonly IDefaultCultureAccessor _defaultCultureAccessor;
2019-03-05 11:15:30 +01:00
private readonly UrlSegmentProviderCollection _urlSegmentProviders;
2016-05-27 14:26:28 +02:00
// volatile because we read it with no lock
private volatile bool _isReady;
2017-07-12 14:09:31 +02:00
private readonly ContentStore _contentStore;
private readonly ContentStore _mediaStore;
2016-05-27 14:26:28 +02:00
private readonly SnapDictionary<int, Domain> _domainStore;
private readonly object _storesLock = new object();
private BPlusTree<int, ContentNodeKit> _localContentDb;
private BPlusTree<int, ContentNodeKit> _localMediaDb;
private readonly bool _localDbExists;
// define constant - determines whether to use cache when previewing
// to store eg routes, property converted values, anything - caching
// means faster execution, but uses memory - not sure if we want it
// so making it configurable.
2016-05-27 14:26:28 +02:00
public static readonly bool FullCacheWhenPreviewing = true;
// define constant - determines whether to cache the published content
2017-10-31 12:48:24 +01:00
// objects (in the elements cache, or snapshot cache, depending on preview)
// or to re-fetch them all the time. caching is faster but uses more
2016-05-27 14:26:28 +02:00
// memory. not sure what we want.
public static readonly bool CachePublishedContentChildren = true;
// define constant - determines whether to cache the content cache root
2017-10-31 12:48:24 +01:00
// objects (in the elements cache, or snapshot cache, depending on preview)
// or to re-fetch them all the time. caching is faster but uses more
2016-05-27 14:26:28 +02:00
// memory - not sure what we want.
public static readonly bool CacheContentCacheRoots = true;
#region Constructors
2016-07-01 16:27:23 +02:00
//private static int _singletonCheck;
public PublishedSnapshotService(Options options, IMainDom mainDom, IRuntimeState runtime,
2018-03-30 14:00:44 +02:00
ServiceContext serviceContext, IPublishedContentTypeFactory publishedContentTypeFactory, IdkMap idkMap,
2018-04-30 21:29:49 +02:00
IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor,
IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IScopeProvider scopeProvider,
IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository,
2018-04-30 21:29:49 +02:00
IDefaultCultureAccessor defaultCultureAccessor,
IDataSource dataSource, IGlobalSettings globalSettings, ISiteDomainHelper siteDomainHelper,
2019-03-05 11:15:30 +01:00
IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory,
UrlSegmentProviderCollection urlSegmentProviders)
2018-04-30 21:29:49 +02:00
: base(publishedSnapshotAccessor, variationContextAccessor)
2016-05-27 14:26:28 +02:00
{
2016-07-01 16:27:23 +02:00
//if (Interlocked.Increment(ref _singletonCheck) > 1)
// throw new Exception("Singleton must be instantiated only once!");
2016-07-01 16:27:23 +02:00
2016-05-27 14:26:28 +02:00
_serviceContext = serviceContext;
2017-10-17 17:43:15 +02:00
_publishedContentTypeFactory = publishedContentTypeFactory;
_umbracoContextAccessor = umbracoContextAccessor;
_dataSource = dataSource;
2016-05-27 14:26:28 +02:00
_logger = logger;
2017-05-12 14:49:44 +02:00
_scopeProvider = scopeProvider;
2017-12-12 15:04:13 +01:00
_documentRepository = documentRepository;
_mediaRepository = mediaRepository;
_memberRepository = memberRepository;
2018-04-30 21:29:49 +02:00
_defaultCultureAccessor = defaultCultureAccessor;
_globalSettings = globalSettings;
_siteDomainHelper = siteDomainHelper;
2019-03-05 11:15:30 +01:00
_urlSegmentProviders = urlSegmentProviders;
2016-05-27 14:26:28 +02:00
2019-01-16 11:37:20 +01:00
// we need an Xml serializer here so that the member cache can support XPath,
// for members this is done by navigating the serialized-to-xml member
_entitySerializer = entitySerializer;
2016-05-27 14:26:28 +02:00
// we always want to handle repository events, configured or not
// assuming no repository event will trigger before the whole db is ready
// (ideally we'd have Upgrading.App vs Upgrading.Data application states...)
InitializeRepositoryEvents();
// however, the cache is NOT available until we are configured, because loading
// content (and content types) from database cannot be consistent (see notes in "Handle
// Notifications" region), so
// - notifications will be ignored
2017-10-31 12:48:24 +01:00
// - trying to obtain a published snapshot from the service will throw
if (runtime.Level != RuntimeLevel.Run)
2016-05-27 14:26:28 +02:00
return;
2017-12-12 15:04:13 +01:00
if (options.IgnoreLocalDb == false)
2016-05-27 14:26:28 +02:00
{
var registered = mainDom.Register(
null,
() =>
{
lock (_storesLock)
{
_contentStore.ReleaseLocalDb();
_localContentDb = null;
_mediaStore.ReleaseLocalDb();
_localMediaDb = null;
}
});
if (registered)
{
2019-02-13 13:28:11 +01:00
var path = GetLocalFilesPath();
var localContentDbPath = Path.Combine(path, "NuCache.Content.db");
var localMediaDbPath = Path.Combine(path, "NuCache.Media.db");
2016-05-27 14:26:28 +02:00
_localDbExists = System.IO.File.Exists(localContentDbPath) && System.IO.File.Exists(localMediaDbPath);
// if both local databases exist then GetTree will open them, else new databases will be created
2016-05-27 14:26:28 +02:00
_localContentDb = BTree.GetTree(localContentDbPath, _localDbExists);
_localMediaDb = BTree.GetTree(localMediaDbPath, _localDbExists);
}
// stores are created with a db so they can write to it, but they do not read from it,
// stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to
// figure out whether it can read the databases or it should populate them from sql
_contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger, _localContentDb);
_mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger, _localMediaDb);
2016-05-27 14:26:28 +02:00
}
else
{
_contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger);
_mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, _umbracoContextAccessor, logger);
2016-05-27 14:26:28 +02:00
}
_domainStore = new SnapDictionary<int, Domain>();
2019-03-05 11:15:30 +01:00
publishedModelFactory.WithSafeLiveFactory(LoadCaches);
2018-03-30 14:00:44 +02:00
2019-02-10 11:25:46 +01:00
Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default;
int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? default;
2018-03-30 14:00:44 +02:00
if (idkMap != null)
{
2019-02-10 11:25:46 +01:00
idkMap.SetMapper(UmbracoObjectTypes.Document, id => GetUid(_contentStore, id), uid => GetId(_contentStore, uid));
idkMap.SetMapper(UmbracoObjectTypes.Media, id => GetUid(_mediaStore, id), uid => GetId(_mediaStore, uid));
2018-03-30 14:00:44 +02:00
}
2016-05-27 14:26:28 +02:00
}
private void LoadCaches()
2016-05-27 14:26:28 +02:00
{
lock (_storesLock)
{
// populate the stores
try
{
2018-04-29 20:02:38 +02:00
if (_localDbExists)
2016-05-27 14:26:28 +02:00
{
LockAndLoadContent(LoadContentFromLocalDbLocked);
LockAndLoadMedia(LoadMediaFromLocalDbLocked);
}
else
{
LockAndLoadContent(LoadContentFromDatabaseLocked);
LockAndLoadMedia(LoadMediaFromDatabaseLocked);
}
LockAndLoadDomains();
}
catch (Exception ex)
2016-05-27 14:26:28 +02:00
{
_logger.Fatal<PublishedSnapshotService>(ex, "Panic, exception while loading cache data.");
2016-05-27 14:26:28 +02:00
}
// finally, cache is ready!
2016-05-27 14:26:28 +02:00
_isReady = true;
}
}
private void InitializeRepositoryEvents()
{
// TODO: The reason these events are in the repository is for legacy, the events should exist at the service
// level now since we can fire these events within the transaction... so move the events to service level
2016-05-27 14:26:28 +02:00
// plug repository event handlers
// these trigger within the transaction to ensure consistency
// and are used to maintain the central, database-level XML cache
2017-12-12 15:04:13 +01:00
DocumentRepository.ScopeEntityRemove += OnContentRemovingEntity;
2016-05-27 14:26:28 +02:00
//ContentRepository.RemovedVersion += OnContentRemovedVersion;
2017-12-12 15:04:13 +01:00
DocumentRepository.ScopedEntityRefresh += OnContentRefreshedEntity;
MediaRepository.ScopeEntityRemove += OnMediaRemovingEntity;
2016-05-27 14:26:28 +02:00
//MediaRepository.RemovedVersion += OnMediaRemovedVersion;
2017-12-12 15:04:13 +01:00
MediaRepository.ScopedEntityRefresh += OnMediaRefreshedEntity;
MemberRepository.ScopeEntityRemove += OnMemberRemovingEntity;
2016-05-27 14:26:28 +02:00
//MemberRepository.RemovedVersion += OnMemberRemovedVersion;
2017-12-12 15:04:13 +01:00
MemberRepository.ScopedEntityRefresh += OnMemberRefreshedEntity;
2016-05-27 14:26:28 +02:00
// plug
ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity;
MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity;
MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity;
2016-05-27 14:26:28 +02:00
}
2017-07-17 17:59:46 +02:00
private void TearDownRepositoryEvents()
{
2017-12-12 15:04:13 +01:00
DocumentRepository.ScopeEntityRemove -= OnContentRemovingEntity;
2017-07-17 17:59:46 +02:00
//ContentRepository.RemovedVersion -= OnContentRemovedVersion;
2017-12-12 15:04:13 +01:00
DocumentRepository.ScopedEntityRefresh -= OnContentRefreshedEntity;
MediaRepository.ScopeEntityRemove -= OnMediaRemovingEntity;
2017-07-17 17:59:46 +02:00
//MediaRepository.RemovedVersion -= OnMediaRemovedVersion;
2017-12-12 15:04:13 +01:00
MediaRepository.ScopedEntityRefresh -= OnMediaRefreshedEntity;
MemberRepository.ScopeEntityRemove -= OnMemberRemovingEntity;
2017-07-17 17:59:46 +02:00
//MemberRepository.RemovedVersion -= OnMemberRemovedVersion;
2017-12-12 15:04:13 +01:00
MemberRepository.ScopedEntityRefresh -= OnMemberRefreshedEntity;
2017-07-17 17:59:46 +02:00
ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity;
MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity;
MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity;
2017-07-17 17:59:46 +02:00
}
public override void Dispose()
{
TearDownRepositoryEvents();
base.Dispose();
}
2016-05-27 14:26:28 +02:00
public class Options
{
2017-10-31 12:48:24 +01:00
// disabled: prevents the published snapshot from updating and exposing changes
// or even creating a new published snapshot to see changes, uses old cache = bad
2017-07-19 19:59:46 +02:00
//
2017-10-31 12:48:24 +01:00
//// indicates that the snapshot cache should reuse the application request cache
//// otherwise a new cache object would be created for the snapshot specifically,
//// which is the default - web boot manager uses this to optimize facades
2017-10-31 12:48:24 +01:00
//public bool PublishedSnapshotCacheIsApplicationRequestCache;
2016-05-27 14:26:28 +02:00
public bool IgnoreLocalDb;
}
#endregion
2019-02-13 13:28:11 +01:00
#region Local files
private string GetLocalFilesPath()
{
2019-02-15 08:46:57 +01:00
var path = Path.Combine(_globalSettings.LocalTempPath, "NuCache");
2019-02-13 13:28:11 +01:00
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
return path;
}
private void DeleteLocalFilesForContent()
{
if (_isReady && _localContentDb != null)
throw new InvalidOperationException("Cannot delete local files while the cache uses them.");
var path = GetLocalFilesPath();
var localContentDbPath = Path.Combine(path, "NuCache.Content.db");
if (File.Exists(localContentDbPath))
File.Delete(localContentDbPath);
}
private void DeleteLocalFilesForMedia()
{
if (_isReady && _localMediaDb != null)
throw new InvalidOperationException("Cannot delete local files while the cache uses them.");
var path = GetLocalFilesPath();
var localMediaDbPath = Path.Combine(path, "NuCache.Media.db");
if (File.Exists(localMediaDbPath))
File.Delete(localMediaDbPath);
}
#endregion
2016-06-30 10:30:43 +02:00
#region Environment
public override bool EnsureEnvironment(out IEnumerable<string> errors)
{
// must have app_data and be able to write files into it
2019-02-13 13:28:11 +01:00
var ok = FilePermissionHelper.TryCreateDirectory(GetLocalFilesPath());
errors = ok ? Enumerable.Empty<string>() : new[] { "NuCache local files." };
2016-06-30 10:30:43 +02:00
return ok;
}
#endregion
2016-05-27 14:26:28 +02:00
#region Populate Stores
// sudden panic... but in RepeatableRead can a content that I haven't already read, be removed
// before I read it? NO! because the WHOLE content tree is read-locked using WithReadLocked.
// don't panic.
2017-12-12 15:04:13 +01:00
private void LockAndLoadContent(Action<IScope> action)
2016-05-27 14:26:28 +02:00
{
2019-02-22 15:27:22 +01:00
// first get a writer, then a scope
// if there already is a scope, the writer will attach to it
// otherwise, it will only exist here - cheap
2019-03-14 19:48:44 +01:00
using (_contentStore.GetScopedWriteLock(_scopeProvider))
2019-02-22 15:27:22 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2019-02-22 15:27:22 +01:00
scope.ReadLock(Constants.Locks.ContentTree);
action(scope);
scope.Complete();
}
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void LoadContentFromDatabaseLocked(IScope scope)
2016-05-27 14:26:28 +02:00
{
// locks:
// contentStore is wlocked (1 thread)
// content (and types) are read-locked
var contentTypes = _serviceContext.ContentTypeService.GetAll()
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
2016-05-27 14:26:28 +02:00
_contentStore.UpdateContentTypes(null, contentTypes, null);
_localContentDb?.Clear();
_logger.Debug<PublishedSnapshotService>("Loading content from database...");
2016-05-27 14:26:28 +02:00
var sw = Stopwatch.StartNew();
2017-12-12 15:04:13 +01:00
var kits = _dataSource.GetAllContentSources(scope);
2016-05-27 14:26:28 +02:00
_contentStore.SetAll(kits);
sw.Stop();
_logger.Debug<PublishedSnapshotService>("Loaded content from database ({Duration}ms)", sw.ElapsedMilliseconds);
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void LoadContentFromLocalDbLocked(IScope scope)
2016-05-27 14:26:28 +02:00
{
var contentTypes = _serviceContext.ContentTypeService.GetAll()
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
2016-05-27 14:26:28 +02:00
_contentStore.UpdateContentTypes(null, contentTypes, null);
_logger.Debug<PublishedSnapshotService>("Loading content from local db...");
2016-05-27 14:26:28 +02:00
var sw = Stopwatch.StartNew();
var kits = _localContentDb.Select(x => x.Value).OrderBy(x => x.Node.Level);
2016-05-27 14:26:28 +02:00
_contentStore.SetAll(kits);
sw.Stop();
_logger.Debug<PublishedSnapshotService>("Loaded content from local db ({Duration}ms)", sw.ElapsedMilliseconds);
2016-05-27 14:26:28 +02:00
}
// keep these around - might be useful
//private void LoadContentBranch(IContent content)
//{
// LoadContent(content);
// foreach (var child in content.Children())
// LoadContentBranch(child);
//}
//private void LoadContent(IContent content)
//{
// var contentService = _serviceContext.ContentService as ContentService;
// var newest = content;
// var published = newest.Published
// ? newest
// : (newest.HasPublishedVersion ? contentService.GetByVersion(newest.PublishedVersionGuid) : null);
// var contentNode = CreateContentNode(newest, published);
// _contentStore.Set(contentNode);
//}
2017-12-12 15:04:13 +01:00
private void LockAndLoadMedia(Action<IScope> action)
2016-05-27 14:26:28 +02:00
{
2019-02-22 15:27:22 +01:00
// see note in LockAndLoadContent
2019-03-14 19:48:44 +01:00
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
2019-02-22 15:27:22 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2019-02-22 15:27:22 +01:00
scope.ReadLock(Constants.Locks.MediaTree);
action(scope);
scope.Complete();
}
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void LoadMediaFromDatabaseLocked(IScope scope)
2016-05-27 14:26:28 +02:00
{
// locks & notes: see content
var mediaTypes = _serviceContext.MediaTypeService.GetAll()
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
2016-05-27 14:26:28 +02:00
_mediaStore.UpdateContentTypes(null, mediaTypes, null);
_localMediaDb?.Clear();
_logger.Debug<PublishedSnapshotService>("Loading media from database...");
2016-05-27 14:26:28 +02:00
var sw = Stopwatch.StartNew();
2017-12-12 15:04:13 +01:00
var kits = _dataSource.GetAllMediaSources(scope);
2016-05-27 14:26:28 +02:00
_mediaStore.SetAll(kits);
sw.Stop();
_logger.Debug<PublishedSnapshotService>("Loaded media from database ({Duration}ms)", sw.ElapsedMilliseconds);
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void LoadMediaFromLocalDbLocked(IScope scope)
2016-05-27 14:26:28 +02:00
{
var mediaTypes = _serviceContext.MediaTypeService.GetAll()
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
2016-05-27 14:26:28 +02:00
_mediaStore.UpdateContentTypes(null, mediaTypes, null);
_logger.Debug<PublishedSnapshotService>("Loading media from local db...");
2016-05-27 14:26:28 +02:00
var sw = Stopwatch.StartNew();
var kits = _localMediaDb.Select(x => x.Value);
_mediaStore.SetAll(kits);
sw.Stop();
_logger.Debug<PublishedSnapshotService>("Loaded media from local db ({Duration}ms)", sw.ElapsedMilliseconds);
2016-05-27 14:26:28 +02:00
}
// keep these around - might be useful
//private void LoadMediaBranch(IMedia media)
//{
// LoadMedia(media);
// foreach (var child in media.Children())
// LoadMediaBranch(child);
//}
//private void LoadMedia(IMedia media)
//{
// var mediaType = _contentTypeCache.Get(PublishedItemType.Media, media.ContentTypeId);
// var mediaData = new ContentData
// {
// Name = media.Name,
// Published = true,
// Version = media.Version,
// VersionDate = media.UpdateDate,
// WriterId = media.CreatorId, // what else?
// TemplateId = -1, // have none
// Properties = GetPropertyValues(media)
// };
// var mediaNode = new ContentNode(media.Id, mediaType,
// media.Level, media.Path, media.SortOrder,
// media.ParentId, media.CreateDate, media.CreatorId,
// null, mediaData);
// _mediaStore.Set(mediaNode);
//}
//private Dictionary<string, object> GetPropertyValues(IContentBase content)
//{
// var propertyEditorResolver = PropertyEditorResolver.Current; // should inject
// return content
// .Properties
// .Select(property =>
// {
// var e = propertyEditorResolver.GetByAlias(property.PropertyType.PropertyEditorAlias);
// var v = e == null
// ? property.Value
// : e.ValueEditor.ConvertDbToString(property, property.PropertyType, _serviceContext.DataTypeService);
// return new KeyValuePair<string, object>(property.Alias, v);
// })
// .ToDictionary(x => x.Key, x => x.Value);
//}
//private ContentData CreateContentData(IContent content)
//{
// return new ContentData
// {
// Name = content.Name,
// Published = content.Published,
// Version = content.Version,
// VersionDate = content.UpdateDate,
// WriterId = content.WriterId,
// TemplateId = content.Template == null ? -1 : content.Template.Id,
// Properties = GetPropertyValues(content)
// };
//}
//private ContentNode CreateContentNode(IContent newest, IContent published)
//{
// var contentType = _contentTypeCache.Get(PublishedItemType.Content, newest.ContentTypeId);
// var draftData = newest.Published
// ? null
// : CreateContentData(newest);
// var publishedData = newest.Published
// ? CreateContentData(newest)
// : (published == null ? null : CreateContentData(published));
// var contentNode = new ContentNode(newest.Id, contentType,
// newest.Level, newest.Path, newest.SortOrder,
// newest.ParentId, newest.CreateDate, newest.CreatorId,
// draftData, publishedData);
// return contentNode;
//}
private void LockAndLoadDomains()
{
2019-02-22 15:27:22 +01:00
// see note in LockAndLoadContent
2019-03-14 19:48:44 +01:00
using (_domainStore.GetScopedWriteLock(_scopeProvider))
2019-02-22 15:27:22 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2019-02-22 15:27:22 +01:00
scope.ReadLock(Constants.Locks.Domains);
LoadDomainsLocked();
scope.Complete();
}
2016-05-27 14:26:28 +02:00
}
private void LoadDomainsLocked()
{
var domains = _serviceContext.DomainService.GetAll(true);
foreach (var domain in domains
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
2018-04-26 16:03:08 +02:00
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard)))
2016-05-27 14:26:28 +02:00
{
_domainStore.Set(domain.Id, domain);
}
}
#endregion
#region Handle Notifications
// note: if the service is not ready, ie _isReady is false, then notifications are ignored
2017-10-31 12:48:24 +01:00
// SetUmbracoVersionStep issues a DistributedCache.Instance.RefreshAll...() call which should cause
2016-05-27 14:26:28 +02:00
// the entire content, media etc caches to reload from database -- and then the app restarts -- however,
// at the time SetUmbracoVersionStep runs, Umbraco is not fully initialized and therefore some property
// value converters, etc are not registered, and rebuilding the NuCache may not work properly.
//
// More details: ApplicationContext.IsConfigured being false, ApplicationEventHandler.ExecuteWhen... is
// called and in most cases events are skipped, so property value converters are not registered or
// removed, so PublishedPropertyType either initializes with the wrong converter, or throws because it
// detects more than one converter for a property type.
//
// It's not an issue for XmlStore - the app restart takes place *after* the install has refreshed the
// cache, and XmlStore just writes a new umbraco.config file upon RefreshAll, so that's OK.
//
// But for NuCache... we cannot rebuild the cache now. So it will NOT work and we are not fixing it,
// because now we should ALWAYS run with the database server messenger, and then the RefreshAll will
// be processed as soon as we are configured and the messenger processes instructions.
// note: notifications for content type and data type changes should be invoked with the
// pure live model factory, if any, locked and refreshed - see ContentTypeCacheRefresher and
// DataTypeCacheRefresher
2016-05-27 14:26:28 +02:00
public override void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged)
{
2019-02-13 13:28:11 +01:00
// no cache, trash everything
2016-05-27 14:26:28 +02:00
if (_isReady == false)
{
2019-02-13 13:28:11 +01:00
DeleteLocalFilesForContent();
draftChanged = publishedChanged = true;
2016-05-27 14:26:28 +02:00
return;
}
2019-03-14 19:48:44 +01:00
using (_contentStore.GetScopedWriteLock(_scopeProvider))
2016-05-27 14:26:28 +02:00
{
NotifyLocked(payloads, out bool draftChanged2, out bool publishedChanged2);
draftChanged = draftChanged2;
publishedChanged = publishedChanged2;
}
2016-05-27 14:26:28 +02:00
if (draftChanged || publishedChanged)
2018-04-29 20:02:38 +02:00
((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync();
2016-05-27 14:26:28 +02:00
}
private void NotifyLocked(IEnumerable<ContentCacheRefresher.JsonPayload> payloads, out bool draftChanged, out bool publishedChanged)
{
publishedChanged = false;
draftChanged = false;
// locks:
// content (and content types) are read-locked while reading content
// contentStore is wlocked (so readable, only no new views)
// and it can be wlocked by 1 thread only at a time
// contentStore is write-locked during changes
foreach (var payload in payloads)
{
_logger.Debug<PublishedSnapshotService>("Notified {ChangeTypes} for content {ContentId}", payload.ChangeTypes, payload.Id);
2016-05-27 14:26:28 +02:00
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.ContentTree);
LoadContentFromDatabaseLocked(scope);
scope.Complete();
2016-05-27 14:26:28 +02:00
}
draftChanged = publishedChanged = true;
continue;
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
if (_contentStore.Clear(payload.Id))
draftChanged = publishedChanged = true;
continue;
}
if (payload.ChangeTypes.HasTypesNone(TreeChangeTypes.RefreshNode | TreeChangeTypes.RefreshBranch))
{
// ?!
continue;
}
// TODO: should we do some RV check here? (later)
2016-05-27 14:26:28 +02:00
var capture = payload;
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.ContentTree);
2016-05-27 14:26:28 +02:00
if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
// ?? should we do some RV check here?
2017-12-12 15:04:13 +01:00
var kits = _dataSource.GetBranchContentSources(scope, capture.Id);
2016-05-27 14:26:28 +02:00
_contentStore.SetBranch(capture.Id, kits);
}
else
{
// ?? should we do some RV check here?
2017-12-12 15:04:13 +01:00
var kit = _dataSource.GetContentSource(scope, capture.Id);
2016-05-27 14:26:28 +02:00
if (kit.IsEmpty)
{
_contentStore.Clear(capture.Id);
}
else
{
_contentStore.Set(kit);
}
}
2017-12-12 15:04:13 +01:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
// ?? cannot tell really because we're not doing RV checks
draftChanged = publishedChanged = true;
}
}
public override void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged)
{
2019-02-13 13:28:11 +01:00
// no cache, trash everything
2016-05-27 14:26:28 +02:00
if (_isReady == false)
{
2019-02-13 13:28:11 +01:00
DeleteLocalFilesForMedia();
anythingChanged = true;
2016-05-27 14:26:28 +02:00
return;
}
2019-03-14 19:48:44 +01:00
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
2016-05-27 14:26:28 +02:00
{
NotifyLocked(payloads, out bool anythingChanged2);
anythingChanged = anythingChanged2;
}
2016-05-27 14:26:28 +02:00
if (anythingChanged)
2018-04-29 20:02:38 +02:00
((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync();
2016-05-27 14:26:28 +02:00
}
private void NotifyLocked(IEnumerable<MediaCacheRefresher.JsonPayload> payloads, out bool anythingChanged)
{
anythingChanged = false;
// locks:
// see notes for content cache refresher
foreach (var payload in payloads)
{
_logger.Debug<PublishedSnapshotService>("Notified {ChangeTypes} for media {MediaId}", payload.ChangeTypes, payload.Id);
2016-05-27 14:26:28 +02:00
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.MediaTree);
LoadMediaFromDatabaseLocked(scope);
scope.Complete();
2016-05-27 14:26:28 +02:00
}
anythingChanged = true;
continue;
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
if (_mediaStore.Clear(payload.Id))
anythingChanged = true;
continue;
}
if (payload.ChangeTypes.HasTypesNone(TreeChangeTypes.RefreshNode | TreeChangeTypes.RefreshBranch))
{
// ?!
continue;
}
// TODO: should we do some RV checks here? (later)
2016-05-27 14:26:28 +02:00
var capture = payload;
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.MediaTree);
2016-05-27 14:26:28 +02:00
if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
// ?? should we do some RV check here?
2017-12-12 15:04:13 +01:00
var kits = _dataSource.GetBranchMediaSources(scope, capture.Id);
2016-05-27 14:26:28 +02:00
_mediaStore.SetBranch(capture.Id, kits);
}
else
{
// ?? should we do some RV check here?
2017-12-12 15:04:13 +01:00
var kit = _dataSource.GetMediaSource(scope, capture.Id);
2016-05-27 14:26:28 +02:00
if (kit.IsEmpty)
{
_mediaStore.Clear(capture.Id);
}
else
{
_mediaStore.Set(kit);
}
}
2017-12-12 15:04:13 +01:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
// ?? cannot tell really because we're not doing RV checks
anythingChanged = true;
}
}
public override void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads)
{
// no cache, nothing we can do
if (_isReady == false)
return;
foreach (var payload in payloads)
_logger.Debug<PublishedSnapshotService>("Notified {ChangeTypes} for {ItemType} {ItemId}", payload.ChangeTypes, payload.ItemType, payload.Id);
2016-05-27 14:26:28 +02:00
2017-07-17 17:59:46 +02:00
Notify<IContentType>(_contentStore, payloads, RefreshContentTypesLocked);
Notify<IMediaType>(_mediaStore, payloads, RefreshMediaTypesLocked);
2016-05-27 14:26:28 +02:00
2018-04-29 20:02:38 +02:00
((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync();
2017-07-17 17:59:46 +02:00
}
2016-05-27 14:26:28 +02:00
2017-07-17 17:59:46 +02:00
private void Notify<T>(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action<IEnumerable<int>, IEnumerable<int>, IEnumerable<int>, IEnumerable<int>> action)
{
var nameOfT = typeof(T).Name;
2016-05-27 14:26:28 +02:00
2017-07-17 17:59:46 +02:00
var removedIds = new List<int>();
var refreshedIds = new List<int>();
var otherIds = new List<int>();
var newIds = new List<int>();
2016-05-27 14:26:28 +02:00
2017-07-17 17:59:46 +02:00
foreach (var payload in payloads)
{
if (payload.ItemType != nameOfT) continue;
if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove))
removedIds.Add(payload.Id);
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain))
refreshedIds.Add(payload.Id);
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther))
otherIds.Add(payload.Id);
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Create))
newIds.Add(payload.Id);
}
2016-05-27 14:26:28 +02:00
if (removedIds.Count == 0 && refreshedIds.Count == 0 && otherIds.Count == 0 && newIds.Count == 0) return;
2016-05-27 14:26:28 +02:00
2019-03-14 19:48:44 +01:00
using (store.GetScopedWriteLock(_scopeProvider))
2017-07-17 17:59:46 +02:00
{
// ReSharper disable AccessToModifiedClosure
action(removedIds, refreshedIds, otherIds, newIds);
// ReSharper restore AccessToModifiedClosure
}
2016-05-27 14:26:28 +02:00
}
public override void Notify(DataTypeCacheRefresher.JsonPayload[] payloads)
{
// no cache, nothing we can do
if (_isReady == false)
return;
var idsA = payloads.Select(x => x.Id).ToArray();
foreach (var payload in payloads)
_logger.Debug<PublishedSnapshotService>("Notified {RemovedStatus} for data type {DataTypeId}",
payload.Removed ? "Removed" : "Refreshed",
payload.Id);
2016-05-27 14:26:28 +02:00
2019-03-14 19:48:44 +01:00
using (_contentStore.GetScopedWriteLock(_scopeProvider))
using (_mediaStore.GetScopedWriteLock(_scopeProvider))
{
// TODO: need to add a datatype lock
2018-02-09 14:34:28 +01:00
// this is triggering datatypes reload in the factory, and right after we create some
// content types by loading them ... there's a race condition here, which would require
// some locking on datatypes
_publishedContentTypeFactory.NotifyDataTypeChanges(idsA);
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
{
scope.ReadLock(Constants.Locks.ContentTree);
_contentStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Content, id));
scope.Complete();
}
2016-05-27 14:26:28 +02:00
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
{
scope.ReadLock(Constants.Locks.MediaTree);
_mediaStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Media, id));
scope.Complete();
}
2017-12-12 15:04:13 +01:00
}
2016-05-27 14:26:28 +02:00
2018-04-29 20:02:38 +02:00
((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync();
2016-05-27 14:26:28 +02:00
}
public override void Notify(DomainCacheRefresher.JsonPayload[] payloads)
{
// no cache, nothing we can do
if (_isReady == false)
return;
2019-02-22 15:27:22 +01:00
// see note in LockAndLoadContent
2019-03-14 19:48:44 +01:00
using (_domainStore.GetScopedWriteLock(_scopeProvider))
2016-05-27 14:26:28 +02:00
{
foreach (var payload in payloads)
{
switch (payload.ChangeType)
{
case DomainChangeTypes.RefreshAll:
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.Domains);
2016-05-27 14:26:28 +02:00
LoadDomainsLocked();
2017-12-12 15:04:13 +01:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
break;
case DomainChangeTypes.Remove:
2016-05-27 14:26:28 +02:00
_domainStore.Clear(payload.Id);
break;
case DomainChangeTypes.Refresh:
2016-05-27 14:26:28 +02:00
var domain = _serviceContext.DomainService.GetById(payload.Id);
if (domain == null) continue;
if (domain.RootContentId.HasValue == false) continue; // anomaly
if (domain.LanguageIsoCode.IsNullOrWhiteSpace()) continue; // anomaly
var culture = CultureInfo.GetCultureInfo(domain.LanguageIsoCode);
2018-04-26 16:03:08 +02:00
_domainStore.Set(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard));
2016-05-27 14:26:28 +02:00
break;
}
}
}
2016-05-27 14:26:28 +02:00
}
#endregion
#region Content Types
2017-10-17 17:43:15 +02:00
private IEnumerable<PublishedContentType> CreateContentTypes(PublishedItemType itemType, int[] ids)
2016-05-27 14:26:28 +02:00
{
2017-10-17 17:43:15 +02:00
// XxxTypeService.GetAll(empty) returns everything!
if (ids.Length == 0)
return Enumerable.Empty<PublishedContentType>();
2016-05-27 14:26:28 +02:00
IEnumerable<IContentTypeComposition> contentTypes;
switch (itemType)
{
case PublishedItemType.Content:
contentTypes = _serviceContext.ContentTypeService.GetAll(ids);
break;
case PublishedItemType.Media:
contentTypes = _serviceContext.MediaTypeService.GetAll(ids);
break;
case PublishedItemType.Member:
contentTypes = _serviceContext.MemberTypeService.GetAll(ids);
break;
default:
throw new ArgumentOutOfRangeException(nameof(itemType));
}
// some may be missing - not checking here
return contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x));
2016-05-27 14:26:28 +02:00
}
private PublishedContentType CreateContentType(PublishedItemType itemType, int id)
{
IContentTypeComposition contentType;
switch (itemType)
{
case PublishedItemType.Content:
contentType = _serviceContext.ContentTypeService.Get(id);
break;
case PublishedItemType.Media:
contentType = _serviceContext.MediaTypeService.Get(id);
break;
case PublishedItemType.Member:
contentType = _serviceContext.MemberTypeService.Get(id);
break;
default:
throw new ArgumentOutOfRangeException(nameof(itemType));
}
return contentType == null ? null : _publishedContentTypeFactory.CreateContentType(contentType);
2016-05-27 14:26:28 +02:00
}
2017-07-17 17:59:46 +02:00
private void RefreshContentTypesLocked(IEnumerable<int> removedIds, IEnumerable<int> refreshedIds, IEnumerable<int> otherIds, IEnumerable<int> newIds)
2016-05-27 14:26:28 +02:00
{
// locks:
// content (and content types) are read-locked while reading content
// contentStore is wlocked (so readable, only no new views)
// and it can be wlocked by 1 thread only at a time
var refreshedIdsA = refreshedIds.ToArray();
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.ContentTypes);
2019-02-21 19:06:41 +01:00
2016-05-27 14:26:28 +02:00
var typesA = CreateContentTypes(PublishedItemType.Content, refreshedIdsA).ToArray();
2017-12-12 15:04:13 +01:00
var kits = _dataSource.GetTypeContentSources(scope, refreshedIdsA);
2019-02-21 19:06:41 +01:00
2016-05-27 14:26:28 +02:00
_contentStore.UpdateContentTypes(removedIds, typesA, kits);
2017-07-17 17:59:46 +02:00
_contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray()).ToArray());
_contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray()).ToArray());
2017-12-12 15:04:13 +01:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
}
2017-07-17 17:59:46 +02:00
private void RefreshMediaTypesLocked(IEnumerable<int> removedIds, IEnumerable<int> refreshedIds, IEnumerable<int> otherIds, IEnumerable<int> newIds)
2016-05-27 14:26:28 +02:00
{
// locks:
// media (and content types) are read-locked while reading media
// mediaStore is wlocked (so readable, only no new views)
// and it can be wlocked by 1 thread only at a time
var refreshedIdsA = refreshedIds.ToArray();
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.MediaTypes);
2019-02-21 19:06:41 +01:00
2016-05-27 14:26:28 +02:00
var typesA = CreateContentTypes(PublishedItemType.Media, refreshedIdsA).ToArray();
2017-12-12 15:04:13 +01:00
var kits = _dataSource.GetTypeMediaSources(scope, refreshedIdsA);
2019-02-21 19:06:41 +01:00
2016-05-27 14:26:28 +02:00
_mediaStore.UpdateContentTypes(removedIds, typesA, kits);
2017-07-17 17:59:46 +02:00
_mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray());
_mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray());
2017-12-12 15:04:13 +01:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
}
#endregion
2017-10-31 12:48:24 +01:00
#region Create, Get Published Snapshot
2016-05-27 14:26:28 +02:00
private long _contentGen, _mediaGen, _domainGen;
2019-01-17 11:01:23 +01:00
private IAppCache _elementsCache;
2016-05-27 14:26:28 +02:00
2018-04-27 11:38:50 +10:00
public override IPublishedSnapshot CreatePublishedSnapshot(string previewToken)
2016-05-27 14:26:28 +02:00
{
// no cache, no joy
if (_isReady == false)
2017-10-31 12:48:24 +01:00
throw new InvalidOperationException("The published snapshot service has not properly initialized.");
2016-05-27 14:26:28 +02:00
var preview = previewToken.IsNullOrWhiteSpace() == false;
2018-04-27 11:38:50 +10:00
return new PublishedSnapshot(this, preview);
2016-05-27 14:26:28 +02:00
}
// gets a new set of elements
// always creates a new set of elements,
// even though the underlying elements may not change (store snapshots)
2018-04-27 11:38:50 +10:00
public PublishedSnapshot.PublishedSnapshotElements GetElements(bool previewDefault)
2016-05-27 14:26:28 +02:00
{
2019-01-18 08:29:16 +01:00
// note: using ObjectCacheAppCache for elements and snapshot caches
2016-05-27 14:26:28 +02:00
// is not recommended because it creates an inner MemoryCache which is a heavy
2019-01-18 08:29:16 +01:00
// thing - better use a dictionary-based cache which "just" creates a concurrent
2016-05-27 14:26:28 +02:00
// dictionary
2019-01-18 08:29:16 +01:00
// for snapshot cache, DictionaryAppCache MAY be OK but it is not thread-safe,
2016-05-27 14:26:28 +02:00
// nothing like that...
2019-01-18 08:29:16 +01:00
// for elements cache, DictionaryAppCache is a No-No, use something better.
// ie FastDictionaryAppCache (thread safe and all)
2016-05-27 14:26:28 +02:00
2017-07-12 14:09:31 +02:00
ContentStore.Snapshot contentSnap, mediaSnap;
2016-05-27 14:26:28 +02:00
SnapDictionary<int, Domain>.Snapshot domainSnap;
2019-01-17 11:01:23 +01:00
IAppCache elementsCache;
2016-05-27 14:26:28 +02:00
lock (_storesLock)
{
2017-07-18 19:24:27 +02:00
var scopeContext = _scopeProvider.Context;
if (scopeContext == null)
{
contentSnap = _contentStore.CreateSnapshot();
mediaSnap = _mediaStore.CreateSnapshot();
domainSnap = _domainStore.CreateSnapshot();
2017-10-31 12:48:24 +01:00
elementsCache = _elementsCache;
2017-07-18 19:24:27 +02:00
}
else
{
contentSnap = _contentStore.LiveSnapshot;
mediaSnap = _mediaStore.LiveSnapshot;
domainSnap = _domainStore.Test.LiveSnapshot;
2017-10-31 12:48:24 +01:00
elementsCache = _elementsCache;
2017-07-18 19:24:27 +02:00
2017-07-19 19:59:46 +02:00
// this is tricky
// we are returning elements composed from live snapshots, which we need to replace
// with actual snapshots when the context is gone - but when the action runs, there
// still is a context - so we cannot get elements - just resync = nulls the current
// elements
// just need to make sure nothing gets elements in another enlisted action... so using
// a MaxValue to make sure this one runs last, and it should be ok
2017-10-31 12:48:24 +01:00
scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) =>
2017-07-18 19:24:27 +02:00
{
2018-04-29 20:02:38 +02:00
((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync();
2017-07-19 19:59:46 +02:00
}, int.MaxValue);
2017-07-18 19:24:27 +02:00
}
2016-05-27 14:26:28 +02:00
// create a new snapshot cache if snapshots are different gens
2017-10-31 12:48:24 +01:00
if (contentSnap.Gen != _contentGen || mediaSnap.Gen != _mediaGen || domainSnap.Gen != _domainGen || _elementsCache == null)
2016-05-27 14:26:28 +02:00
{
_contentGen = contentSnap.Gen;
_mediaGen = mediaSnap.Gen;
_domainGen = domainSnap.Gen;
2019-01-18 08:29:16 +01:00
elementsCache = _elementsCache = new FastDictionaryAppCache();
2016-05-27 14:26:28 +02:00
}
}
2019-01-18 08:29:16 +01:00
var snapshotCache = new DictionaryAppCache();
2017-07-18 19:24:27 +02:00
2017-10-17 17:43:15 +02:00
var memberTypeCache = new PublishedContentTypeCache(null, null, _serviceContext.MemberTypeService, _publishedContentTypeFactory, _logger);
2016-05-27 14:26:28 +02:00
2018-04-30 21:29:49 +02:00
var defaultCulture = _defaultCultureAccessor.DefaultCulture;
2018-04-26 16:03:08 +02:00
var domainCache = new DomainCache(domainSnap, defaultCulture);
var domainHelper = new DomainHelper(domainCache, _siteDomainHelper);
2016-05-27 14:26:28 +02:00
2018-04-27 11:38:50 +10:00
return new PublishedSnapshot.PublishedSnapshotElements
2016-05-27 14:26:28 +02:00
{
ContentCache = new ContentCache(previewDefault, contentSnap, snapshotCache, elementsCache, domainHelper, _globalSettings, _serviceContext.LocalizationService),
2017-10-31 12:48:24 +01:00
MediaCache = new MediaCache(previewDefault, mediaSnap, snapshotCache, elementsCache),
MemberCache = new MemberCache(previewDefault, snapshotCache, _serviceContext.MemberService, memberTypeCache, PublishedSnapshotAccessor, VariationContextAccessor, _umbracoContextAccessor, _entitySerializer),
2016-05-27 14:26:28 +02:00
DomainCache = domainCache,
2017-10-31 12:48:24 +01:00
SnapshotCache = snapshotCache,
ElementsCache = elementsCache
2016-05-27 14:26:28 +02:00
};
}
#endregion
#region Preview
public override string EnterPreview(IUser user, int contentId)
{
return "preview"; // anything
}
public override void RefreshPreview(string previewToken, int contentId)
{
// nothing
}
public override void ExitPreview(string previewToken)
{
// nothing
}
#endregion
#region Handle Repository Events For Database PreCache
// note: if the service is not ready, ie _isReady is false, then we still handle repository events,
2017-10-31 12:48:24 +01:00
// because we can, we do not need a working published snapshot to do it - the only reason why it could cause an
2016-05-27 14:26:28 +02:00
// issue is if the database table is not ready, but that should be prevented by migrations.
// we need them to be "repository" events ie to trigger from within the repository transaction,
// because they need to be consistent with the content that is being refreshed/removed - and that
// should be guaranteed by a DB transaction
2017-12-12 15:04:13 +01:00
private void OnContentRemovingEntity(DocumentRepository sender, DocumentRepository.ScopedEntityEventArgs args)
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
OnRemovedEntity(args.Scope.Database, args.Entity);
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void OnMediaRemovingEntity(MediaRepository sender, MediaRepository.ScopedEntityEventArgs args)
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
OnRemovedEntity(args.Scope.Database, args.Entity);
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void OnMemberRemovingEntity(MemberRepository sender, MemberRepository.ScopedEntityEventArgs args)
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
OnRemovedEntity(args.Scope.Database, args.Entity);
2016-05-27 14:26:28 +02:00
}
2016-12-16 14:18:37 +01:00
private void OnRemovedEntity(IUmbracoDatabase db, IContentBase item)
2016-05-27 14:26:28 +02:00
{
db.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = item.Id });
}
2017-12-12 15:04:13 +01:00
private void OnContentRefreshedEntity(DocumentRepository sender, DocumentRepository.ScopedEntityEventArgs args)
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
var db = args.Scope.Database;
var content = (Content)args.Entity;
2016-05-27 14:26:28 +02:00
// always refresh the edited data
2016-05-27 14:26:28 +02:00
OnRepositoryRefreshed(db, content, false);
// if unpublishing, remove published data from table
if (content.PublishedState == PublishedState.Unpublishing)
2016-05-27 14:26:28 +02:00
db.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = content.Id });
// if publishing, refresh the published data
else if (content.PublishedState == PublishedState.Publishing)
OnRepositoryRefreshed(db, content, true);
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void OnMediaRefreshedEntity(MediaRepository sender, MediaRepository.ScopedEntityEventArgs args)
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
var db = args.Scope.Database;
2016-05-27 14:26:28 +02:00
var media = args.Entity;
// refresh the edited data
OnRepositoryRefreshed(db, media, false);
2016-05-27 14:26:28 +02:00
}
2017-12-12 15:04:13 +01:00
private void OnMemberRefreshedEntity(MemberRepository sender, MemberRepository.ScopedEntityEventArgs args)
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
var db = args.Scope.Database;
2016-05-27 14:26:28 +02:00
var member = args.Entity;
// refresh the edited data
2016-05-27 14:26:28 +02:00
OnRepositoryRefreshed(db, member, true);
}
2016-12-16 14:18:37 +01:00
private void OnRepositoryRefreshed(IUmbracoDatabase db, IContentBase content, bool published)
2016-05-27 14:26:28 +02:00
{
// use a custom SQL to update row version on each update
//db.InsertOrUpdate(dto);
var dto = GetDto(content, published);
db.InsertOrUpdate(dto,
"SET data=@data, rv=rv+1 WHERE nodeId=@id AND published=@published",
new
{
data = dto.Data,
id = dto.NodeId,
published = dto.Published
});
}
private void OnContentTypeRefreshedEntity(IContentTypeService sender, ContentTypeChange<IContentType>.EventArgs args)
{
const ContentTypeChangeTypes types // only for those that have been refreshed
= ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther;
var contentTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray();
if (contentTypeIds.Any())
RebuildContentDbCache(contentTypeIds: contentTypeIds);
}
private void OnMediaTypeRefreshedEntity(IMediaTypeService sender, ContentTypeChange<IMediaType>.EventArgs args)
{
const ContentTypeChangeTypes types // only for those that have been refreshed
= ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther;
var mediaTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray();
if (mediaTypeIds.Any())
RebuildMediaDbCache(contentTypeIds: mediaTypeIds);
}
private void OnMemberTypeRefreshedEntity(IMemberTypeService sender, ContentTypeChange<IMemberType>.EventArgs args)
{
const ContentTypeChangeTypes types // only for those that have been refreshed
= ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther;
var memberTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray();
if (memberTypeIds.Any())
RebuildMemberDbCache(contentTypeIds: memberTypeIds);
}
private ContentNuDto GetDto(IContentBase content, bool published)
2016-05-27 14:26:28 +02:00
{
// should inject these in ctor
// BUT for the time being we decide not to support ConvertDbToXml/String
//var propertyEditorResolver = PropertyEditorResolver.Current;
//var dataTypeService = ApplicationContext.Current.Services.DataTypeService;
var propertyData = new Dictionary<string, PropertyData[]>();
2016-05-27 14:26:28 +02:00
foreach (var prop in content.Properties)
{
2017-12-07 13:22:32 +01:00
var pdatas = new List<PropertyData>();
foreach (var pvalue in prop.Values)
{
2018-10-22 17:19:14 +02:00
// sanitize - properties should be ok but ... never knows
if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment))
continue;
2018-05-08 16:41:37 +02:00
// note: at service level, invariant is 'null', but here invariant becomes 'string.Empty'
2017-12-07 13:22:32 +01:00
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
if (value != null)
2018-05-08 16:41:37 +02:00
pdatas.Add(new PropertyData { Culture = pvalue.Culture ?? string.Empty, Segment = pvalue.Segment ?? string.Empty, Value = value });
2017-12-07 13:22:32 +01:00
//Core.Composing.Current.Logger.Debug<PublishedSnapshotService>($"{content.Id} {prop.Alias} [{pvalue.LanguageId},{pvalue.Segment}] {value} {(published?"pub":"edit")}");
//if (value != null)
//{
// var e = propertyEditorResolver.GetByAlias(prop.PropertyType.PropertyEditorAlias);
// // We are converting to string, even for database values which are integer or
// // DateTime, which is not optimum. Doing differently would require that we have a way to tell
// // whether the conversion to XML string changes something or not... which we don't, and we
// // don't want to implement it as PropertyValueEditor.ConvertDbToXml/String should die anyway.
// // Don't think about improving the situation here: this is a corner case and the real
// // thing to do is to get rig of PropertyValueEditor.ConvertDbToXml/String.
// // Use ConvertDbToString to keep it simple, although everywhere we use ConvertDbToXml and
// // nothing ensures that the two methods are consistent.
// if (e != null)
// value = e.ValueEditor.ConvertDbToString(prop, prop.PropertyType, dataTypeService);
//}
}
propertyData[prop.Alias] = pdatas.ToArray();
2016-05-27 14:26:28 +02:00
}
var cultureData = new Dictionary<string, CultureVariation>();
2018-04-26 16:03:08 +02:00
2018-10-22 17:19:14 +02:00
// sanitize - names should be ok but ... never knows
2019-02-05 14:06:48 +01:00
if (content.ContentType.VariesByCulture())
2018-10-22 17:19:14 +02:00
{
2018-10-25 10:32:05 +02:00
var infos = content is IContent document
2018-04-26 16:03:08 +02:00
? (published
2018-10-23 15:04:41 +02:00
? document.PublishCultureInfos
: document.CultureInfos)
: content.CultureInfos;
2018-04-26 16:03:08 +02:00
// ReSharper disable once UseDeconstruction
foreach (var cultureInfo in infos)
2018-10-22 17:19:14 +02:00
{
var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture);
2019-03-05 11:15:30 +01:00
cultureData[cultureInfo.Culture] = new CultureVariation
{
Name = cultureInfo.Name,
UrlSegment = content.GetUrlSegment(_urlSegmentProviders, cultureInfo.Culture),
Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue,
IsDraft = cultureIsDraft
};
2018-10-22 17:19:14 +02:00
}
}
2018-04-25 15:55:27 +02:00
//the dictionary that will be serialized
var nestedData = new ContentNestedData
{
PropertyData = propertyData,
2019-03-05 11:15:30 +01:00
CultureData = cultureData,
UrlSegment = content.GetUrlSegment(_urlSegmentProviders)
2018-04-25 15:55:27 +02:00
};
2016-05-27 14:26:28 +02:00
var dto = new ContentNuDto
{
NodeId = content.Id,
Published = published,
// note that numeric values (which are Int32) are serialized without their
// type (eg "value":1234) and JsonConvert by default deserializes them as Int64
2018-04-25 15:55:27 +02:00
Data = JsonConvert.SerializeObject(nestedData)
2016-05-27 14:26:28 +02:00
};
2017-12-07 13:22:32 +01:00
//Core.Composing.Current.Logger.Debug<PublishedSnapshotService>(dto.Data);
2016-05-27 14:26:28 +02:00
return dto;
}
#endregion
#region Rebuild Database PreCache
2019-02-13 09:53:17 +01:00
public override void Rebuild()
{
2019-02-13 13:28:11 +01:00
_logger.Debug<PublishedSnapshotService>("Rebuilding...");
2019-02-13 09:53:17 +01:00
using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
{
scope.ReadLock(Constants.Locks.ContentTree);
scope.ReadLock(Constants.Locks.MediaTree);
scope.ReadLock(Constants.Locks.MemberTree);
RebuildContentDbCacheLocked(scope, 5000, null);
RebuildMediaDbCacheLocked(scope, 5000, null);
RebuildMemberDbCacheLocked(scope, 5000, null);
scope.Complete();
}
}
2016-05-27 14:26:28 +02:00
public void RebuildContentDbCache(int groupSize = 5000, IEnumerable<int> contentTypeIds = null)
{
2017-05-12 14:49:44 +02:00
using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.ContentTree);
RebuildContentDbCacheLocked(scope, groupSize, contentTypeIds);
2017-05-12 14:49:44 +02:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
}
// assumes content tree lock
2017-12-12 15:04:13 +01:00
private void RebuildContentDbCacheLocked(IScope scope, int groupSize, IEnumerable<int> contentTypeIds)
2016-05-27 14:26:28 +02:00
{
var contentTypeIdsA = contentTypeIds?.ToArray();
2017-09-19 18:19:05 +02:00
var contentObjectType = Constants.ObjectTypes.Document;
2017-12-12 15:04:13 +01:00
var db = scope.Database;
2016-05-27 14:26:28 +02:00
// remove all - if anything fails the transaction will rollback
if (contentTypeIds == null || contentTypeIdsA.Length == 0)
{
// must support SQL-CE
db.Execute(@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
)",
new { objType = contentObjectType });
}
else
{
// assume number of ctypes won't blow IN(...)
// must support SQL-CE
db.Execute($@"DELETE FROM cmsContentNu
2016-05-27 14:26:28 +02:00
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
2016-05-27 14:26:28 +02:00
WHERE umbracoNode.nodeObjectType=@objType
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
2016-05-27 14:26:28 +02:00
)",
new { objType = contentObjectType, ctypes = contentTypeIdsA });
}
// insert back - if anything fails the transaction will rollback
2017-12-12 15:04:13 +01:00
var query = scope.SqlContext.Query<IContent>();
2016-05-27 14:26:28 +02:00
if (contentTypeIds != null && contentTypeIdsA.Length > 0)
query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...)
long pageIndex = 0;
long processed = 0;
long total;
do
{
var descendants = _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
2016-05-27 14:26:28 +02:00
var items = new List<ContentNuDto>();
foreach (var c in descendants)
2017-12-07 13:22:32 +01:00
{
// always the edited version
items.Add(GetDto(c, false));
// and also the published version if it makes any sense
if (c.Published)
items.Add(GetDto(c, true));
}
2016-05-27 14:26:28 +02:00
db.BulkInsertRecords(items);
2016-05-27 14:26:28 +02:00
processed += items.Count;
} while (processed < total);
}
public void RebuildMediaDbCache(int groupSize = 5000, IEnumerable<int> contentTypeIds = null)
{
2017-05-12 14:49:44 +02:00
using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.MediaTree);
RebuildMediaDbCacheLocked(scope, groupSize, contentTypeIds);
2017-05-12 14:49:44 +02:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
}
// assumes media tree lock
2017-12-12 15:04:13 +01:00
public void RebuildMediaDbCacheLocked(IScope scope, int groupSize, IEnumerable<int> contentTypeIds)
2016-05-27 14:26:28 +02:00
{
var contentTypeIdsA = contentTypeIds?.ToArray();
2017-09-19 18:19:05 +02:00
var mediaObjectType = Constants.ObjectTypes.Media;
2017-12-12 15:04:13 +01:00
var db = scope.Database;
2016-05-27 14:26:28 +02:00
// remove all - if anything fails the transaction will rollback
if (contentTypeIds == null || contentTypeIdsA.Length == 0)
{
// must support SQL-CE
db.Execute(@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
)",
new { objType = mediaObjectType });
}
else
{
// assume number of ctypes won't blow IN(...)
// must support SQL-CE
db.Execute($@"DELETE FROM cmsContentNu
2016-05-27 14:26:28 +02:00
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
2016-05-27 14:26:28 +02:00
WHERE umbracoNode.nodeObjectType=@objType
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
2016-05-27 14:26:28 +02:00
)",
new { objType = mediaObjectType, ctypes = contentTypeIdsA });
}
// insert back - if anything fails the transaction will rollback
2017-12-12 15:04:13 +01:00
var query = scope.SqlContext.Query<IMedia>();
2016-05-27 14:26:28 +02:00
if (contentTypeIds != null && contentTypeIdsA.Length > 0)
query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...)
long pageIndex = 0;
long processed = 0;
long total;
do
{
var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
2017-12-07 13:22:32 +01:00
var items = descendants.Select(m => GetDto(m, false)).ToArray();
db.BulkInsertRecords(items);
2016-05-27 14:26:28 +02:00
processed += items.Length;
} while (processed < total);
}
public void RebuildMemberDbCache(int groupSize = 5000, IEnumerable<int> contentTypeIds = null)
{
2017-05-12 14:49:44 +02:00
using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.MemberTree);
RebuildMemberDbCacheLocked(scope, groupSize, contentTypeIds);
2017-05-12 14:49:44 +02:00
scope.Complete();
2016-05-27 14:26:28 +02:00
}
}
// assumes member tree lock
2017-12-12 15:04:13 +01:00
public void RebuildMemberDbCacheLocked(IScope scope, int groupSize, IEnumerable<int> contentTypeIds)
2016-05-27 14:26:28 +02:00
{
var contentTypeIdsA = contentTypeIds?.ToArray();
2017-09-19 18:19:05 +02:00
var memberObjectType = Constants.ObjectTypes.Member;
2017-12-12 15:04:13 +01:00
var db = scope.Database;
2016-05-27 14:26:28 +02:00
// remove all - if anything fails the transaction will rollback
if (contentTypeIds == null || contentTypeIdsA.Length == 0)
{
// must support SQL-CE
db.Execute(@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
)",
new { objType = memberObjectType });
}
else
{
// assume number of ctypes won't blow IN(...)
// must support SQL-CE
db.Execute($@"DELETE FROM cmsContentNu
2016-05-27 14:26:28 +02:00
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
2016-05-27 14:26:28 +02:00
WHERE umbracoNode.nodeObjectType=@objType
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
2016-05-27 14:26:28 +02:00
)",
new { objType = memberObjectType, ctypes = contentTypeIdsA });
}
// insert back - if anything fails the transaction will rollback
2017-12-12 15:04:13 +01:00
var query = scope.SqlContext.Query<IMember>();
2016-05-27 14:26:28 +02:00
if (contentTypeIds != null && contentTypeIdsA.Length > 0)
query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...)
long pageIndex = 0;
long processed = 0;
long total;
do
{
var descendants = _memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
2017-12-07 13:22:32 +01:00
var items = descendants.Select(m => GetDto(m, false)).ToArray();
db.BulkInsertRecords(items);
2016-05-27 14:26:28 +02:00
processed += items.Length;
} while (processed < total);
}
public bool VerifyContentDbCache()
{
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.ContentTree);
var ok = VerifyContentDbCacheLocked(scope);
scope.Complete();
2016-05-27 14:26:28 +02:00
return ok;
}
}
// assumes content tree lock
2017-12-12 15:04:13 +01:00
private bool VerifyContentDbCacheLocked(IScope scope)
2016-05-27 14:26:28 +02:00
{
2017-12-07 13:22:32 +01:00
// every document should have a corresponding row for edited properties
// and if published, may have a corresponding row for published properties
2016-05-27 14:26:28 +02:00
2017-09-19 18:19:05 +02:00
var contentObjectType = Constants.ObjectTypes.Document;
2017-12-12 15:04:13 +01:00
var db = scope.Database;
2016-05-27 14:26:28 +02:00
var count = db.ExecuteScalar<int>($@"SELECT COUNT(*)
2016-05-27 14:26:28 +02:00
FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Document} ON umbracoNode.id={Constants.DatabaseSchema.Tables.Document}.nodeId
2017-12-07 13:22:32 +01:00
LEFT JOIN cmsContentNu nuEdited ON (umbracoNode.id=nuEdited.nodeId AND nuEdited.published=0)
LEFT JOIN cmsContentNu nuPublished ON (umbracoNode.id=nuPublished.nodeId AND nuPublished.published=1)
2016-05-27 14:26:28 +02:00
WHERE umbracoNode.nodeObjectType=@objType
AND nuEdited.nodeId IS NULL OR ({Constants.DatabaseSchema.Tables.Document}.published=1 AND nuPublished.nodeId IS NULL);"
2016-05-27 14:26:28 +02:00
, new { objType = contentObjectType });
return count == 0;
}
public bool VerifyMediaDbCache()
{
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.MediaTree);
var ok = VerifyMediaDbCacheLocked(scope);
scope.Complete();
2016-05-27 14:26:28 +02:00
return ok;
}
}
// assumes media tree lock
2017-12-12 15:04:13 +01:00
public bool VerifyMediaDbCacheLocked(IScope scope)
2016-05-27 14:26:28 +02:00
{
2017-12-07 13:22:32 +01:00
// every media item should have a corresponding row for edited properties
2016-05-27 14:26:28 +02:00
2017-09-19 18:19:05 +02:00
var mediaObjectType = Constants.ObjectTypes.Media;
2017-12-12 15:04:13 +01:00
var db = scope.Database;
2016-05-27 14:26:28 +02:00
var count = db.ExecuteScalar<int>(@"SELECT COUNT(*)
FROM umbracoNode
2017-12-07 13:22:32 +01:00
LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0)
2016-05-27 14:26:28 +02:00
WHERE umbracoNode.nodeObjectType=@objType
AND cmsContentNu.nodeId IS NULL
", new { objType = mediaObjectType });
return count == 0;
}
public bool VerifyMemberDbCache()
{
2017-12-12 15:04:13 +01:00
using (var scope = _scopeProvider.CreateScope())
2016-05-27 14:26:28 +02:00
{
2017-12-12 15:04:13 +01:00
scope.ReadLock(Constants.Locks.MemberTree);
var ok = VerifyMemberDbCacheLocked(scope);
scope.Complete();
2016-05-27 14:26:28 +02:00
return ok;
}
}
// assumes member tree lock
2017-12-12 15:04:13 +01:00
public bool VerifyMemberDbCacheLocked(IScope scope)
2016-05-27 14:26:28 +02:00
{
2017-12-07 13:22:32 +01:00
// every member item should have a corresponding row for edited properties
2016-05-27 14:26:28 +02:00
2017-09-19 18:19:05 +02:00
var memberObjectType = Constants.ObjectTypes.Member;
2017-12-12 15:04:13 +01:00
var db = scope.Database;
2016-05-27 14:26:28 +02:00
var count = db.ExecuteScalar<int>(@"SELECT COUNT(*)
FROM umbracoNode
2017-12-07 13:22:32 +01:00
LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0)
2016-05-27 14:26:28 +02:00
WHERE umbracoNode.nodeObjectType=@objType
AND cmsContentNu.nodeId IS NULL
", new { objType = memberObjectType });
return count == 0;
}
#endregion
#region Instrument
public string GetStatus()
{
var dbCacheIsOk = VerifyContentDbCache()
&& VerifyMediaDbCache()
&& VerifyMemberDbCache();
var cg = _contentStore.GenCount;
var mg = _mediaStore.GenCount;
var cs = _contentStore.SnapCount;
var ms = _mediaStore.SnapCount;
var ce = _contentStore.Count;
var me = _mediaStore.Count;
2019-04-03 11:57:18 +02:00
return
2016-05-27 14:26:28 +02:00
" Database cache is " + (dbCacheIsOk ? "ok" : "NOT ok (rebuild?)") + "." +
2019-04-03 11:57:18 +02:00
" ContentStore contains " + ce + " item" + (ce > 1 ? "s" : "") +
" and has " + cg + " generation" + (cg > 1 ? "s" : "") +
" and " + cs + " snapshot" + (cs > 1 ? "s" : "") + "." +
" MediaStore contains " + me + " item" + (ce > 1 ? "s" : "") +
" and has " + mg + " generation" + (mg > 1 ? "s" : "") +
" and " + ms + " snapshot" + (ms > 1 ? "s" : "") + ".";
2016-05-27 14:26:28 +02:00
}
public void Collect()
{
var contentCollect = _contentStore.CollectAsync();
var mediaCollect = _mediaStore.CollectAsync();
System.Threading.Tasks.Task.WaitAll(contentCollect, mediaCollect);
}
#endregion
}
}