2015-03-04 12:16:28 +01:00
using System ;
using System.Collections.Generic ;
2015-07-07 19:36:17 +02:00
using System.Diagnostics ;
2015-03-04 12:16:28 +01:00
using System.Globalization ;
using System.IO ;
using System.Linq ;
2015-07-03 15:33:07 +02:00
using System.Threading ;
2015-03-04 12:16:28 +01:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2016-04-12 15:11:07 +02:00
using NPoco ;
2015-03-04 12:16:28 +01:00
using Umbraco.Core.Cache ;
2017-05-30 15:46:25 +02:00
using Umbraco.Core.Composing ;
2015-03-04 12:16:28 +01:00
using Umbraco.Core.Logging ;
using Umbraco.Core.Persistence ;
2017-12-28 09:06:33 +01:00
using Umbraco.Core.Persistence.Dtos ;
2019-11-20 13:38:41 +01:00
using Umbraco.Core.Hosting ;
2017-05-12 14:49:44 +02:00
using Umbraco.Core.Scoping ;
2015-03-04 12:16:28 +01:00
namespace Umbraco.Core.Sync
{
/// <summary>
/// An <see cref="IServerMessenger"/> that works by storing messages in the database.
/// </summary>
//
// this messenger writes ALL instructions to the database,
// but only processes instructions coming from remote servers,
// thus ensuring that instructions run only once
//
2020-03-03 07:29:51 +01:00
public class DatabaseServerMessenger : ServerMessengerBase , IDatabaseServerMessenger
2015-03-04 12:16:28 +01:00
{
2020-03-25 15:06:22 +11:00
private readonly IMainDom _mainDom ;
2015-07-03 15:33:07 +02:00
private readonly ManualResetEvent _syncIdle ;
private readonly object _locko = new object ( ) ;
2018-11-27 10:37:33 +01:00
private readonly IProfilingLogger _profilingLogger ;
2020-03-25 15:06:22 +11:00
private readonly IServerRegistrar _serverRegistrar ;
2019-11-20 13:38:41 +01:00
private readonly IHostingEnvironment _hostingEnvironment ;
2019-12-18 11:16:43 +01:00
private readonly CacheRefresherCollection _cacheRefreshers ;
2017-09-22 18:28:21 +02:00
private readonly ISqlContext _sqlContext ;
2018-04-06 13:51:54 +10:00
private readonly Lazy < string > _distCacheFilePath ;
2015-03-04 12:16:28 +01:00
private int _lastId = - 1 ;
private DateTime _lastSync ;
2016-07-05 12:29:16 +02:00
private DateTime _lastPruned ;
2015-03-04 12:16:28 +01:00
private bool _initialized ;
2015-07-03 15:33:07 +02:00
private bool _syncing ;
private bool _released ;
2015-03-04 12:16:28 +01:00
2018-03-27 10:04:07 +02:00
public DatabaseServerMessengerOptions Options { get ; }
2015-03-04 12:16:28 +01:00
2016-09-01 19:06:08 +02:00
public DatabaseServerMessenger (
2020-09-15 15:14:44 +02:00
IMainDom mainDom , IScopeProvider scopeProvider , ISqlContext sqlContext , IProfilingLogger proflog , ILogger logger , IServerRegistrar serverRegistrar ,
2019-12-18 11:16:43 +01:00
bool distributedEnabled , DatabaseServerMessengerOptions options , IHostingEnvironment hostingEnvironment , CacheRefresherCollection cacheRefreshers )
2015-03-04 12:16:28 +01:00
: base ( distributedEnabled )
{
2017-05-12 14:49:44 +02:00
ScopeProvider = scopeProvider ? ? throw new ArgumentNullException ( nameof ( scopeProvider ) ) ;
2017-09-22 18:28:21 +02:00
_sqlContext = sqlContext ;
2020-03-25 15:06:22 +11:00
_mainDom = mainDom ;
2017-05-12 14:49:44 +02:00
_profilingLogger = proflog ? ? throw new ArgumentNullException ( nameof ( proflog ) ) ;
2020-03-25 15:06:22 +11:00
_serverRegistrar = serverRegistrar ;
2019-11-20 13:38:41 +01:00
_hostingEnvironment = hostingEnvironment ;
2019-12-18 11:16:43 +01:00
_cacheRefreshers = cacheRefreshers ;
2020-09-15 15:14:44 +02:00
Logger = logger ;
2017-05-12 14:49:44 +02:00
Options = options ? ? throw new ArgumentNullException ( nameof ( options ) ) ;
2016-07-05 12:29:16 +02:00
_lastPruned = _lastSync = DateTime . UtcNow ;
2015-07-03 15:33:07 +02:00
_syncIdle = new ManualResetEvent ( true ) ;
2019-11-20 13:38:41 +01:00
_distCacheFilePath = new Lazy < string > ( ( ) = > GetDistCacheFilePath ( hostingEnvironment ) ) ;
2020-08-21 00:02:35 +10:00
// See notes on LocalIdentity
LocalIdentity = NetworkHelper . MachineName // eg DOMAIN\SERVER
+ "/" + _hostingEnvironment . ApplicationId // eg /LM/S3SVC/11/ROOT
+ " [P" + Process . GetCurrentProcess ( ) . Id // eg 1234
+ "/D" + AppDomain . CurrentDomain . Id // eg 22
+ "] " + Guid . NewGuid ( ) . ToString ( "N" ) . ToUpper ( ) ; // make it truly unique
2015-03-04 12:16:28 +01:00
}
2016-09-01 19:06:08 +02:00
protected ILogger Logger { get ; }
2017-05-12 14:49:44 +02:00
protected IScopeProvider ScopeProvider { get ; }
2016-09-01 19:06:08 +02:00
2017-09-22 18:48:58 +02:00
protected Sql < ISqlContext > Sql ( ) = > _sqlContext . Sql ( ) ;
2018-04-03 16:15:59 +02:00
2018-03-21 09:06:32 +01:00
private string DistCacheFilePath = > _distCacheFilePath . Value ;
2016-09-01 19:06:08 +02:00
2015-03-04 12:16:28 +01:00
#region Messenger
2018-05-01 10:39:04 +10:00
protected override bool RequiresDistributed ( ICacheRefresher refresher , MessageType dispatchType )
2015-03-04 12:16:28 +01:00
{
2016-01-28 14:19:32 +01:00
// we don't care if there's servers listed or not,
2015-03-04 12:16:28 +01:00
// if distributed call is enabled we will make the call
return _initialized & & DistributedEnabled ;
}
protected override void DeliverRemote (
ICacheRefresher refresher ,
MessageType messageType ,
IEnumerable < object > ids = null ,
string json = null )
{
2016-09-01 19:06:08 +02:00
var idsA = ids ? . ToArray ( ) ;
2015-03-04 12:16:28 +01:00
2018-03-27 10:04:07 +02:00
if ( GetArrayType ( idsA , out var idType ) = = false )
2016-09-01 19:06:08 +02:00
throw new ArgumentException ( "All items must be of the same type, either int or Guid." , nameof ( ids ) ) ;
2015-03-04 12:16:28 +01:00
var instructions = RefreshInstruction . GetInstructions ( refresher , messageType , idsA , idType , json ) ;
var dto = new CacheInstructionDto
{
UtcStamp = DateTime . UtcNow ,
Instructions = JsonConvert . SerializeObject ( instructions , Formatting . None ) ,
2018-03-21 09:06:32 +01:00
OriginIdentity = LocalIdentity ,
InstructionCount = instructions . Sum ( x = > x . JsonIdCount )
2015-03-04 12:16:28 +01:00
} ;
2017-05-12 14:49:44 +02:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
scope . Database . Insert ( dto ) ;
scope . Complete ( ) ;
}
2015-03-04 12:16:28 +01:00
}
#endregion
#region Sync
/// <summary>
/// Boots the messenger.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// Callers MUST ensure thread-safety.
/// </remarks>
2015-07-29 11:22:12 +02:00
protected void Boot ( )
2015-03-04 12:16:28 +01:00
{
2017-10-31 12:48:24 +01:00
// weight:10, must release *before* the published snapshot service, because once released
2015-07-03 15:33:07 +02:00
// the service will *not* be able to properly handle our notifications anymore
const int weight = 10 ;
2017-10-31 12:48:24 +01:00
2020-03-25 15:06:22 +11:00
var registered = _mainDom . Register (
2015-07-03 15:33:07 +02:00
( ) = >
{
lock ( _locko )
{
_released = true ; // no more syncs
}
2016-10-25 15:50:33 +02:00
2016-10-26 18:01:32 +02:00
// wait a max of 5 seconds and then return, so that we don't block
// the entire MainDom callbacks chain and prevent the AppDomain from
// properly releasing MainDom - a timeout here means that one refresher
// is taking too much time processing, however when it's done we will
// not update lastId and stop everything
var idle = _syncIdle . WaitOne ( 5000 ) ;
if ( idle = = false )
2016-10-26 11:37:59 +02:00
{
2020-09-14 10:17:32 +02:00
Logger . LogWarning < DatabaseServerMessenger > ( "The wait lock timed out, application is shutting down. The current instruction batch will be re-processed." ) ;
2016-10-26 18:01:32 +02:00
}
2015-07-03 15:33:07 +02:00
} ,
weight ) ;
if ( registered = = false )
return ;
2015-07-14 16:21:05 +02:00
ReadLastSynced ( ) ; // get _lastId
2017-05-12 14:49:44 +02:00
using ( var scope = ScopeProvider . CreateScope ( ) )
{
2019-01-22 18:03:39 -05:00
EnsureInstructions ( scope . Database ) ; // reset _lastId if instructions are missing
2017-05-12 14:49:44 +02:00
Initialize ( scope . Database ) ; // boot
scope . Complete ( ) ;
}
2015-03-04 12:16:28 +01:00
}
/// <summary>
/// Initializes a server that has never synchronized before.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
2015-03-05 10:45:43 +01:00
/// Callers MUST ensure thread-safety.
2015-03-04 12:16:28 +01:00
/// </remarks>
2017-05-12 14:49:44 +02:00
private void Initialize ( IUmbracoDatabase database )
2015-03-04 12:16:28 +01:00
{
2015-07-03 15:33:07 +02:00
lock ( _locko )
2015-03-05 10:45:43 +01:00
{
2015-07-03 15:33:07 +02:00
if ( _released ) return ;
2016-01-28 14:19:32 +01:00
var coldboot = false ;
2015-07-03 15:33:07 +02:00
if ( _lastId < 0 ) // never synced before
{
2016-01-28 14:19:32 +01:00
// we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new
2015-07-03 15:33:07 +02:00
// server and it will need to rebuild it's own caches, eg Lucene or the xml cache file.
2020-09-14 10:17:32 +02:00
Logger . LogWarning < DatabaseServerMessenger > ( "No last synced Id found, this generally means this is a new server/install."
2016-01-28 14:19:32 +01:00
+ " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in"
+ " the database and maintain cache updates based on that Id." ) ;
2015-07-03 15:33:07 +02:00
2016-01-28 14:19:32 +01:00
coldboot = true ;
2015-07-03 15:33:07 +02:00
}
2016-01-28 12:14:30 +01:00
else
{
2018-03-21 09:06:32 +01:00
//check for how many instructions there are to process, each row contains a count of the number of instructions contained in each
//row so we will sum these numbers to get the actual count.
var count = database . ExecuteScalar < int > ( "SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId" , new { lastId = _lastId } ) ;
2016-10-25 16:01:55 +02:00
if ( count > Options . MaxProcessingInstructionCount )
2016-01-28 12:14:30 +01:00
{
//too many instructions, proceed to cold boot
2020-09-14 10:17:32 +02:00
Logger . LogWarning < DatabaseServerMessenger > (
2018-08-14 15:08:32 +01:00
"The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})."
2016-01-28 14:19:32 +01:00
+ " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id"
2018-08-14 15:08:32 +01:00
+ " to the latest found in the database and maintain cache updates based on that Id." ,
count , Options . MaxProcessingInstructionCount ) ;
2016-01-28 12:14:30 +01:00
2016-01-28 14:19:32 +01:00
coldboot = true ;
}
}
2016-01-28 12:14:30 +01:00
2016-01-28 14:19:32 +01:00
if ( coldboot )
{
2015-07-03 15:33:07 +02:00
// go get the last id in the db and store it
// note: do it BEFORE initializing otherwise some instructions might get lost
// when doing it before, some instructions might run twice - not an issue
2017-05-12 14:49:44 +02:00
var maxId = database . ExecuteScalar < int > ( "SELECT MAX(id) FROM umbracoCacheInstruction" ) ;
2016-02-11 18:07:31 +01:00
//if there is a max currently, or if we've never synced
if ( maxId > 0 | | _lastId < 0 )
SaveLastSynced ( maxId ) ;
2015-07-03 15:33:07 +02:00
// execute initializing callbacks
2016-10-25 16:01:55 +02:00
if ( Options . InitializingCallbacks ! = null )
foreach ( var callback in Options . InitializingCallbacks )
2015-07-03 15:33:07 +02:00
callback ( ) ;
}
2015-03-04 12:16:28 +01:00
2015-07-03 15:33:07 +02:00
_initialized = true ;
}
2015-03-04 12:16:28 +01:00
}
/// <summary>
/// Synchronize the server (throttled).
/// </summary>
2019-12-18 11:16:43 +01:00
public void Sync ( )
2015-03-04 12:16:28 +01:00
{
2015-07-03 15:33:07 +02:00
lock ( _locko )
{
2016-01-28 14:19:32 +01:00
if ( _syncing )
2015-07-03 15:33:07 +02:00
return ;
2015-03-04 12:16:28 +01:00
2016-10-25 15:50:33 +02:00
//Don't continue if we are released
2015-07-03 15:33:07 +02:00
if ( _released )
return ;
2015-03-04 12:16:28 +01:00
2016-10-25 16:01:55 +02:00
if ( ( DateTime . UtcNow - _lastSync ) . TotalSeconds < = Options . ThrottleSeconds )
2015-07-03 15:33:07 +02:00
return ;
2015-03-04 12:16:28 +01:00
2016-10-25 15:50:33 +02:00
//Set our flag and the lock to be in it's original state (i.e. it can be awaited)
2015-07-03 15:33:07 +02:00
_syncing = true ;
_syncIdle . Reset ( ) ;
2015-03-04 12:16:28 +01:00
_lastSync = DateTime . UtcNow ;
2015-07-03 15:33:07 +02:00
}
2015-03-04 12:16:28 +01:00
2015-07-03 15:33:07 +02:00
try
{
2015-07-15 10:29:28 +02:00
using ( _profilingLogger . DebugDuration < DatabaseServerMessenger > ( "Syncing from database..." ) )
2017-05-12 14:49:44 +02:00
using ( var scope = ScopeProvider . CreateScope ( ) )
2015-03-04 12:16:28 +01:00
{
2017-05-12 14:49:44 +02:00
ProcessDatabaseInstructions ( scope . Database ) ;
2016-07-05 12:29:16 +02:00
2016-10-25 15:50:33 +02:00
//Check for pruning throttling
2017-05-12 14:49:44 +02:00
if ( _released | | ( DateTime . UtcNow - _lastPruned ) . TotalSeconds < = Options . PruneThrottleSeconds )
{
scope . Complete ( ) ;
2016-07-05 12:29:16 +02:00
return ;
2017-05-12 14:49:44 +02:00
}
2016-07-05 12:29:16 +02:00
_lastPruned = _lastSync ;
2020-03-25 15:06:22 +11:00
switch ( _serverRegistrar . GetCurrentServerRole ( ) )
2015-10-21 14:29:18 +01:00
{
case ServerRole . Single :
case ServerRole . Master :
2017-05-12 14:49:44 +02:00
PruneOldInstructions ( scope . Database ) ;
2015-10-21 14:29:18 +01:00
break ;
}
2017-05-12 14:49:44 +02:00
scope . Complete ( ) ;
2015-03-04 12:16:28 +01:00
}
2015-07-03 15:33:07 +02:00
}
finally
{
2016-10-26 11:37:59 +02:00
lock ( _locko )
{
//We must reset our flag and signal any waiting locks
_syncing = false ;
}
2015-07-03 15:33:07 +02:00
_syncIdle . Set ( ) ;
2015-03-04 12:16:28 +01:00
}
}
/// <summary>
/// Process instructions from the database.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// </remarks>
2016-06-28 16:31:24 +02:00
/// <returns>
/// Returns the number of processed instructions
/// </returns>
2017-05-12 14:49:44 +02:00
private void ProcessDatabaseInstructions ( IUmbracoDatabase database )
2015-03-04 12:16:28 +01:00
{
// NOTE
2016-01-28 14:19:32 +01:00
// we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that
2015-03-04 12:16:28 +01:00
// would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests
2016-01-28 14:19:32 +01:00
// (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are
2015-03-04 12:16:28 +01:00
// pending requests after being processed, they'll just be processed on the next poll.
//
2019-01-26 09:42:14 -05:00
// TODO: not true if we're running on a background thread, assuming we can?
2015-03-04 12:16:28 +01:00
2016-09-01 19:06:08 +02:00
var sql = Sql ( ) . SelectAll ( )
2016-04-12 15:11:07 +02:00
. From < CacheInstructionDto > ( )
. Where < CacheInstructionDto > ( dto = > dto . Id > _lastId )
. OrderBy < CacheInstructionDto > ( dto = > dto . Id ) ;
2015-03-04 12:16:28 +01:00
2016-10-26 12:01:43 +02:00
//only retrieve the top 100 (just in case there's tons)
2016-10-26 18:01:32 +02:00
// even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many
2016-10-26 12:01:43 +02:00
// rows in one request thread since each row can contain a ton of instructions (until 7.5.5 in which case
// a row can only contain MaxProcessingInstructionCount)
2016-11-04 18:40:42 +01:00
var topSql = sql . SelectTop ( 100 ) ;
2015-03-04 12:16:28 +01:00
// only process instructions coming from a remote server, and ignore instructions coming from
// the local server as they've already been processed. We should NOT assume that the sequence of
// instructions in the database makes any sense whatsoever, because it's all async.
2015-07-07 19:36:17 +02:00
var localIdentity = LocalIdentity ;
2015-03-04 12:16:28 +01:00
var lastId = 0 ;
2016-10-26 18:01:32 +02:00
2016-10-26 12:19:36 +02:00
//tracks which ones have already been processed to avoid duplicates
var processed = new HashSet < RefreshInstruction > ( ) ;
2016-10-26 18:01:32 +02:00
//It would have been nice to do this in a Query instead of Fetch using a data reader to save
2019-01-22 18:03:39 -05:00
// some memory however we cannot do that because inside of this loop the cache refreshers are also
2016-10-26 11:37:59 +02:00
// performing some lookups which cannot be done with an active reader open
2017-05-12 14:49:44 +02:00
foreach ( var dto in database . Fetch < CacheInstructionDto > ( topSql ) )
2015-03-04 12:16:28 +01:00
{
2016-10-25 16:23:49 +02:00
//If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot
// continue processing anything otherwise we'll hold up the app domain shutdown
if ( _released )
{
break ;
}
2015-04-08 14:21:58 +02:00
if ( dto . OriginIdentity = = localIdentity )
2015-03-04 12:16:28 +01:00
{
2015-04-08 14:21:58 +02:00
// just skip that local one but update lastId nevertheless
2015-03-04 12:16:28 +01:00
lastId = dto . Id ;
2015-04-08 14:21:58 +02:00
continue ;
}
// deserialize remote instructions & skip if it fails
JArray jsonA ;
try
{
jsonA = JsonConvert . DeserializeObject < JArray > ( dto . Instructions ) ;
2015-03-04 12:16:28 +01:00
}
catch ( JsonException ex )
{
2020-09-16 09:40:49 +02:00
Logger . LogError ( ex , "Failed to deserialize instructions ({DtoId}: '{DtoInstructions}')." ,
2018-08-16 12:00:12 +01:00
dto . Id ,
dto . Instructions ) ;
2015-04-08 14:21:58 +02:00
lastId = dto . Id ; // skip
continue ;
}
2015-03-04 12:16:28 +01:00
2016-10-25 15:50:33 +02:00
var instructionBatch = GetAllInstructions ( jsonA ) ;
//process as per-normal
2016-10-26 12:19:36 +02:00
var success = ProcessDatabaseInstructions ( instructionBatch , dto , processed , ref lastId ) ;
2016-10-25 15:50:33 +02:00
2016-10-25 16:23:49 +02:00
//if they couldn't be all processed (i.e. we're shutting down) then exit
if ( success = = false )
2015-04-08 14:21:58 +02:00
{
2020-09-15 08:45:40 +02:00
Logger . LogInformation ( "The current batch of instructions was not processed, app is shutting down" ) ;
2016-10-25 16:23:49 +02:00
break ;
2015-03-04 12:16:28 +01:00
}
2016-10-26 18:01:32 +02:00
2015-03-04 12:16:28 +01:00
}
if ( lastId > 0 )
SaveLastSynced ( lastId ) ;
}
2016-10-25 16:23:49 +02:00
/// <summary>
/// Processes the instruction batch and checks for errors
/// </summary>
/// <param name="instructionBatch"></param>
/// <param name="dto"></param>
2016-10-26 12:19:36 +02:00
/// <param name="processed">
/// Tracks which instructions have already been processed to avoid duplicates
/// </param>
2016-10-25 16:23:49 +02:00
/// <param name="lastId"></param>
/// <returns>
/// returns true if all instructions in the batch were processed, otherwise false if they could not be due to the app being shut down
/// </returns>
2016-10-26 12:19:36 +02:00
private bool ProcessDatabaseInstructions ( IReadOnlyCollection < RefreshInstruction > instructionBatch , CacheInstructionDto dto , HashSet < RefreshInstruction > processed , ref int lastId )
2016-10-25 15:50:33 +02:00
{
// execute remote instructions & update lastId
try
{
2016-10-26 12:19:36 +02:00
var result = NotifyRefreshers ( instructionBatch , processed ) ;
2016-10-25 16:23:49 +02:00
if ( result )
2015-04-08 14:21:58 +02:00
{
2016-10-25 16:23:49 +02:00
//if all instructions we're processed, set the last id
lastId = dto . Id ;
}
return result ;
2016-10-25 15:50:33 +02:00
}
//catch (ThreadAbortException ex)
//{
2019-01-22 18:03:39 -05:00
// //This will occur if the instructions processing is taking too long since this is occurring on a request thread.
2016-10-25 15:50:33 +02:00
// // Or possibly if IIS terminates the appdomain. In any case, we should deal with this differently perhaps...
//}
catch ( Exception ex )
{
2020-09-16 09:40:49 +02:00
Logger . LogError (
2018-08-16 12:00:12 +01:00
ex ,
2018-08-17 15:41:58 +01:00
"DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({DtoId}: '{DtoInstructions}'). Instruction is being skipped/ignored" ,
2018-08-16 12:00:12 +01:00
dto . Id ,
dto . Instructions ) ;
2015-07-15 10:50:01 +02:00
2016-10-25 15:50:33 +02:00
//we cannot throw here because this invalid instruction will just keep getting processed over and over and errors
// will be thrown over and over. The only thing we can do is ignore and move on.
2016-10-25 16:23:49 +02:00
lastId = dto . Id ;
return false ;
2015-03-04 12:16:28 +01:00
}
2016-10-25 15:50:33 +02:00
////if this is returned it will not be saved
//return -1;
2015-03-04 12:16:28 +01:00
}
/// <summary>
2016-01-05 14:20:13 +01:00
/// Remove old instructions from the database
2015-03-04 12:16:28 +01:00
/// </summary>
2016-01-05 14:20:13 +01:00
/// <remarks>
2016-01-28 14:19:32 +01:00
/// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which would cause
2016-01-05 14:20:13 +01:00
/// the site to cold boot if there's been no instruction activity for more than DaysToRetainInstructions.
/// See: http://issues.umbraco.org/issue/U4-7643#comment=67-25085
/// </remarks>
2017-05-12 14:49:44 +02:00
private void PruneOldInstructions ( IUmbracoDatabase database )
2015-03-04 12:16:28 +01:00
{
2016-10-25 16:01:55 +02:00
var pruneDate = DateTime . UtcNow . AddDays ( - Options . DaysToRetainInstructions ) ;
2016-07-05 12:29:16 +02:00
// using 2 queries is faster than convoluted joins
2017-05-12 14:49:44 +02:00
var maxId = database . ExecuteScalar < int > ( "SELECT MAX(id) FROM umbracoCacheInstruction;" ) ;
2016-07-05 12:29:16 +02:00
var delete = new Sql ( ) . Append ( @"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId" ,
new { pruneDate , maxId } ) ;
2017-05-12 14:49:44 +02:00
database . Execute ( delete ) ;
2015-03-04 12:16:28 +01:00
}
2015-07-14 16:21:05 +02:00
/// <summary>
/// Ensure that the last instruction that was processed is still in the database.
/// </summary>
2016-06-23 16:53:50 +02:00
/// <remarks>
/// If the last instruction is not in the database anymore, then the messenger
2015-07-14 16:21:05 +02:00
/// should not try to process any instructions, because some instructions might be lost,
2016-06-23 16:53:50 +02:00
/// and it should instead cold-boot.
/// However, if the last synced instruction id is '0' and there are '0' records, then this indicates
/// that it's a fresh site and no user actions have taken place, in this circumstance we do not want to cold
/// boot. See: http://issues.umbraco.org/issue/U4-8627
/// </remarks>
2017-05-12 14:49:44 +02:00
private void EnsureInstructions ( IUmbracoDatabase database )
2015-07-14 16:21:05 +02:00
{
2016-06-23 16:53:50 +02:00
if ( _lastId = = 0 )
{
2016-09-01 19:06:08 +02:00
var sql = Sql ( ) . Select ( "COUNT(*)" )
2016-07-08 16:32:06 +02:00
. From < CacheInstructionDto > ( ) ;
2015-07-14 16:21:05 +02:00
2017-05-12 14:49:44 +02:00
var count = database . ExecuteScalar < int > ( sql ) ;
2016-01-05 14:20:13 +01:00
2016-06-23 16:53:50 +02:00
//if there are instructions but we haven't synced, then a cold boot is necessary
if ( count > 0 )
_lastId = - 1 ;
}
else
{
2016-09-01 19:06:08 +02:00
var sql = Sql ( ) . SelectAll ( )
2016-07-08 16:32:06 +02:00
. From < CacheInstructionDto > ( )
. Where < CacheInstructionDto > ( dto = > dto . Id = = _lastId ) ;
2015-07-14 16:21:05 +02:00
2017-05-12 14:49:44 +02:00
var dtos = database . Fetch < CacheInstructionDto > ( sql ) ;
2016-01-05 14:20:13 +01:00
2016-06-23 16:53:50 +02:00
//if the last synced instruction is not found in the db, then a cold boot is necessary
if ( dtos . Count = = 0 )
_lastId = - 1 ;
}
2015-07-14 16:21:05 +02:00
}
2016-01-28 14:19:32 +01:00
2015-03-04 12:16:28 +01:00
/// <summary>
/// Reads the last-synced id from file into memory.
/// </summary>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// </remarks>
private void ReadLastSynced ( )
{
2018-03-21 09:06:32 +01:00
if ( File . Exists ( DistCacheFilePath ) = = false ) return ;
2015-03-04 12:16:28 +01:00
2018-03-21 09:06:32 +01:00
var content = File . ReadAllText ( DistCacheFilePath ) ;
if ( int . TryParse ( content , out var last ) )
2015-03-04 12:16:28 +01:00
_lastId = last ;
}
/// <summary>
/// Updates the in-memory last-synced id and persists it to file.
/// </summary>
/// <param name="id">The id.</param>
/// <remarks>
/// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// </remarks>
private void SaveLastSynced ( int id )
{
2018-03-21 09:06:32 +01:00
File . WriteAllText ( DistCacheFilePath , id . ToString ( CultureInfo . InvariantCulture ) ) ;
2015-03-04 12:16:28 +01:00
_lastId = id ;
}
/// <summary>
2015-07-07 19:36:17 +02:00
/// Gets the unique local identity of the executing AppDomain.
2015-03-04 12:16:28 +01:00
/// </summary>
2015-07-07 19:36:17 +02:00
/// <remarks>
/// <para>It is not only about the "server" (machine name and appDomainappId), but also about
/// an AppDomain, within a Process, on that server - because two AppDomains running at the same
/// time on the same server (eg during a restart) are, practically, a LB setup.</para>
/// <para>Practically, all we really need is the guid, the other infos are here for information
/// and debugging purposes.</para>
/// </remarks>
2020-08-21 00:02:35 +10:00
protected string LocalIdentity { get ; }
2015-03-04 12:16:28 +01:00
2019-11-20 13:38:41 +01:00
private string GetDistCacheFilePath ( IHostingEnvironment hostingEnvironment )
2015-03-04 12:16:28 +01:00
{
2019-11-20 15:21:09 +01:00
var fileName = _hostingEnvironment . ApplicationId . ReplaceNonAlphanumericChars ( string . Empty ) + "-lastsynced.txt" ;
2015-03-04 12:16:28 +01:00
2019-11-20 13:38:41 +01:00
var distCacheFilePath = Path . Combine ( hostingEnvironment . LocalTempPath , "DistCache" , fileName ) ;
2018-03-21 09:06:32 +01:00
//ensure the folder exists
var folder = Path . GetDirectoryName ( distCacheFilePath ) ;
if ( folder = = null )
throw new InvalidOperationException ( "The folder could not be determined for the file " + distCacheFilePath ) ;
if ( Directory . Exists ( folder ) = = false )
Directory . CreateDirectory ( folder ) ;
return distCacheFilePath ;
2015-03-04 12:16:28 +01:00
}
#endregion
#region Notify refreshers
2019-12-18 11:16:43 +01:00
private ICacheRefresher GetRefresher ( Guid id )
2015-03-04 12:16:28 +01:00
{
2019-12-18 11:16:43 +01:00
var refresher = _cacheRefreshers [ id ] ;
2015-03-04 12:16:28 +01:00
if ( refresher = = null )
throw new InvalidOperationException ( "Cache refresher with ID \"" + id + "\" does not exist." ) ;
return refresher ;
}
2019-12-18 11:16:43 +01:00
private IJsonCacheRefresher GetJsonRefresher ( Guid id )
2015-03-04 12:16:28 +01:00
{
return GetJsonRefresher ( GetRefresher ( id ) ) ;
}
private static IJsonCacheRefresher GetJsonRefresher ( ICacheRefresher refresher )
{
var jsonRefresher = refresher as IJsonCacheRefresher ;
if ( jsonRefresher = = null )
2016-05-26 17:12:04 +02:00
throw new InvalidOperationException ( "Cache refresher with ID \"" + refresher . RefresherUniqueId + "\" does not implement " + typeof ( IJsonCacheRefresher ) + "." ) ;
2015-03-04 12:16:28 +01:00
return jsonRefresher ;
}
2016-10-25 15:50:33 +02:00
/// <summary>
/// Parses out the individual instructions to be processed
/// </summary>
/// <param name="jsonArray"></param>
/// <returns></returns>
private static List < RefreshInstruction > GetAllInstructions ( IEnumerable < JToken > jsonArray )
2015-03-04 12:16:28 +01:00
{
2016-10-25 15:50:33 +02:00
var result = new List < RefreshInstruction > ( ) ;
2015-03-04 12:16:28 +01:00
foreach ( var jsonItem in jsonArray )
{
// could be a JObject in which case we can convert to a RefreshInstruction,
// otherwise it could be another JArray - in which case we'll iterate that.
var jsonObj = jsonItem as JObject ;
if ( jsonObj ! = null )
{
var instruction = jsonObj . ToObject < RefreshInstruction > ( ) ;
2016-10-25 15:50:33 +02:00
result . Add ( instruction ) ;
2015-03-04 12:16:28 +01:00
}
else
{
2016-10-25 15:50:33 +02:00
var jsonInnerArray = ( JArray ) jsonItem ;
result . AddRange ( GetAllInstructions ( jsonInnerArray ) ) ; // recurse
}
}
return result ;
}
/// <summary>
/// executes the instructions against the cache refresher instances
/// </summary>
/// <param name="instructions"></param>
2016-10-26 12:19:36 +02:00
/// <param name="processed"></param>
2016-10-25 16:23:49 +02:00
/// <returns>
2019-01-22 18:03:39 -05:00
/// Returns true if all instructions were processed, otherwise false if the processing was interrupted (i.e. app shutdown)
2016-10-25 16:23:49 +02:00
/// </returns>
2016-10-26 12:19:36 +02:00
private bool NotifyRefreshers ( IEnumerable < RefreshInstruction > instructions , HashSet < RefreshInstruction > processed )
2016-10-25 15:50:33 +02:00
{
foreach ( var instruction in instructions )
{
2016-10-25 16:23:49 +02:00
//Check if the app is shutting down, we need to exit if this happens.
if ( _released )
{
return false ;
}
2016-10-26 11:37:59 +02:00
//this has already been processed
if ( processed . Contains ( instruction ) )
continue ;
2016-10-25 15:50:33 +02:00
switch ( instruction . RefreshType )
{
case RefreshMethodType . RefreshAll :
RefreshAll ( instruction . RefresherId ) ;
break ;
case RefreshMethodType . RefreshByGuid :
RefreshByGuid ( instruction . RefresherId , instruction . GuidId ) ;
break ;
case RefreshMethodType . RefreshById :
RefreshById ( instruction . RefresherId , instruction . IntId ) ;
break ;
case RefreshMethodType . RefreshByIds :
RefreshByIds ( instruction . RefresherId , instruction . JsonIds ) ;
break ;
case RefreshMethodType . RefreshByJson :
RefreshByJson ( instruction . RefresherId , instruction . JsonPayload ) ;
break ;
case RefreshMethodType . RemoveById :
RemoveById ( instruction . RefresherId , instruction . IntId ) ;
break ;
2015-03-04 12:16:28 +01:00
}
2016-10-26 11:37:59 +02:00
processed . Add ( instruction ) ;
2015-03-04 12:16:28 +01:00
}
2016-10-25 16:23:49 +02:00
return true ;
2015-03-04 12:16:28 +01:00
}
2019-12-18 11:16:43 +01:00
private void RefreshAll ( Guid uniqueIdentifier )
2015-03-04 12:16:28 +01:00
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . RefreshAll ( ) ;
}
2019-12-18 11:16:43 +01:00
private void RefreshByGuid ( Guid uniqueIdentifier , Guid id )
2015-03-04 12:16:28 +01:00
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . Refresh ( id ) ;
}
2019-12-18 11:16:43 +01:00
private void RefreshById ( Guid uniqueIdentifier , int id )
2015-03-04 12:16:28 +01:00
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . Refresh ( id ) ;
}
2019-12-18 11:16:43 +01:00
private void RefreshByIds ( Guid uniqueIdentifier , string jsonIds )
2015-03-04 12:16:28 +01:00
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
foreach ( var id in JsonConvert . DeserializeObject < int [ ] > ( jsonIds ) )
refresher . Refresh ( id ) ;
}
2019-12-18 11:16:43 +01:00
private void RefreshByJson ( Guid uniqueIdentifier , string jsonPayload )
2015-03-04 12:16:28 +01:00
{
var refresher = GetJsonRefresher ( uniqueIdentifier ) ;
refresher . Refresh ( jsonPayload ) ;
}
2019-12-18 11:16:43 +01:00
private void RemoveById ( Guid uniqueIdentifier , int id )
2015-03-04 12:16:28 +01:00
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . Remove ( id ) ;
}
#endregion
}
2016-01-05 14:20:13 +01:00
}