Merge branch 'v8/8.6' into v8/8.7
# Conflicts: # src/SolutionInfo.cs # src/Umbraco.Core/Runtime/SqlMainDomLock.cs # src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js # src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js
This commit is contained in:
@@ -4,6 +4,10 @@ using System.Data.SqlClient;
|
||||
|
||||
namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
{
|
||||
// See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues
|
||||
// Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ?
|
||||
// but i guess that's not netcore so we'll just leave it.
|
||||
|
||||
/// <summary>
|
||||
/// Provides the transient error detection logic for transient faults that are specific to SQL Azure.
|
||||
/// </summary>
|
||||
@@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception object to be verified.</param>
|
||||
/// <returns>True if the specified exception is considered as transient, otherwise false.</returns>
|
||||
/// <returns>true if the specified exception is considered as transient; otherwise, false.</returns>
|
||||
public bool IsTransient(Exception ex)
|
||||
{
|
||||
if (ex != null)
|
||||
@@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
|
||||
|
||||
return true;
|
||||
|
||||
// SQL Error Code: 40197
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40197:
|
||||
// SQL Error Code: 10928
|
||||
// Resource ID: %d. The %s limit for the database is %d and has been reached.
|
||||
case 10928:
|
||||
// SQL Error Code: 10929
|
||||
// Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d.
|
||||
// However, the server is currently too busy to support requests greater than %d for this database.
|
||||
case 10929:
|
||||
// SQL Error Code: 10053
|
||||
// A transport-level error has occurred when receiving results from the server.
|
||||
// An established connection was aborted by the software in your host machine.
|
||||
case 10053:
|
||||
// SQL Error Code: 10054
|
||||
// A transport-level error has occurred when sending the request to the server.
|
||||
// A transport-level error has occurred when sending the request to the server.
|
||||
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
|
||||
case 10054:
|
||||
// SQL Error Code: 10060
|
||||
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
|
||||
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
|
||||
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
|
||||
// because the connected party did not properly respond after a period of time, or established connection failed
|
||||
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
|
||||
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
|
||||
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
|
||||
// because the connected party did not properly respond after a period of time, or established connection failed
|
||||
// because connected host has failed to respond.)"}
|
||||
case 10060:
|
||||
// SQL Error Code: 40197
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40197:
|
||||
// SQL Error Code: 40540
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40540:
|
||||
// SQL Error Code: 40613
|
||||
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
|
||||
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
|
||||
// support, and provide them the session tracing ID of ZZZZZ.
|
||||
case 40613:
|
||||
// SQL Error Code: 40143
|
||||
// The service has encountered an error processing your request. Please try again.
|
||||
case 40143:
|
||||
// SQL Error Code: 233
|
||||
// The client was unable to establish a connection because of an error during connection initialization process before login.
|
||||
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
|
||||
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
|
||||
// The client was unable to establish a connection because of an error during connection initialization process before login.
|
||||
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
|
||||
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
|
||||
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
|
||||
case 233:
|
||||
// SQL Error Code: 64
|
||||
// A connection was successfully established with the server, but then an error occurred during the login process.
|
||||
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
|
||||
// A connection was successfully established with the server, but then an error occurred during the login process.
|
||||
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
|
||||
case 64:
|
||||
// DBNETLIB Error Code: 20
|
||||
// The instance of SQL Server you attempted to connect to does not support encryption.
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// </summary>
|
||||
void ClearSchedule(DateTime date);
|
||||
|
||||
void ClearSchedule(DateTime date, ContentScheduleAction action);
|
||||
|
||||
bool HasContentForExpiration(DateTime date);
|
||||
bool HasContentForRelease(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// Gets <see cref="IContent"/> objects having an expiration date before (lower than, or equal to) a specified date.
|
||||
/// </summary>
|
||||
|
||||
@@ -1017,6 +1017,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
Database.Execute(sql);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearSchedule(DateTime date, ContentScheduleAction action)
|
||||
{
|
||||
var a = action.ToString();
|
||||
var sql = Sql().Delete<ContentScheduleDto>().Where<ContentScheduleDto>(x => x.Date <= date && x.Action == a);
|
||||
Database.Execute(sql);
|
||||
}
|
||||
|
||||
private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date)
|
||||
{
|
||||
var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql
|
||||
.SelectCount()
|
||||
.From<ContentScheduleDto>()
|
||||
.Where<ContentScheduleDto>(x => x.Action == SqlTemplate.Arg<string>("action") && x.Date <= SqlTemplate.Arg<DateTime>("date")));
|
||||
|
||||
var sql = template.Sql(action.ToString(), date);
|
||||
return sql;
|
||||
}
|
||||
|
||||
public bool HasContentForExpiration(DateTime date)
|
||||
{
|
||||
var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date);
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
public bool HasContentForRelease(DateTime date)
|
||||
{
|
||||
var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date);
|
||||
return Database.ExecuteScalar<int>(sql) > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContent> GetContentForRelease(DateTime date)
|
||||
{
|
||||
|
||||
@@ -21,12 +21,11 @@ namespace Umbraco.Core.Runtime
|
||||
private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom";
|
||||
private const string UpdatedSuffix = "_updated";
|
||||
private readonly ILogger _logger;
|
||||
private IUmbracoDatabase _db;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider();
|
||||
private bool _mainDomChanging = false;
|
||||
private readonly UmbracoDatabaseFactory _dbFactory;
|
||||
private bool _hasError;
|
||||
private bool _errorDuringAcquiring;
|
||||
private object _locker = new object();
|
||||
|
||||
public SqlMainDomLock(ILogger logger)
|
||||
@@ -56,25 +55,24 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Acquiring lock...");
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
var tempId = Guid.NewGuid().ToString();
|
||||
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
// wait to get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch(SqlException ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not acquire MainDom.");
|
||||
_hasError = true;
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,15 +80,12 @@ namespace Umbraco.Core.Runtime
|
||||
throw;
|
||||
}
|
||||
|
||||
var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown
|
||||
var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown
|
||||
if (result == RecordPersistenceType.Insert)
|
||||
{
|
||||
// if we've inserted, then there was no MainDom so we can instantly acquire
|
||||
|
||||
// TODO: see the other TODO, could we just delete the row and that would indicate that we
|
||||
// are MainDom? then we don't leave any orphan rows behind.
|
||||
|
||||
InsertLockRecord(_lockId); // so update with our appdomain id
|
||||
InsertLockRecord(_lockId, db); // so update with our appdomain id
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
@@ -100,23 +95,23 @@ namespace Umbraco.Core.Runtime
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, cannot acquire MainDom");
|
||||
_hasError = true;
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
transaction.Complete();
|
||||
}
|
||||
|
||||
|
||||
return await WaitForExistingAsync(tempId, millisecondsTimeout);
|
||||
}
|
||||
|
||||
public Task ListenAsync()
|
||||
{
|
||||
if (_hasError)
|
||||
if (_errorDuringAcquiring)
|
||||
{
|
||||
_logger.Warn<SqlMainDomLock>("Could not acquire MainDom, listening is canceled.");
|
||||
return Task.CompletedTask;
|
||||
@@ -142,8 +137,15 @@ namespace Umbraco.Core.Runtime
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// poll every 1 second
|
||||
Thread.Sleep(1000);
|
||||
// poll every couple of seconds
|
||||
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
|
||||
Thread.Sleep(2000);
|
||||
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
// if we aren't configured, we just keep looping since we can't query the db
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_dbFactory.Configured)
|
||||
{
|
||||
@@ -160,20 +162,14 @@ namespace Umbraco.Core.Runtime
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var db = GetDatabase();
|
||||
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that
|
||||
// we are still the maindom. An empty value might be better because then we won't have any orphan rows
|
||||
// if the app is terminated. Could that work?
|
||||
|
||||
if (!IsMainDomValue(_lockId))
|
||||
if (!IsMainDomValue(_lockId, db))
|
||||
{
|
||||
// we are no longer main dom, another one has come online, exit
|
||||
_mainDomChanging = true;
|
||||
@@ -183,38 +179,23 @@ namespace Umbraco.Core.Runtime
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, listening is canceled.");
|
||||
_hasError = true;
|
||||
return;
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during listening.");
|
||||
|
||||
// We need to keep on listening unless we've been notified by our own AppDomain to shutdown since
|
||||
// we don't want to shutdown resources controlled by MainDom inadvertently. We'll just keep listening otherwise.
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetDatabase()
|
||||
{
|
||||
if (_db.InTransaction)
|
||||
_db.AbortTransaction();
|
||||
_db.Dispose();
|
||||
_db = null;
|
||||
}
|
||||
|
||||
private IUmbracoDatabase GetDatabase()
|
||||
{
|
||||
if (_db != null)
|
||||
return _db;
|
||||
|
||||
_db = _dbFactory.CreateDatabase();
|
||||
return _db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for any existing MainDom to release so we can continue booting
|
||||
/// </summary>
|
||||
@@ -227,121 +208,131 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var db = GetDatabase();
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
while(true)
|
||||
while (true)
|
||||
{
|
||||
// poll very often, we need to take over as fast as we can
|
||||
Thread.Sleep(100);
|
||||
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
|
||||
Thread.Sleep(1000);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// the row
|
||||
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
|
||||
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
|
||||
{
|
||||
// the other main dom has updated our record
|
||||
// Or the other maindom shutdown super fast and just deleted the record
|
||||
// which indicates that we
|
||||
// can acquire it and it has shutdown.
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
|
||||
{
|
||||
// in this case, the prefixed ID is different which means
|
||||
// another new AppDomain has come online and is wanting to take over. In that case, we will not
|
||||
// acquire.
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
var acquired = TryAcquire(db, tempId, updatedTempId);
|
||||
if (acquired.HasValue)
|
||||
return acquired.Value;
|
||||
|
||||
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
|
||||
{
|
||||
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
|
||||
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
|
||||
// and it's just been left as an orphan row.
|
||||
// There's really know way of knowing unless we are constantly updating the row for the current maindom
|
||||
// which isn't ideal.
|
||||
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
|
||||
if (IsLockTimeoutException(ex))
|
||||
{
|
||||
// something is wrong, we cannot acquire, not much we can do
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
|
||||
_hasError = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
}
|
||||
return AcquireWhenMaxWaitTimeElapsed(db);
|
||||
}
|
||||
}
|
||||
}, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId)
|
||||
{
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
// get a read lock
|
||||
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// the row
|
||||
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
|
||||
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
|
||||
{
|
||||
// the other main dom has updated our record
|
||||
// Or the other maindom shutdown super fast and just deleted the record
|
||||
// which indicates that we
|
||||
// can acquire it and it has shutdown.
|
||||
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId, db);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
|
||||
{
|
||||
// in this case, the prefixed ID is different which means
|
||||
// another new AppDomain has come online and is wanting to take over. In that case, we will not
|
||||
// acquire.
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex as SqlException))
|
||||
{
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
// unexpected
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
transaction.Complete();
|
||||
}
|
||||
|
||||
return null; // continue
|
||||
}
|
||||
|
||||
private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db)
|
||||
{
|
||||
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
|
||||
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
|
||||
// and it's just been left as an orphan row.
|
||||
// There's really know way of knowing unless we are constantly updating the row for the current maindom
|
||||
// which isn't ideal.
|
||||
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
|
||||
|
||||
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
|
||||
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
// so now we update the row with our appdomain id
|
||||
InsertLockRecord(_lockId, db);
|
||||
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (IsLockTimeoutException(ex as SqlException))
|
||||
{
|
||||
// something is wrong, we cannot acquire, not much we can do
|
||||
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
|
||||
_errorDuringAcquiring = true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts or updates the key/value row
|
||||
/// </summary>
|
||||
private RecordPersistenceType InsertLockRecord(string id)
|
||||
private RecordPersistenceType InsertLockRecord(string id, IUmbracoDatabase db)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.InsertOrUpdate(new KeyValueDto
|
||||
{
|
||||
Key = MainDomKey,
|
||||
@@ -354,9 +345,8 @@ namespace Umbraco.Core.Runtime
|
||||
/// Checks if the DB row value is equals the value
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool IsMainDomValue(string val)
|
||||
private bool IsMainDomValue(string val, IUmbracoDatabase db)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
return db.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val",
|
||||
new { key = MainDomKey, val = val }) == 1;
|
||||
}
|
||||
@@ -366,7 +356,7 @@ namespace Umbraco.Core.Runtime
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222;
|
||||
private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222;
|
||||
|
||||
#region IDisposable Support
|
||||
private bool _disposedValue = false; // To detect redundant calls
|
||||
@@ -385,11 +375,11 @@ namespace Umbraco.Core.Runtime
|
||||
|
||||
if (_dbFactory.Configured)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
using var db = _dbFactory.CreateDatabase();
|
||||
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
try
|
||||
{
|
||||
db.BeginTransaction(IsolationLevel.ReadCommitted);
|
||||
|
||||
// get a write lock
|
||||
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
|
||||
|
||||
@@ -402,24 +392,21 @@ namespace Umbraco.Core.Runtime
|
||||
if (_mainDomChanging)
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, updating row, new application is booting.");
|
||||
db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
var count = db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug<SqlMainDomLock>("Releasing MainDom, deleting row, application is shutting down.");
|
||||
db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
var count = db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResetDatabase();
|
||||
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
|
||||
_hasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db?.CompleteTransaction();
|
||||
ResetDatabase();
|
||||
transaction.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
private IQuery<IContent> _queryNotTrashed;
|
||||
//TODO: The non-lazy object should be injected
|
||||
private readonly Lazy<PropertyValidationService> _propertyValidationService = new Lazy<PropertyValidationService>(() => new PropertyValidationService());
|
||||
|
||||
|
||||
|
||||
#region Constructors
|
||||
|
||||
@@ -879,7 +879,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types.");
|
||||
}
|
||||
|
||||
if(content.Name != null && content.Name.Length > 255)
|
||||
if (content.Name != null && content.Name.Length > 255)
|
||||
{
|
||||
throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
|
||||
}
|
||||
@@ -1247,7 +1247,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (culturesUnpublishing != null)
|
||||
{
|
||||
// This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
|
||||
|
||||
|
||||
var langs = string.Join(", ", allLangs
|
||||
.Where(x => culturesUnpublishing.InvariantContains(x.IsoCode))
|
||||
.Select(x => x.CultureName));
|
||||
@@ -1256,7 +1256,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
if (publishResult == null)
|
||||
throw new PanicException("publishResult == null - should not happen");
|
||||
|
||||
switch(publishResult.Result)
|
||||
switch (publishResult.Result)
|
||||
{
|
||||
case PublishResultType.FailedPublishMandatoryCultureMissing:
|
||||
//occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
|
||||
@@ -1270,7 +1270,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)");
|
||||
return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Audit(AuditType.Unpublish, userId, content.Id);
|
||||
@@ -1290,7 +1290,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
changeType = TreeChangeTypes.RefreshBranch; // whole branch
|
||||
else if (isNew == false && previouslyPublished)
|
||||
changeType = TreeChangeTypes.RefreshNode; // single node
|
||||
|
||||
|
||||
|
||||
// invalidate the node/branch
|
||||
if (!branchOne) // for branches, handled by SaveAndPublishBranch
|
||||
@@ -1364,24 +1364,90 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PublishResult> PerformScheduledPublish(DateTime date)
|
||||
=> PerformScheduledPublishInternal(date).ToList();
|
||||
|
||||
// beware! this method yields results, so the returned IEnumerable *must* be
|
||||
// enumerated for anything to happen - dangerous, so private + exposed via
|
||||
// the public method above, which forces ToList().
|
||||
private IEnumerable<PublishResult> PerformScheduledPublishInternal(DateTime date)
|
||||
{
|
||||
var allLangs = new Lazy<List<ILanguage>>(() => _languageRepository.GetMany().ToList());
|
||||
var evtMsgs = EventMessagesFactory.Get();
|
||||
var results = new List<PublishResult>();
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
|
||||
PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void PerformScheduledPublishingExpiration(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
|
||||
{
|
||||
using var scope = ScopeProvider.CreateScope();
|
||||
|
||||
// do a fast read without any locks since this executes often to see if we even need to proceed
|
||||
if (_documentRepository.HasContentForExpiration(date))
|
||||
{
|
||||
// now take a write lock since we'll be updating
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
var allLangs = _languageRepository.GetMany().ToList();
|
||||
foreach (var d in _documentRepository.GetContentForExpiration(date))
|
||||
{
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
|
||||
.Select(x => x.Culture)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
continue; //shouldn't happen but no point in processing this document if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
{
|
||||
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var c in pendingCultures)
|
||||
{
|
||||
//Clear this schedule for this culture
|
||||
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
|
||||
//set the culture to be published
|
||||
d.UnpublishCulture(c);
|
||||
}
|
||||
|
||||
var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
results.Add(result);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
|
||||
var result = Unpublish(d, userId: d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
_documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private void PerformScheduledPublishingRelease(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
|
||||
{
|
||||
using var scope = ScopeProvider.CreateScope();
|
||||
|
||||
// do a fast read without any locks since this executes often to see if we even need to proceed
|
||||
if (_documentRepository.HasContentForRelease(date))
|
||||
{
|
||||
// now take a write lock since we'll be updating
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
foreach (var d in _documentRepository.GetContentForRelease(date))
|
||||
{
|
||||
PublishResult result;
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
@@ -1391,11 +1457,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
.ToList();
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
continue; //shouldn't happen but no point in processing this document if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
{
|
||||
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
|
||||
continue; // this document is canceled move next
|
||||
}
|
||||
|
||||
var publishing = true;
|
||||
foreach (var culture in pendingCultures)
|
||||
@@ -1407,94 +1476,51 @@ namespace Umbraco.Core.Services.Implement
|
||||
|
||||
//publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
|
||||
Property[] invalidProperties = null;
|
||||
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture));
|
||||
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture));
|
||||
var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
|
||||
if (invalidProperties != null && invalidProperties.Length > 0)
|
||||
Logger.Warn<ContentService>("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
|
||||
d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
|
||||
|
||||
publishing &= tryPublish; //set the culture to be published
|
||||
if (!publishing) break; // no point continuing
|
||||
if (!publishing) continue; // move to next document
|
||||
}
|
||||
|
||||
PublishResult result;
|
||||
|
||||
if (d.Trashed)
|
||||
result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
|
||||
else if (!publishing)
|
||||
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
|
||||
else
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
|
||||
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
|
||||
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
|
||||
yield return result;
|
||||
results.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Release, date);
|
||||
|
||||
result = d.Trashed
|
||||
var result = d.Trashed
|
||||
? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d)
|
||||
: SaveAndPublish(d, userId: d.WriterId);
|
||||
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
|
||||
yield return result;
|
||||
results.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var d in _documentRepository.GetContentForExpiration(date))
|
||||
{
|
||||
PublishResult result;
|
||||
if (d.ContentType.VariesByCulture())
|
||||
{
|
||||
//find which cultures have pending schedules
|
||||
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
|
||||
.Select(x => x.Culture)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
_documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
|
||||
|
||||
if (pendingCultures.Count == 0)
|
||||
break; //shouldn't happen but no point in continuing if there's nothing there
|
||||
|
||||
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
|
||||
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
|
||||
|
||||
foreach (var c in pendingCultures)
|
||||
{
|
||||
//Clear this schedule for this culture
|
||||
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
|
||||
//set the culture to be published
|
||||
d.UnpublishCulture(c);
|
||||
}
|
||||
|
||||
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
//Clear this schedule
|
||||
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
|
||||
result = Unpublish(d, userId: d.WriterId);
|
||||
if (result.Success == false)
|
||||
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
|
||||
yield return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
_documentRepository.ClearSchedule(date);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
// utility 'PublishCultures' func used by SaveAndPublishBranch
|
||||
@@ -2650,7 +2676,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
// there will be nothing to publish/unpublish.
|
||||
return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// missing mandatory culture = cannot be published
|
||||
var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
|
||||
@@ -3163,6 +3189,6 @@ namespace Umbraco.Core.Services.Implement
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Examine;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
|
||||
namespace Umbraco.Examine
|
||||
{
|
||||
@@ -12,12 +14,20 @@ namespace Umbraco.Examine
|
||||
/// </summary>
|
||||
public class IndexRebuilder
|
||||
{
|
||||
private readonly IProfilingLogger _logger;
|
||||
private readonly IEnumerable<IIndexPopulator> _populators;
|
||||
public IExamineManager ExamineManager { get; }
|
||||
|
||||
[Obsolete("Use constructor with all dependencies")]
|
||||
public IndexRebuilder(IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
|
||||
: this(Current.ProfilingLogger, examineManager, populators)
|
||||
{
|
||||
}
|
||||
|
||||
public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
|
||||
{
|
||||
_populators = populators;
|
||||
_logger = logger;
|
||||
ExamineManager = examineManager;
|
||||
}
|
||||
|
||||
@@ -50,8 +60,18 @@ namespace Umbraco.Examine
|
||||
index.CreateIndex(); // clear the index
|
||||
}
|
||||
|
||||
//run the populators in parallel against all indexes
|
||||
Parallel.ForEach(_populators, populator => populator.Populate(indexes));
|
||||
// run each populator over the indexes
|
||||
foreach(var populator in _populators)
|
||||
{
|
||||
try
|
||||
{
|
||||
populator.Populate(indexes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error<IndexRebuilder>(e, "Index populating failed for populator {Populator}", populator.GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
371
src/Umbraco.TestData/LoadTestController.cs
Normal file
371
src/Umbraco.TestData/LoadTestController.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Models;
|
||||
using System.Web;
|
||||
using System.Web.Hosting;
|
||||
using System.Web.Routing;
|
||||
using System.Diagnostics;
|
||||
using Umbraco.Core.Composing;
|
||||
using System.Configuration;
|
||||
|
||||
// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting
|
||||
|
||||
namespace Umbraco.TestData
|
||||
{
|
||||
public class LoadTestController : Controller
|
||||
{
|
||||
public LoadTestController(ServiceContext serviceContext)
|
||||
{
|
||||
_serviceContext = serviceContext;
|
||||
}
|
||||
|
||||
private static readonly Random _random = new Random();
|
||||
private static readonly object _locko = new object();
|
||||
|
||||
private static volatile int _containerId = -1;
|
||||
|
||||
private const string _containerAlias = "LoadTestContainer";
|
||||
private const string _contentAlias = "LoadTestContent";
|
||||
private const int _textboxDefinitionId = -88;
|
||||
private const int _maxCreate = 1000;
|
||||
|
||||
private static readonly string HeadHtml = @"<html>
|
||||
<head>
|
||||
<title>LoadTest</title>
|
||||
<style>
|
||||
body { font-family: arial; }
|
||||
a,a:visited { color: blue; }
|
||||
h1 { margin: 0; padding: 0; font-size: 120%; font-weight: bold; }
|
||||
h1 a { text-decoration: none; }
|
||||
div.block { margin: 20px 0; }
|
||||
ul { margin:0; }
|
||||
div.ver { font-size: 80%; }
|
||||
div.head { padding:0 0 10px 0; margin: 0 0 20px 0; border-bottom: 1px solid #cccccc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""head"">
|
||||
<h1><a href=""/LoadTest"">LoadTest</a></h1>
|
||||
<div class=""ver"">" + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"</div>
|
||||
</div>
|
||||
";
|
||||
|
||||
private const string FootHtml = @"</body>
|
||||
</html>";
|
||||
|
||||
private static readonly string _containerTemplateText = @"
|
||||
@inherits Umbraco.Web.Mvc.UmbracoViewPage
|
||||
@{
|
||||
Layout = null;
|
||||
var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault();
|
||||
var contents = container.Children().ToArray();
|
||||
var groups = contents.GroupBy(x => x.Value<string>(""origin""));
|
||||
var id = contents.Length > 0 ? contents[0].Id : -1;
|
||||
var wurl = Request.QueryString[""u""] == ""1"";
|
||||
var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length;
|
||||
}
|
||||
" + HeadHtml + @"
|
||||
<div class=""block"">
|
||||
<span @Html.Raw(missing ? ""style=\""color:red;\"""" : """")>@contents.Length items</span>
|
||||
<ul>
|
||||
@foreach (var group in groups)
|
||||
{
|
||||
<li>@group.Key: @group.Count()</li>
|
||||
}
|
||||
</ul></div>
|
||||
<div class=""block"">
|
||||
@foreach (var content in contents)
|
||||
{
|
||||
while (content.Id > id)
|
||||
{
|
||||
<div style=""color:red;"">@id :: MISSING</div>
|
||||
id++;
|
||||
}
|
||||
if (wurl)
|
||||
{
|
||||
<div>@content.Id :: @content.Name :: @content.Url</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>@content.Id :: @content.Name</div>
|
||||
} id++;
|
||||
}
|
||||
</div>
|
||||
" + FootHtml;
|
||||
private readonly ServiceContext _serviceContext;
|
||||
|
||||
private ActionResult ContentHtml(string s)
|
||||
{
|
||||
return Content(HeadHtml + s + FootHtml);
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
var html = @"Welcome. You can:
|
||||
<ul>
|
||||
<li><a href=""/LoadTestContainer"">List existing contents</a> (u:url)</li>
|
||||
<li><a href=""/LoadTest/Create?o=browser"">Create a content</a> (o:origin, r:restart, n:number)</li>
|
||||
<li><a href=""/LoadTest/Clear"">Clear all contents</a></li>
|
||||
<li><a href=""/LoadTest/Domains"">List the current domains in w3wp.exe</a></li>
|
||||
<li><a href=""/LoadTest/Restart"">Restart the current AppDomain</a></li>
|
||||
<li><a href=""/LoadTest/Recycle"">Recycle the AppPool</a></li>
|
||||
<li><a href=""/LoadTest/Die"">Cause w3wp.exe to die</a></li>
|
||||
</ul>
|
||||
";
|
||||
|
||||
return ContentHtml(html);
|
||||
}
|
||||
|
||||
private ActionResult EnsureInitialize()
|
||||
{
|
||||
if (_containerId > 0) return null;
|
||||
|
||||
lock (_locko)
|
||||
{
|
||||
if (_containerId > 0) return null;
|
||||
|
||||
var contentTypeService = _serviceContext.ContentTypeService;
|
||||
var contentType = contentTypeService.Get(_contentAlias);
|
||||
if (contentType == null)
|
||||
return ContentHtml("Not installed, first you must <a href=\"/LoadTest/Install\">install</a>.");
|
||||
|
||||
var containerType = contentTypeService.Get(_containerAlias);
|
||||
if (containerType == null)
|
||||
return ContentHtml("Panic! Container type is missing.");
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
var container = contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault();
|
||||
if (container == null)
|
||||
return ContentHtml("Panic! Container is missing.");
|
||||
|
||||
_containerId = container.Id;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Install()
|
||||
{
|
||||
var dataTypeService = _serviceContext.DataTypeService;
|
||||
|
||||
//var dataType = dataTypeService.GetAll(Constants.DataTypes.DefaultContentListView);
|
||||
|
||||
|
||||
//if (!dict.ContainsKey("pageSize")) dict["pageSize"] = new PreValue("10");
|
||||
//dict["pageSize"].Value = "200";
|
||||
//dataTypeService.SavePreValues(dataType, dict);
|
||||
|
||||
var contentTypeService = _serviceContext.ContentTypeService;
|
||||
|
||||
var contentType = new ContentType(-1)
|
||||
{
|
||||
Alias = _contentAlias,
|
||||
Name = "LoadTest Content",
|
||||
Description = "Content for LoadTest",
|
||||
Icon = "icon-document"
|
||||
};
|
||||
var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId);
|
||||
contentType.AddPropertyType(new PropertyType(def)
|
||||
{
|
||||
Name = "Origin",
|
||||
Alias = "origin",
|
||||
Description = "The origin of the content.",
|
||||
});
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var containerTemplate = ImportTemplate(_serviceContext,
|
||||
"LoadTestContainer", "LoadTestContainer", _containerTemplateText);
|
||||
|
||||
var containerType = new ContentType(-1)
|
||||
{
|
||||
Alias = _containerAlias,
|
||||
Name = "LoadTest Container",
|
||||
Description = "Container for LoadTest content",
|
||||
Icon = "icon-document",
|
||||
AllowedAsRoot = true,
|
||||
IsContainer = true
|
||||
};
|
||||
containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[]
|
||||
{
|
||||
new ContentTypeSort(new Lazy<int>(() => contentType.Id), 0, contentType.Alias),
|
||||
});
|
||||
containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate });
|
||||
containerType.SetDefaultTemplate(containerTemplate);
|
||||
contentTypeService.Save(containerType);
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
var content = contentService.Create("LoadTestContainer", -1, _containerAlias);
|
||||
contentService.SaveAndPublish(content);
|
||||
|
||||
return ContentHtml("Installed.");
|
||||
}
|
||||
|
||||
public ActionResult Create(int n = 1, int r = 0, string o = null)
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
if (r < 0) r = 0;
|
||||
if (r > 100) r = 100;
|
||||
var restart = GetRandom(0, 100) > (100 - r);
|
||||
|
||||
var contentService = _serviceContext.ContentService;
|
||||
|
||||
if (n < 1) n = 1;
|
||||
if (n > _maxCreate) n = _maxCreate;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o;
|
||||
var content = contentService.Create(name, _containerId, _contentAlias);
|
||||
content.SetValue("origin", o);
|
||||
contentService.SaveAndPublish(content);
|
||||
}
|
||||
|
||||
if (restart)
|
||||
DoRestart();
|
||||
|
||||
return ContentHtml("Created " + n + " content"
|
||||
+ (restart ? ", and restarted" : "")
|
||||
+ ".");
|
||||
}
|
||||
|
||||
private int GetRandom(int minValue, int maxValue)
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
return _random.Next(minValue, maxValue);
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Clear()
|
||||
{
|
||||
var res = EnsureInitialize();
|
||||
if (res != null) return res;
|
||||
|
||||
var contentType = _serviceContext.ContentTypeService.Get(_contentAlias);
|
||||
_serviceContext.ContentService.DeleteOfType(contentType.Id);
|
||||
|
||||
return ContentHtml("Cleared.");
|
||||
}
|
||||
|
||||
private void DoRestart()
|
||||
{
|
||||
HttpContext.User = null;
|
||||
System.Web.HttpContext.Current.User = null;
|
||||
Thread.CurrentPrincipal = null;
|
||||
HttpRuntime.UnloadAppDomain();
|
||||
}
|
||||
|
||||
public ActionResult Restart()
|
||||
{
|
||||
DoRestart();
|
||||
|
||||
return ContentHtml("Restarted.");
|
||||
}
|
||||
|
||||
public ActionResult Die()
|
||||
{
|
||||
var timer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
throw new Exception("die!");
|
||||
});
|
||||
timer.Change(100, 0);
|
||||
|
||||
return ContentHtml("Dying.");
|
||||
}
|
||||
|
||||
public ActionResult Domains()
|
||||
{
|
||||
var currentDomain = AppDomain.CurrentDomain;
|
||||
var currentName = currentDomain.FriendlyName;
|
||||
var pos = currentName.IndexOf('-');
|
||||
if (pos > 0) currentName = currentName.Substring(0, pos);
|
||||
|
||||
var text = new System.Text.StringBuilder();
|
||||
text.Append("<div class=\"block\">Process ID: " + Process.GetCurrentProcess().Id + "</div>");
|
||||
text.Append("<div class=\"block\">");
|
||||
text.Append("<div>IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "</div>");
|
||||
text.Append("<div>App ID: " + currentName + "</div>");
|
||||
//text.Append("<div>AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "</div>");
|
||||
text.Append("</div>");
|
||||
|
||||
text.Append("<div class=\"block\">Domains:<ul>");
|
||||
text.Append("<li>Not implemented.</li>");
|
||||
/*
|
||||
foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id))
|
||||
{
|
||||
var name = domain.FriendlyName;
|
||||
pos = name.IndexOf('-');
|
||||
if (pos > 0) name = name.Substring(0, pos);
|
||||
text.Append("<li style=\""
|
||||
+ (name != currentName ? "color: #cccccc;" : "")
|
||||
//+ (domain.Id == currentDomain.Id ? "" : "")
|
||||
+ "\">"
|
||||
+"[" + domain.Id + "] " + name
|
||||
+ (domain.IsDefaultAppDomain() ? " (default)" : "")
|
||||
+ (domain.Id == currentDomain.Id ? " (current)" : "")
|
||||
+ "</li>");
|
||||
}
|
||||
*/
|
||||
text.Append("</ul></div>");
|
||||
|
||||
return ContentHtml(text.ToString());
|
||||
}
|
||||
|
||||
public ActionResult Recycle()
|
||||
{
|
||||
return ContentHtml("Not implemented—please use IIS console.");
|
||||
}
|
||||
|
||||
private static Template ImportTemplate(ServiceContext svces, string name, string alias, string text, ITemplate master = null)
|
||||
{
|
||||
var t = new Template(name, alias) { Content = text };
|
||||
if (master != null)
|
||||
t.SetMasterTemplate(master);
|
||||
svces.FileService.SaveTemplate(t);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestComponent : IComponent
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
|
||||
return;
|
||||
|
||||
RouteTable.Routes.MapRoute(
|
||||
name: "LoadTest",
|
||||
url: "LoadTest/{action}",
|
||||
defaults: new
|
||||
{
|
||||
controller = "LoadTest",
|
||||
action = "Index"
|
||||
},
|
||||
namespaces: new[] { "Umbraco.TestData" }
|
||||
);
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class TestComposer : ComponentComposer<TestComponent>, IUserComposer
|
||||
{
|
||||
public override void Compose(Composition composition)
|
||||
{
|
||||
base.Compose(composition);
|
||||
|
||||
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
|
||||
return;
|
||||
|
||||
composition.Register(typeof(LoadTestController), Lifetime.Request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="LoadTestController.cs" />
|
||||
<Compile Include="SegmentTestController.cs" />
|
||||
<Compile Include="UmbracoTestDataController.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
||||
@@ -11,7 +11,7 @@ You can easily add you own tours to the Help-drawer or show and start tours from
|
||||
anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install <a href="https://our.umbraco.com/projects/starter-kits/the-starter-kit/">The Starter Kit</a> in Umbraco 7.8
|
||||
|
||||
<h1><b>Extending the help drawer with custom tours</b></h1>
|
||||
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
|
||||
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
|
||||
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
|
||||
picked up by Umbraco and shown in the Help-drawer.
|
||||
|
||||
@@ -277,7 +277,6 @@ In the following example you see how to run some custom logic before a step goes
|
||||
}
|
||||
|
||||
startStep();
|
||||
// tour completed - final step
|
||||
} else {
|
||||
// tour completed - final step
|
||||
scope.loadingStep = true;
|
||||
|
||||
@@ -129,6 +129,21 @@ function clipboardService(notificationsService, eventsService, localStorageServi
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.services.clipboardService#registrerPropertyClearingResolver
|
||||
* @methodOf umbraco.services.clipboardService
|
||||
*
|
||||
* @param {string} function A method executed for every property and inner properties copied.
|
||||
*
|
||||
* @description
|
||||
* Executed for all properties including inner properties when performing a copy action.
|
||||
*/
|
||||
service.registrerClearPropertyResolver = function(resolver) {
|
||||
clearPropertyResolvers.push(resolver);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.services.clipboardService#copy
|
||||
|
||||
@@ -126,6 +126,10 @@
|
||||
<Project>{52ac0ba8-a60e-4e36-897b-e8b97a54ed1c}</Project>
|
||||
<Name>Umbraco.ModelsBuilder.Embedded</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Umbraco.TestData\Umbraco.TestData.csproj">
|
||||
<Project>{fb5676ed-7a69-492c-b802-e7b24144c0fc}</Project>
|
||||
<Name>Umbraco.TestData</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
|
||||
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
|
||||
<Name>Umbraco.Web</Name>
|
||||
@@ -349,6 +353,9 @@
|
||||
<DevelopmentServerPort>8700</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8700</IISUrl>
|
||||
<DevelopmentServerPort>8640</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8640</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>False</UseCustomServer>
|
||||
<CustomServerUrl>
|
||||
@@ -430,4 +437,4 @@
|
||||
<Message Text="ConfigFile: $(OriginalFileName) -> $(OutputFileName)" Importance="high" Condition="Exists('$(ModifiedFileName)')" />
|
||||
<Copy SourceFiles="$(ModifiedFileName)" DestinationFiles="$(OutputFileName)" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" Condition="Exists('$(ModifiedFileName)')" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -218,7 +218,7 @@ namespace Umbraco.Web.Compose
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error<InstructionProcessTask>("Failed (will repeat).", e);
|
||||
_logger.Error<InstructionProcessTask>(e, "Failed (will repeat).");
|
||||
}
|
||||
return true; // repeat
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Sync;
|
||||
using Umbraco.Web.HealthCheck;
|
||||
|
||||
@@ -15,16 +16,17 @@ namespace Umbraco.Web.Scheduling
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly HealthCheckCollection _healthChecks;
|
||||
private readonly HealthCheckNotificationMethodCollection _notifications;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IProfilingLogger _logger;
|
||||
|
||||
public HealthCheckNotifier(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
|
||||
HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications,
|
||||
IRuntimeState runtimeState,
|
||||
IProfilingLogger logger)
|
||||
IScopeProvider scopeProvider, IRuntimeState runtimeState, IProfilingLogger logger)
|
||||
: base(runner, delayMilliseconds, periodMilliseconds)
|
||||
{
|
||||
_healthChecks = healthChecks;
|
||||
_notifications = notifications;
|
||||
_scopeProvider = scopeProvider;
|
||||
_runtimeState = runtimeState;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -51,6 +53,10 @@ namespace Umbraco.Web.Scheduling
|
||||
return false; // do NOT repeat, going down
|
||||
}
|
||||
|
||||
// Ensure we use an explicit scope since we are running on a background thread and plugin health
|
||||
// checks can be making service/database calls so we want to ensure the CallContext/Ambient scope
|
||||
// isn't used since that can be problematic.
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
using (_logger.DebugDuration<HealthCheckNotifier>("Health checks executing", "Health checks complete"))
|
||||
{
|
||||
var healthCheckConfig = Current.Configs.HealthChecks();
|
||||
|
||||
@@ -70,8 +70,7 @@ namespace Umbraco.Web.Scheduling
|
||||
return false; // do NOT repeat, going down
|
||||
}
|
||||
|
||||
// running on a background task, and Log.CleanLogs uses the old SqlHelper,
|
||||
// better wrap in a scope and ensure it's all cleaned up and nothing leaks
|
||||
// Ensure we use an explicit scope since we are running on a background thread.
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
using (_logger.DebugDuration<LogScrubber>("Log scrubbing executing", "Log scrubbing complete"))
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Sync;
|
||||
|
||||
@@ -55,30 +56,24 @@ namespace Umbraco.Web.Scheduling
|
||||
|
||||
try
|
||||
{
|
||||
// ensure we run with an UmbracoContext, because this may run in a background task,
|
||||
// yet developers may be using the 'current' UmbracoContext in the event handlers
|
||||
//
|
||||
// TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events
|
||||
// - UmbracoContext 'current' needs to be refactored and cleaned up
|
||||
// - batched messenger should not depend on a current HttpContext
|
||||
// but then what should be its "scope"? could we attach it to scopes?
|
||||
// - and we should definitively *not* have to flush it here (should be auto)
|
||||
//
|
||||
using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext())
|
||||
// We don't need an explicit scope here because PerformScheduledPublish creates it's own scope
|
||||
// so it's safe as it will create it's own ambient scope.
|
||||
// Ensure we run with an UmbracoContext, because this will run in a background task,
|
||||
// and developers may be using the UmbracoContext in the event handlers.
|
||||
|
||||
using var contextReference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
// run
|
||||
var result = _contentService.PerformScheduledPublish(DateTime.Now);
|
||||
foreach (var grouped in result.GroupBy(x => x.Result))
|
||||
_logger.Info<ScheduledPublishing>("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// if running on a temp context, we have to flush the messenger
|
||||
if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m)
|
||||
m.FlushBatch();
|
||||
}
|
||||
// run
|
||||
var result = _contentService.PerformScheduledPublish(DateTime.Now);
|
||||
foreach (var grouped in result.GroupBy(x => x.Result))
|
||||
_logger.Info<ScheduledPublishing>("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// if running on a temp context, we have to flush the messenger
|
||||
if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m)
|
||||
m.FlushBatch();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace Umbraco.Web.Scheduling
|
||||
}
|
||||
|
||||
var periodInMilliseconds = healthCheckConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000;
|
||||
var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _runtime, logger);
|
||||
var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _scopeProvider, _runtime, logger);
|
||||
_healthCheckRunner.TryAdd(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user