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 System.Web ;
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.IO ;
using Umbraco.Core.Logging ;
using Umbraco.Core.Models.Rdbms ;
using Umbraco.Core.Persistence ;
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
//
2015-07-15 17:27:01 +02:00
public class DatabaseServerMessenger : ServerMessengerBase
2015-03-04 12:16:28 +01:00
{
2016-09-01 19:06:08 +02:00
private readonly IRuntimeState _runtime ;
2015-07-03 15:33:07 +02:00
private readonly ManualResetEvent _syncIdle ;
private readonly object _locko = new object ( ) ;
2016-09-01 19:06:08 +02:00
private readonly ProfilingLogger _profilingLogger ;
2017-05-12 14:49:44 +02:00
private readonly IDatabaseContext _databaseContext ;
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
2016-11-29 10:31:25 +01:00
protected DatabaseServerMessengerOptions Options { get ; }
2015-03-04 12:16:28 +01:00
2016-09-01 19:06:08 +02:00
public DatabaseServerMessenger (
2017-05-12 14:49:44 +02:00
IRuntimeState runtime , IScopeProvider scopeProvider , IDatabaseContext databaseContext , ILogger logger , ProfilingLogger proflog ,
2016-09-01 19:06:08 +02:00
bool distributedEnabled , DatabaseServerMessengerOptions options )
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 ) ) ;
Logger = logger ? ? throw new ArgumentNullException ( nameof ( logger ) ) ;
_databaseContext = databaseContext ;
2016-09-01 19:06:08 +02:00
_runtime = runtime ;
2017-05-12 14:49:44 +02:00
_profilingLogger = proflog ? ? throw new ArgumentNullException ( nameof ( proflog ) ) ;
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 ) ;
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-05-12 14:49:44 +02:00
protected Sql < SqlContext > Sql ( ) = > _databaseContext . Sql ( ) ;
2016-09-01 19:06:08 +02:00
2015-03-04 12:16:28 +01:00
#region Messenger
protected override bool RequiresDistributed ( IEnumerable < IServerAddress > servers , ICacheRefresher refresher , MessageType dispatchType )
{
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 (
IEnumerable < IServerAddress > servers ,
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
Type idType ;
if ( GetArrayType ( idsA , out 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 ) ,
2015-07-07 19:36:17 +02:00
OriginIdentity = LocalIdentity
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
{
2015-07-03 15:33:07 +02:00
// weight:10, must release *before* the facade service, because once released
// the service will *not* be able to properly handle our notifications anymore
const int weight = 10 ;
2016-09-01 19:06:08 +02:00
var runtime = _runtime as RuntimeState ;
if ( runtime = = null ) throw new NotSupportedException ( $"Unsupported IRuntimeState implementation {_runtime.GetType().FullName}, expecting {typeof(RuntimeState).FullName}." ) ;
var registered = runtime . 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
{
2016-11-04 18:40:42 +01:00
Logger . Warn < 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 ( ) )
{
EnsureInstructions ( scope . Database ) ; // reset _lastId if instrs are missing
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.
2016-09-01 19:06:08 +02:00
Logger . Warn < 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
{
//check for how many instructions there are to process
2016-10-26 12:01:43 +02:00
//TODO: In 7.6 we need to store the count of instructions per row since this is not affective because there can be far more than one (if not thousands)
// of instructions in a single row.
2017-05-12 14:49:44 +02:00
var count = database . ExecuteScalar < int > ( "SELECT COUNT(*) 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
2016-09-01 19:06:08 +02:00
Logger . Warn < DatabaseServerMessenger > ( "The instruction count ({0}) exceeds the specified MaxProcessingInstructionCount ({1})."
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"
+ " to the latest found in the database and maintain cache updates based on that Id." ,
2016-10-25 16:01:55 +02:00
( ) = > 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>
protected void Sync ( )
{
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 ;
2016-09-01 19:06:08 +02:00
switch ( Current . RuntimeState . ServerRole )
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.
//
// FIXME not true if we're running on a background thread, assuming we can?
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
2016-10-26 11:37:59 +02:00
// some memory however we cannot do thta because inside of this loop the cache refreshers are also
// 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 )
{
2017-05-12 14:49:44 +02:00
Logger . Error < DatabaseServerMessenger > ( $"Failed to deserialize instructions ({dto.Id}: \" { dto . Instructions } \ ")." , ex ) ;
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
{
2016-11-04 18:40:42 +01:00
Logger . Info < DatabaseServerMessenger > ( "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)
//{
// //This will occur if the instructions processing is taking too long since this is occuring on a request thread.
// // Or possibly if IIS terminates the appdomain. In any case, we should deal with this differently perhaps...
//}
catch ( Exception ex )
{
2016-09-01 19:06:08 +02:00
Logger . Error < DatabaseServerMessenger > (
$"DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({dto.Id}: \" { dto . Instructions } \ "). Instruction is being skipped/ignored" , ex ) ;
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 ( )
{
var path = SyncFilePath ;
if ( File . Exists ( path ) = = false ) return ;
var content = File . ReadAllText ( path ) ;
2017-05-12 14:49:44 +02:00
if ( int . TryParse ( content , out int 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 )
{
File . WriteAllText ( SyncFilePath , id . ToString ( CultureInfo . InvariantCulture ) ) ;
_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>
2016-07-11 10:09:17 +02:00
protected static readonly string LocalIdentity = NetworkHelper . MachineName // eg DOMAIN\SERVER
2015-07-07 19:36:17 +02:00
+ "/" + HttpRuntime . AppDomainAppId // 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
/// <summary>
/// Gets the sync file path for the local server.
/// </summary>
/// <returns>The sync file path for the local server.</returns>
private static string SyncFilePath
{
get
{
var tempFolder = IOHelper . MapPath ( "~/App_Data/TEMP/DistCache/" + NetworkHelper . FileSafeMachineName ) ;
if ( Directory . Exists ( tempFolder ) = = false )
Directory . CreateDirectory ( tempFolder ) ;
return Path . Combine ( tempFolder , HttpRuntime . AppDomainAppId . ReplaceNonAlphanumericChars ( string . Empty ) + "-lastsynced.txt" ) ;
}
}
#endregion
#region Notify refreshers
private static ICacheRefresher GetRefresher ( Guid id )
{
2016-08-13 16:02:35 +02:00
var refresher = Current . 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 ;
}
private static IJsonCacheRefresher GetJsonRefresher ( Guid id )
{
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>
/// Returns true if all instructions were processed, otherwise false if the processing was interupted (i.e. app shutdown)
/// </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
}
private static void RefreshAll ( Guid uniqueIdentifier )
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . RefreshAll ( ) ;
}
private static void RefreshByGuid ( Guid uniqueIdentifier , Guid id )
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . Refresh ( id ) ;
}
private static void RefreshById ( Guid uniqueIdentifier , int id )
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . Refresh ( id ) ;
}
private static void RefreshByIds ( Guid uniqueIdentifier , string jsonIds )
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
foreach ( var id in JsonConvert . DeserializeObject < int [ ] > ( jsonIds ) )
refresher . Refresh ( id ) ;
}
private static void RefreshByJson ( Guid uniqueIdentifier , string jsonPayload )
{
var refresher = GetJsonRefresher ( uniqueIdentifier ) ;
refresher . Refresh ( jsonPayload ) ;
}
private static void RemoveById ( Guid uniqueIdentifier , int id )
{
var refresher = GetRefresher ( uniqueIdentifier ) ;
refresher . Remove ( id ) ;
}
#endregion
}
2016-01-05 14:20:13 +01:00
}