diff --git a/src/OneScript.Core/Execution/BslProcessExtensions.cs b/src/OneScript.Core/Execution/BslProcessExtensions.cs new file mode 100644 index 000000000..debc45c9b --- /dev/null +++ b/src/OneScript.Core/Execution/BslProcessExtensions.cs @@ -0,0 +1,25 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; + +namespace OneScript.Execution +{ + public static class BslProcessExtensions + { + /// + /// Исключение прерывает процесс по запросу отмены. + /// Такое исключение не должно перехватываться bsl-кодом и превращаться в ScriptException. + /// + public static bool IsCancellationOf(this Exception exception, IBslProcess process) + { + return exception is OperationCanceledException canceled + && canceled.CancellationToken.IsCancellationRequested + && canceled.CancellationToken == process.CancellationToken; + } + } +} diff --git a/src/OneScript.Core/Execution/IBslProcess.cs b/src/OneScript.Core/Execution/IBslProcess.cs index 297f11f63..d8eb87f13 100644 --- a/src/OneScript.Core/Execution/IBslProcess.cs +++ b/src/OneScript.Core/Execution/IBslProcess.cs @@ -8,6 +8,7 @@ This Source Code Form is subject to the terms of the #nullable enable using System; +using System.Threading; using OneScript.Contexts; using OneScript.DependencyInjection; using OneScript.Values; @@ -40,6 +41,12 @@ public interface IBslProcess public int VirtualThreadId { get; } + /// + /// Токен отмены исполнения. При запросе отмены исполнитель прерывает bsl-код + /// исключением с этим токеном. + /// + public CancellationToken CancellationToken => CancellationToken.None; + /** * Обертка текущего процесса в терминах BSL. * Может отсутствовать, полностью определяется хост-приложением процесса. diff --git a/src/OneScript.Core/Execution/IBslProcessFactory.cs b/src/OneScript.Core/Execution/IBslProcessFactory.cs index e2679a94f..58226f6c8 100644 --- a/src/OneScript.Core/Execution/IBslProcessFactory.cs +++ b/src/OneScript.Core/Execution/IBslProcessFactory.cs @@ -5,6 +5,8 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ +using System.Threading; + namespace OneScript.Execution { /// @@ -14,5 +16,9 @@ public interface IBslProcessFactory { // Создать новый bsl-процесс с пустым стеком вызовов IBslProcess NewProcess(); + + // Создать новый bsl-процесс, исполнение которого можно отменить через токен. + // Фабрики, которые не поддерживают отмену, создают обычный процесс + IBslProcess NewProcess(CancellationToken cancellationToken) => NewProcess(); } } \ No newline at end of file diff --git a/src/OneScript.Native/Compiler/ExpressionHelpers.cs b/src/OneScript.Native/Compiler/ExpressionHelpers.cs index 8ed0f479e..e686b5dd0 100644 --- a/src/OneScript.Native/Compiler/ExpressionHelpers.cs +++ b/src/OneScript.Native/Compiler/ExpressionHelpers.cs @@ -12,8 +12,10 @@ This Source Code Form is subject to the terms of the using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading; using Microsoft.CSharp.RuntimeBinder; using OneScript.Contexts; +using OneScript.Execution; using OneScript.Language.LexicalAnalysis; using OneScript.Localization; using OneScript.Native.Runtime; @@ -679,6 +681,25 @@ public static Expression InvokeBslNativeMethod(BslNativeMethodInfo nativeMethod, PackArgsToArgsArray(args)); } + public static Expression GetCancellationToken(ParameterExpression process) + { + return Expression.Property(process, nameof(IBslProcess.CancellationToken)); + } + + public static Expression ThrowIfCancellationRequested(Expression cancellationToken) + { + return Expression.Call(cancellationToken, nameof(CancellationToken.ThrowIfCancellationRequested), Type.EmptyTypes); + } + + public static Expression IsCancellationOf(Expression exception, ParameterExpression process) + { + var method = OperationsCache.GetOrAdd( + typeof(BslProcessExtensions), + nameof(BslProcessExtensions.IsCancellationOf)); + + return Expression.Call(method, exception, process); + } + private static Expression PackArgsToArgsArray(IEnumerable args) { return Expression.NewArrayInit(typeof(BslValue), args.Select(ConvertToBslValue)); diff --git a/src/OneScript.Native/Compiler/MethodCompiler.cs b/src/OneScript.Native/Compiler/MethodCompiler.cs index a2be74b8c..636ef3ba7 100644 --- a/src/OneScript.Native/Compiler/MethodCompiler.cs +++ b/src/OneScript.Native/Compiler/MethodCompiler.cs @@ -13,6 +13,7 @@ This Source Code Form is subject to the terms of the using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Threading; using Microsoft.CSharp.RuntimeBinder; using OneScript.Commons; using OneScript.Compilation.Binding; @@ -824,7 +825,9 @@ protected override void VisitWhileNode(WhileLoopNode node) var block = _blocks.LeaveBlock(); + var cancellationToken = Expression.Variable(typeof(CancellationToken)); var result = new List(); + result.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken)); result.Add(Expression.IfThen( Expression.Not(block.BuildStack.Pop()), Expression.Break(block.LoopBreak))); @@ -832,7 +835,13 @@ protected override void VisitWhileNode(WhileLoopNode node) result.AddRange(block.GetStatements()); var loop = Expression.Loop(Expression.Block(result), block.LoopBreak, block.LoopContinue); - _blocks.Add(loop); + _blocks.Add(Expression.Block(new[] {cancellationToken}, new Expression[] {ReadCancellationToken(cancellationToken), loop})); + } + + // Токен читается один раз перед циклом, а не у процесса на каждой итерации + private BinaryExpression ReadCancellationToken(ParameterExpression cancellationToken) + { + return Expression.Assign(cancellationToken, ExpressionHelpers.GetCancellationToken(_processParameter)); } protected override void VisitWhileCondition(BslSyntaxNode node) @@ -871,12 +880,15 @@ protected override void VisitForLoopNode(ForLoopNode node) var initialValue = block.BuildStack.Pop(); var counterVar = block.BuildStack.Pop(); + var cancellationToken = Expression.Variable(typeof(CancellationToken)); var result = new List(); + result.Add(ReadCancellationToken(cancellationToken)); result.Add(Expression.Assign(counterVar, ExpressionHelpers.CreateAssignmentSource(initialValue, counterVar.Type))); var finalVar = Expression.Variable(typeof(decimal)); // TODO: BslNumericValue ? result.Add(Expression.Assign(finalVar, upperLimit)); var loop = new List(); + loop.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken)); loop.Add(Expression.IfThen( Expression.GreaterThan(ExpressionHelpers.ToNumber(counterVar), finalVar), Expression.Break(block.LoopBreak))); @@ -888,7 +900,7 @@ protected override void VisitForLoopNode(ForLoopNode node) result.Add(Expression.Loop(Expression.Block(loop), block.LoopBreak)); - _blocks.Add(Expression.Block(new[] {finalVar}, result)); + _blocks.Add(Expression.Block(new[] {finalVar, cancellationToken}, result)); } protected override void VisitForInitializer(BslSyntaxNode node) @@ -950,7 +962,9 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) var getEnumeratorInvoke = Expression.Call(collectionCast, getEnumeratorMethod); var enumeratorVar = Expression.Variable(typeof(IEnumerator)); + var cancellationToken = Expression.Variable(typeof(CancellationToken)); var result = new List(); + result.Add(ReadCancellationToken(cancellationToken)); result.Add(Expression.Assign(enumeratorVar, getEnumeratorInvoke)); @@ -963,6 +977,7 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) typeof(BslValue)) ); + loop.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken)); loop.Add(assignCurrent); loop.AddRange(block.GetStatements()); @@ -975,7 +990,7 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node) result.Add(finalLoop); - _blocks.Add(Expression.Block(new[] {enumeratorVar}, result)); + _blocks.Add(Expression.Block(new[] {enumeratorVar, cancellationToken}, result)); } protected override void VisitIteratorLoopVariable(TerminalNode node) @@ -1012,8 +1027,12 @@ protected override void VisitTryExceptNode(TryExceptNode node) var except = block.BuildStack.Pop(); var tryBlock = block.BuildStack.Pop(); + // Отмена процесса не должна перехватываться Попыткой + var notCancellation = Expression.Not( + ExpressionHelpers.IsCancellationOf(block.CurrentException, _processParameter)); + _blocks.Add(Expression.TryCatch(tryBlock, - Expression.Catch(block.CurrentException, except)) + Expression.Catch(block.CurrentException, except, notCancellation)) ); } diff --git a/src/OneScript.Native/Runtime/CallableMethod.cs b/src/OneScript.Native/Runtime/CallableMethod.cs index f61237aad..3d77aeb12 100644 --- a/src/OneScript.Native/Runtime/CallableMethod.cs +++ b/src/OneScript.Native/Runtime/CallableMethod.cs @@ -43,6 +43,9 @@ public BslValue Invoke(IBslProcess process, object target, BslValue[] args) throw new InvalidOperationException($"Method {_method} was not compiled"); } + // Проверка на входе в метод прерывает и рекурсию без циклов + process.CancellationToken.ThrowIfCancellationRequested(); + var callableWrapper = GetCallableWrapper(target); return _delegate.Invoke(callableWrapper, args, process); } diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index 26fc2f349..f694a29e9 100644 --- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs +++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs @@ -99,12 +99,22 @@ public ExecutionThreadContext CurrentThread(IBslProcess process) /// /// Приостанавливает выполнение скрипта. + /// Ожидание прерывается, если фоновое задание, в котором оно выполняется, отменено. /// + /// Текущий процесс, в котором вызван данный метод /// Время приостановки в миллисекундах [ContextMethod("Приостановить", "Sleep")] - public void Sleep(int delay) + public void Sleep(IBslProcess process, int delay) { - System.Threading.Thread.Sleep(delay); + var cancellationToken = process.CancellationToken; + + // WaitHandle дороже Thread.Sleep и создает событие ядра, а Sleep(0) еще и уступает квант времени + if (delay == 0 || !cancellationToken.CanBeCanceled) + System.Threading.Thread.Sleep(delay); + else + cancellationToken.WaitHandle.WaitOne(delay); + + cancellationToken.ThrowIfCancellationRequested(); } [ContextMethod("КраткоеПредставлениеОшибки", "BriefErrorDescription")] diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs index d3dd251e3..47e554530 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using OneScript.Commons; using OneScript.Contexts; @@ -24,6 +25,9 @@ public class BackgroundTask : AutoContext { private readonly BslMethodInfo _method; private readonly int _methIndex; + private readonly CancellationTokenSource _cancellation = new CancellationTokenSource(); + private readonly object _cancellationLock = new object(); + private bool _cancellationReleased; private Task _workerTask; private int _taskId; @@ -52,6 +56,24 @@ public Task WorkerTask public int TaskId => _taskId; + /// + /// Задание, из которого запущено это, или null, если запущено не из фонового задания + /// + internal BackgroundTask Parent { get; set; } + + internal bool IsDescendantOf(BackgroundTask ancestor) + { + for (var task = Parent; task != null; task = task.Parent) + { + if (ReferenceEquals(task, ancestor)) + return true; + } + + return false; + } + + public CancellationToken CancellationToken => _cancellation.Token; + [ContextProperty("УникальныйИдентификатор","UUID")] public GuidWrapper Identifier { get; private set; } @@ -76,21 +98,69 @@ public Task WorkerTask /// /// Ждать завершения задания указанное число миллисекунд /// + /// Текущий процесс, в котором вызван данный метод /// Таймаут в миллисекундах. Если ноль - ждать вечно /// Истина - дождались завершения. Ложь - сработал таймаут [ContextMethod("ОжидатьЗавершения", "Wait")] - public bool Wait(int timeout = 0) + public bool Wait(IBslProcess process, int timeout = 0) { timeout = BackgroundTasksManager.ConvertTimeout(timeout); - return WorkerTask.Wait(timeout); + return WorkerTask.Wait(timeout, process.CancellationToken); } + /// + /// Отменяет выполнение задания. Код задания прерывается перед выполнением очередной строки + /// или во время Приостановить(), перехватить отмену через Попытка нельзя. + /// Обработчики завершения потока исполнения при этом отрабатывают. + /// Метод не дожидается остановки задания, для этого используйте ОжидатьЗавершения(). + /// Отмена завершенного задания ничего не делает. + /// + [ContextMethod("Отменить", "Cancel")] + public void Cancel() + { + lock (_cancellationLock) + { + // Источник отмены освобождается, когда задание завершилось + if (!_cancellationReleased) + _cancellation.Cancel(); + } + } + public void ExecuteOnCurrentThread(IBslProcess process) + { + try + { + Execute(process); + } + finally + { + ReleaseCancellation(); + } + } + + // Ожидание в Приостановить() создает у источника отмены событие ядра, + // а задание остается в списке менеджера до Очистить() + private void ReleaseCancellation() + { + lock (_cancellationLock) + { + _cancellationReleased = true; + _cancellation.Dispose(); + } + } + + private void Execute(IBslProcess process) { if (State != TaskStateEnum.NotRunned) throw new RuntimeException(Locale.NStr("ru = 'Неверное состояние задачи';en = 'Incorrect task status'")); + if (_cancellation.IsCancellationRequested) + { + State = TaskStateEnum.Canceled; + return; + } + var parameters = Parameters is ArrayImpl array ? array.ToArray() : Array.Empty(); @@ -117,6 +187,17 @@ public void ExecuteOnCurrentThread(IBslProcess process) ExceptionInfo = new ExceptionInfoContext(exception); } + catch (OperationCanceledException exception) when (exception.CancellationToken == _cancellation.Token) + { + State = TaskStateEnum.Canceled; + } + catch (Exception exception) + { + // Метод встроенного объекта бросает исключения .NET, а не ScriptException: + // без этого задание осталось бы «Активно» без информации об ошибке + State = TaskStateEnum.CompletedWithErrors; + ExceptionInfo = new ExceptionInfoContext(new ExternalSystemException(exception)); + } } } } diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs index c3d253ce2..4a65990f6 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs @@ -46,12 +46,17 @@ public BackgroundTasksManager(ExecutionContext runtimeContext) [ContextMethod("Выполнить", "Execute")] public BackgroundTask Execute(IRuntimeContextInstance target, string methodName, ArrayImpl parameters = null, bool longRunning = false) { - var task = new BackgroundTask(target, methodName, parameters); + var task = new BackgroundTask(target, methodName, parameters) + { + Parent = FindCurrentTask() + }; - var taskCreationOptions = longRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None; + // PreferFairness - в общую очередь пула: иначе ожидающий из потока пула (запрос веб-сервера) + // может выполнить задание прямо в своем потоке, и два процесса поделят блокировки потока + var taskCreationOptions = longRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.PreferFairness; var worker = new Task(() => { - var process = _runtimeContext.Services.Resolve().NewProcess(); + var process = _runtimeContext.Services.Resolve().NewProcess(task.CancellationToken); task.ExecuteOnCurrentThread(process); }, taskCreationOptions); @@ -72,11 +77,12 @@ public void Clear() /// /// Ожидает завершения всех переданных заданий /// + /// Текущий процесс, в котором вызван данный метод /// Массив заданий /// Таймаут ожидания. 0 = ожидать бесконечно /// Истина - дождались все задания, Ложь - истек таймаут [ContextMethod("ОжидатьВсе", "WaitAll")] - public bool WaitAll(ArrayImpl tasks, int timeout = 0) + public bool WaitAll(IBslProcess process, ArrayImpl tasks, int timeout = 0) { var workers = GetWorkerTasks(tasks); timeout = ConvertTimeout(timeout); @@ -84,17 +90,18 @@ public bool WaitAll(ArrayImpl tasks, int timeout = 0) // Фоновые задания перехватывают исключения внутри себя // и выставляют свойство ИнформацияОбОшибке // если WaitAll выбросит исключение, значит действительно что-то пошло не так на уровне самого Task - return Task.WaitAll(workers, timeout); + return Task.WaitAll(workers, timeout, process.CancellationToken); } /// /// Ожидать хотя бы одно из переданных заданий. /// + /// Текущий процесс, в котором вызван данный метод /// Массив заданий /// Таймаут ожидания. 0 = ожидать бесконечно /// Число. Индекс в массиве заданий, указывающий на элемент-задание, которое завершилось. -1 = сработал таймаут [ContextMethod("ОжидатьЛюбое", "WaitAny")] - public int WaitAny(ArrayImpl tasks, int timeout = 0) + public int WaitAny(IBslProcess process, ArrayImpl tasks, int timeout = 0) { var workers = GetWorkerTasks(tasks); timeout = ConvertTimeout(timeout); @@ -102,19 +109,22 @@ public int WaitAny(ArrayImpl tasks, int timeout = 0) // Фоновые задания перехватывают исключения внутри себя // и выставляют свойство ИнформацияОбОшибке // если WaitAny выбросит исключение, значит действительно что-то пошло не так на уровне самого Task - return Task.WaitAny(workers, timeout); + return Task.WaitAny(workers, timeout, process.CancellationToken); } /// /// Блокирует поток до завершения всех заданий. + /// Вызванный из фонового задания, ждет только задания, запущенные из него (и из них дальше). /// Выбрасывает исключение, если какие-то задания завершились аварийно. /// Выброшенное исключение в свойстве Параметры содержит массив аварийных заданий. /// [ContextMethod("ОжидатьЗавершенияЗадач", "WaitCompletionOfTasks")] - public void WaitCompletionOfTasks() + public void WaitCompletionOfTasks(IBslProcess process) { - var snapshot = _tasks.Values.ToArray(); - Task.WaitAll(GetWorkerTasks(snapshot)); + // Родитель и соседние задания могут сами ждать текущее - их ждать нельзя + var current = FindCurrentTask(); + var snapshot = _tasks.Values.Where(x => current == null || x.IsDescendantOf(current)).ToArray(); + Task.WaitAll(GetWorkerTasks(snapshot), process.CancellationToken); var failedTasks = snapshot.Where(x => x.State == TaskStateEnum.CompletedWithErrors) .ToList(); @@ -181,15 +191,21 @@ public ArrayImpl GetBackgroundJobs(StructureImpl filter = default) [ContextMethod("ПолучитьТекущее", "GetCurrent")] public IValue GetCurrent() + { + return (IValue)FindCurrentTask() ?? ValueFactory.Create(); + } + + // Фоновое задание, в котором выполняется код, или null + private BackgroundTask FindCurrentTask() { var currentId = Task.CurrentId; if (currentId == null) - return ValueFactory.Create(); + return null; if (_tasks.TryGetValue(currentId.Value, out var task) && task.State == TaskStateEnum.Running) return task; - return ValueFactory.Create(); + return null; } internal static int ConvertTimeout(int timeout) diff --git a/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs b/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs index 970dd14e6..18448349f 100644 --- a/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs +++ b/src/OneScript.StandardLibrary/Tasks/TaskStateEnum.cs @@ -19,6 +19,8 @@ public enum TaskStateEnum [EnumValue("Завершено", "Completed")] Completed, [EnumValue("ЗавершеноАварийно", "Failed")] - CompletedWithErrors + CompletedWithErrors, + [EnumValue("Отменено", "Canceled")] + Canceled } } \ No newline at end of file diff --git a/src/ScriptEngine/BslProcess.cs b/src/ScriptEngine/BslProcess.cs index 4d7deeb06..8b735ba09 100644 --- a/src/ScriptEngine/BslProcess.cs +++ b/src/ScriptEngine/BslProcess.cs @@ -8,11 +8,14 @@ This Source Code Form is subject to the terms of the using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using OneScript.Contexts; using OneScript.DependencyInjection; using OneScript.Execution; using OneScript.Values; using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using ExecutionContext = ScriptEngine.Machine.ExecutionContext; namespace ScriptEngine { @@ -25,9 +28,13 @@ internal class BslProcess : IBslProcess private bool _isRunning; private bool _disposed; + private CancellationToken _cancellationToken; + private ProcessResourceLocks _resourceLocks; - public BslProcess(int id, ExecutionContext context, IEnumerable executorProviders) + public BslProcess(int id, ExecutionContext context, IEnumerable executorProviders, + CancellationToken cancellationToken) { + _cancellationToken = cancellationToken; _executorProviders = executorProviders.ToArray(); _bslExecutorsByModule = _executorProviders.ToDictionary(item => item.SupportedModuleType, item => item.GetInvokeDelegate()); @@ -40,6 +47,10 @@ public BslProcess(int id, ExecutionContext context, IEnumerable _cancellationToken; + + internal ProcessResourceLocks ResourceLocks => _resourceLocks ??= new ProcessResourceLocks(); + public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMethodInfo method, IValue[] arguments) { var notifyExecutors = !_isRunning; @@ -58,12 +69,22 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe { if (notifyExecutors) { - RaiseTerminationEvent(); - if (BslWrapper is IDisposable disposable) + // Обработчики завершения должны отработать и у отмененного процесса + _cancellationToken = CancellationToken.None; + try { - disposable.Dispose(); + RaiseTerminationEvent(); + if (BslWrapper is IDisposable disposable) + { + disposable.Dispose(); + } } - + finally + { + // Монитор привязан к потоку: не отпущенные процессом блокировки больше никто не освободит + _resourceLocks?.ReleaseAll(); + } + Array.ForEach(_executorProviders, e => e.AfterProcessExit(this)); Services.Dispose(); _isRunning = false; diff --git a/src/ScriptEngine/BslProcessFactory.cs b/src/ScriptEngine/BslProcessFactory.cs index 8d063ab74..39005e5d6 100644 --- a/src/ScriptEngine/BslProcessFactory.cs +++ b/src/ScriptEngine/BslProcessFactory.cs @@ -25,13 +25,15 @@ public BslProcessFactory(IServiceContainer services) _services = services; } - public IBslProcess NewProcess() + public IBslProcess NewProcess() => NewProcess(CancellationToken.None); + + public IBslProcess NewProcess(CancellationToken cancellationToken) { // Создаем новый контекст со всеми зависимостями var context = _services.Resolve(); var executors = _services.ResolveEnumerable(); - return new BslProcess(Interlocked.Increment(ref _threadIdCounter), context, executors); + return new BslProcess(Interlocked.Increment(ref _threadIdCounter), context, executors, cancellationToken); } } } \ No newline at end of file diff --git a/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs b/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs index c1b84fb04..6b161b69b 100644 --- a/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs +++ b/src/ScriptEngine/Machine/Contexts/CriticalSectionContext.cs @@ -6,8 +6,11 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; +using System.Diagnostics; using System.Threading; using OneScript.Contexts; +using OneScript.Exceptions; +using OneScript.Execution; namespace ScriptEngine.Machine.Contexts { @@ -17,6 +20,9 @@ namespace ScriptEngine.Machine.Contexts [ContextClass("БлокировкаРесурса", "ResourceLock")] public class CriticalSectionContext : AutoContext, IDisposable { + // Monitor не принимает токен отмены, поэтому ожидание блокировки идет порциями + private const int CancellationPollInterval = 50; + private object _lockObject; private CriticalSectionContext() @@ -29,10 +35,50 @@ private CriticalSectionContext(object lockObject) _lockObject = lockObject; } + /// + /// Захватывает блокировку, ожидая ее освобождения другими потоками. + /// Ожидание прерывается, если фоновое задание, в котором оно выполняется, отменено. + /// Блокировки, не отпущенные до завершения задания, освобождаются автоматически. + /// + /// Текущий процесс, в котором вызван данный метод + /// Таймаут ожидания в миллисекундах. 0 - ждать бесконечно + /// Истина - блокировка захвачена, Ложь - истек таймаут [ContextMethod("Заблокировать", "Lock")] - public void Lock() + public bool Lock(IBslProcess process, int timeout = 0) + { + if (timeout < 0) + throw RuntimeException.InvalidArgumentValue(); + + var entered = TryEnter(timeout == 0 ? Timeout.Infinite : timeout, process.CancellationToken); + if (entered) + (process as BslProcess)?.ResourceLocks.Entered(_lockObject); + + return entered; + } + + private bool TryEnter(int timeout, CancellationToken cancellationToken) { - Monitor.Enter(_lockObject); + if (!cancellationToken.CanBeCanceled) + return Monitor.TryEnter(_lockObject, timeout); + + if (Monitor.TryEnter(_lockObject)) + return true; + + var start = Stopwatch.GetTimestamp(); + while (true) + { + var wait = timeout == Timeout.Infinite + ? CancellationPollInterval + : (int)Math.Clamp(timeout - Stopwatch.GetElapsedTime(start).TotalMilliseconds, 0, CancellationPollInterval); + + if (Monitor.TryEnter(_lockObject, wait)) + return true; + + cancellationToken.ThrowIfCancellationRequested(); + + if (timeout != Timeout.Infinite && Stopwatch.GetElapsedTime(start).TotalMilliseconds >= timeout) + return false; + } } [ContextMethod("Разблокировать", "Unlock")] diff --git a/src/ScriptEngine/Machine/Contexts/ProcessResourceLocks.cs b/src/ScriptEngine/Machine/Contexts/ProcessResourceLocks.cs new file mode 100644 index 000000000..8063f2de5 --- /dev/null +++ b/src/ScriptEngine/Machine/Contexts/ProcessResourceLocks.cs @@ -0,0 +1,50 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Collections.Generic; +using System.Threading; + +namespace ScriptEngine.Machine.Contexts +{ + /// + /// Блокировки ресурсов, которые захватывал bsl-процесс. + /// Монитор привязан к потоку: если процесс завершился, не отпустив блокировку + /// (например, фоновое задание отменили), освободить ее из другого потока уже нельзя. + /// Процесс отпускает такие блокировки при завершении, в своем же потоке. + /// Сколько раз блокировка захвачена и отпущена ли она, знает сам монитор, + /// поэтому здесь хранятся только объекты блокировок. + /// + internal class ProcessResourceLocks + { + private readonly List _entries = new List(); + + public void Entered(object lockObject) + { + // Уже отпущенные блокировки не нужны, иначе список рос бы на каждой новой блокировке + _entries.RemoveAll(entry => !Monitor.IsEntered(entry)); + + foreach (var entry in _entries) + { + if (ReferenceEquals(entry, lockObject)) + return; + } + + _entries.Add(lockObject); + } + + public void ReleaseAll() + { + foreach (var lockObject in _entries) + { + while (Monitor.IsEntered(lockObject)) + Monitor.Exit(lockObject); + } + + _entries.Clear(); + } + } +} diff --git a/src/ScriptEngine/Machine/MachineInstance.cs b/src/ScriptEngine/Machine/MachineInstance.cs index 5471c9487..654711d5c 100644 --- a/src/ScriptEngine/Machine/MachineInstance.cs +++ b/src/ScriptEngine/Machine/MachineInstance.cs @@ -490,6 +490,10 @@ private void MainCommandLoop() { throw; } + catch (OperationCanceledException exc) when (exc.IsCancellationOf(_process)) + { + throw; + } catch (ScriptException exc) { exc.SetPositionIfEmpty(GetPositionInfo()); @@ -1298,6 +1302,8 @@ private void RaiseException(int arg) private void LineNum(int arg) { + _process.CancellationToken.ThrowIfCancellationRequested(); + if (_currentFrame.LineNumber != arg) { _currentFrame.LineNumber = arg; diff --git a/src/Tests/OneScript.Core.Tests/BackgroundTaskThreadTests.cs b/src/Tests/OneScript.Core.Tests/BackgroundTaskThreadTests.cs new file mode 100644 index 000000000..364fc6803 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/BackgroundTaskThreadTests.cs @@ -0,0 +1,58 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System.Threading.Tasks; +using OneScript.StandardLibrary; +using ScriptEngine.HostedScript; +using ScriptEngine.HostedScript.Extensions; +using ScriptEngine.Hosting; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class BackgroundTaskThreadTests + { + private const string Script = + "Перем Блок;\n" + + "\n" + + "Функция ВзятьБлокировку() Экспорт\n" + + " Если Блок.Заблокировать(50) Тогда\n" + + " Блок.Разблокировать();\n" + + " Возврат Истина;\n" + + " КонецЕсли;\n" + + " Возврат Ложь;\n" + + "КонецФункции\n" + + "\n" + + "Блок = Новый БлокировкаРесурса;\n" + + "Для Номер = 1 По 20 Цикл\n" + + " Блок.Заблокировать();\n" + + " Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, \"ВзятьБлокировку\");\n" + + " Задание.ОжидатьЗавершения();\n" + + " Блок.Разблокировать();\n" + + " Если Задание.Результат Тогда\n" + + " ВызватьИсключение \"Задание получило блокировку, которую держит ожидающий\";\n" + + " КонецЕсли;\n" + + "КонецЦикла;\n"; + + [Fact] + public void TaskAwaitedFromThreadPoolRunsInItsOwnThread() + { + var builder = DefaultEngineBuilder.Create() + .SetDefaultOptions() + .UseImports() + .UseDefaultHosting() + .SetupEnvironment(e => e.AddStandardLibrary()); + var engine = new HostedScriptEngine(builder.Build()); + engine.Initialize(); + + // Как запрос веб-сервера: процесс без отмены в потоке пула. Если задание выполнится + // в этом же потоке, оно войдет в чужую блокировку и при завершении отпустит ее + Task.Run(() => engine.Engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), Script, engine.Engine.NewProcess())).GetAwaiter().GetResult(); + } + } +} diff --git a/tests/tasks.os b/tests/tasks.os index c699008b9..b4a8572ec 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -8,6 +8,9 @@ Перем ЗавершениеПотокаЗафиксировано; Перем ЭлементовВДанныхПриЗавершении; +Перем ЗаданиеЗапущено; +Перем ОтменаПерехвачена; + //////////////////////////////////////////////////////////////////// // Программный интерфейс @@ -42,6 +45,32 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоОбработчикиСобытийВызываютсяВФоновомЗадании"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПараллельноеЧтениеЗначенияСистемногоПеречисления"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПараллельноеЧтениеСвойстваПеречисления"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьВыполняющеесяЗадание"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЗаданиеСразуПослеЗапуска"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьПриостановитьПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_НеПерехватыватьОтменуВПопытке"); + ВсеТесты.Добавить("ТестДолжен_ВызватьОбработчикЗавершенияПотокаПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_НеМенятьСостояниеЗавершенногоЗаданияПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЦиклПокаВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЦиклДляВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьЦиклДляКаждогоВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_ОтменитьРекурсиюВНативномМодуле"); + ВсеТесты.Добавить("ТестДолжен_НеПерехватыватьОтменуВПопыткеНативногоМодуля"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьЗавершенияПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьВсеПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьЛюбоеПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжидатьЗавершенияЗадачПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ПрерватьОжиданиеБлокировкиПриОтмене"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьБлокировкуОтмененногоЗадания"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьБлокировкуЗаданияЗавершившегосяСОшибкой"); + ВсеТесты.Добавить("ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяСебя"); + ВсеТесты.Добавить("ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяРодителя"); + ВсеТесты.Добавить("ТестДолжен_ОжидатьЗавершенияЗадачИзСоседнихЗаданий"); + ВсеТесты.Добавить("ТестДолжен_ЗавершитьАварийноЗаданиеСОшибкойВстроенногоОбъекта"); + ВсеТесты.Добавить("ТестДолжен_ЗахватитьСвободнуюБлокировкуСТаймаутом"); + ВсеТесты.Добавить("ТестДолжен_ВернутьЛожьПоТаймаутуБлокировки"); + ВсеТесты.Добавить("ТестДолжен_ЗахватитьБлокировкуДоИстеченияТаймаута"); + ВсеТесты.Добавить("ТестДолжен_ЗапретитьОтрицательныйТаймаутБлокировки"); Возврат ВсеТесты; @@ -51,6 +80,21 @@ ФоновыеЗадания.Очистить(); КонецПроцедуры +Процедура ПослеЗапускаТеста() Экспорт + // Бесконечные задания упавшего теста не должны пережить его, + // в том числе еще не начавшие выполняться + Незавершенные = Новый Массив; + Для Каждого Задание Из ФоновыеЗадания.ПолучитьФоновыеЗадания() Цикл + Состояние = Задание.Состояние; + Если Состояние = СостояниеФоновогоЗадания.НеВыполнялось + Или Состояние = СостояниеФоновогоЗадания.Активно Тогда + Задание.Отменить(); + Незавершенные.Добавить(Задание); + КонецЕсли; + КонецЦикла; + ФоновыеЗадания.ОжидатьВсе(Незавершенные, 5000); +КонецПроцедуры + Процедура ПроцедураБезПараметров() Экспорт Приостановить(500); КонецПроцедуры @@ -794,3 +838,535 @@ ПозднееЗадание.ОжидатьЗавершения(); КонецПроцедуры + +Процедура БесконечныйЦикл() Экспорт + ЗаданиеЗапущено = Истина; + Пока Истина Цикл + КонецЦикла; +КонецПроцедуры + +Процедура ДолгоеОжидание() Экспорт + ЗаданиеЗапущено = Истина; + Приостановить(60000); +КонецПроцедуры + +Процедура БесконечныйЦиклВПопытке() Экспорт + ЗаданиеЗапущено = Истина; + Пока Истина Цикл + Попытка + Приостановить(10); + Исключение + ОтменаПерехвачена = Истина; + КонецПопытки; + КонецЦикла; +КонецПроцедуры + +Процедура БесконечныйЦиклСОбработчикомЗавершения() Экспорт + ПодписатьсяНаЗавершениеПотока(); + БесконечныйЦикл(); +КонецПроцедуры + +Процедура ДождатьсяЗапускаЗадания() + + Для Итерация = 1 По 1000 Цикл + Если ЗаданиеЗапущено = Истина Тогда + Возврат; + КонецЕсли; + Приостановить(10); + КонецЦикла; + + ВызватьИсключение "Фоновое задание не запустилось"; + +КонецПроцедуры + +Процедура ПроверитьЧтоЗаданиеОтменено(Задание) + + юТест.ПроверитьИстину(Задание.ОжидатьЗавершения(10000), "Отмененное задание должно завершиться"); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Отменено, Задание.Состояние); + юТест.ПроверитьРавенство(Неопределено, Задание.ИнформацияОбОшибке); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьВыполняющеесяЗадание() Экспорт + + ЗаданиеЗапущено = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦикл"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьЗаданиеСразуПослеЗапуска() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦикл"); + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьПриостановитьПриОтмене() Экспорт + + ЗаданиеЗапущено = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ДолгоеОжидание"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_НеПерехватыватьОтменуВПопытке() Экспорт + + ЗаданиеЗапущено = Ложь; + ОтменаПерехвачена = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦиклВПопытке"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + юТест.ПроверитьРавенство(Ложь, ОтменаПерехвачена, "Отмена не должна перехватываться в Попытка"); + +КонецПроцедуры + +Процедура ТестДолжен_ВызватьОбработчикЗавершенияПотокаПриОтмене() Экспорт + + ЗаданиеЗапущено = Ложь; + ЗавершениеПотокаЗафиксировано = Ложь; + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "БесконечныйЦиклСОбработчикомЗавершения"); + ДождатьсяЗапускаЗадания(); + + Задание.Отменить(); + + ПроверитьЧтоЗаданиеОтменено(Задание); + юТест.ПроверитьРавенство(Истина, ЗавершениеПотокаЗафиксировано, + "Обработчик ПриЗавершении должен вызываться и у отмененного задания"); + +КонецПроцедуры + +Процедура ТестДолжен_НеМенятьСостояниеЗавершенногоЗаданияПриОтмене() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "Пустышка"); + Задание.ОжидатьЗавершения(); + + Задание.Отменить(); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние); + +КонецПроцедуры + +Функция ЗагрузитьНативныйМодульДляОтмены() + + Текст = + "#native + |Процедура ЦиклПока(Сигнал) Экспорт + | Сигнал.Добавить(Истина); + | Пока Истина Цикл + | КонецЦикла; + |КонецПроцедуры + | + |Процедура ЦиклДля(Сигнал) Экспорт + | Сигнал.Добавить(Истина); + | Для Сч = 1 По 1000000000000 Цикл + | КонецЦикла; + |КонецПроцедуры + | + |Процедура ЦиклДляКаждого(Сигнал) Экспорт + | Элементы = Новый Массив(1000); + | Сигнал.Добавить(Истина); + | Для Каждого А Из Элементы Цикл + | Для Каждого Б Из Элементы Цикл + | Для Каждого В Из Элементы Цикл + | Для Каждого Г Из Элементы Цикл + | КонецЦикла; + | КонецЦикла; + | КонецЦикла; + | КонецЦикла; + |КонецПроцедуры + | + |Функция Фибоначчи(Н) Экспорт + | Если Н < 2 Тогда + | Возврат Н; + | КонецЕсли; + | Возврат Фибоначчи(Н - 1) + Фибоначчи(Н - 2); + |КонецФункции + | + |Процедура Рекурсия(Сигнал) Экспорт + | Сигнал.Добавить(Истина); + | Фибоначчи(100); + |КонецПроцедуры + | + |Процедура ЦиклВПопытке(Сигнал, Перехват) Экспорт + | Сигнал.Добавить(Истина); + | Пока Истина Цикл + | Попытка + | Приостановить(10); + | Исключение + | Перехват.Добавить(Истина); + | КонецПопытки; + | КонецЦикла; + |КонецПроцедуры"; + + Возврат ЗагрузитьСценарийИзСтроки(Текст); + +КонецФункции + +Функция ЗапуститьСоСигналом(Объект, ИмяМетода, ДополнительныйПараметр = Неопределено) + + Сигнал = Новый Массив; + Параметры = Новый Массив; + Параметры.Добавить(Сигнал); + Если ДополнительныйПараметр <> Неопределено Тогда + Параметры.Добавить(ДополнительныйПараметр); + КонецЕсли; + + Задание = ФоновыеЗадания.Выполнить(Объект, ИмяМетода, Параметры); + + Для Итерация = 1 По 1000 Цикл + Если Сигнал.Количество() > 0 Тогда + Прервать; + КонецЕсли; + Приостановить(10); + КонецЦикла; + юТест.ПроверитьРавенство(1, Сигнал.Количество(), "Фоновое задание не запустилось"); + + // После сигнала задание переходит к блокирующему вызову, дадим ему туда дойти + Приостановить(100); + + Возврат Задание; + +КонецФункции + +Функция ЗапуститьИОтменитьНативноеЗадание(ИмяМетода, Перехват = Неопределено) + + Задание = ЗапуститьСоСигналом(ЗагрузитьНативныйМодульДляОтмены(), ИмяМетода, Перехват); + Задание.Отменить(); + + Возврат Задание; + +КонецФункции + +Процедура ТестДолжен_ОтменитьЦиклПокаВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклПока"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьЦиклДляВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклДля"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьЦиклДляКаждогоВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклДляКаждого"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_ОтменитьРекурсиюВНативномМодуле() Экспорт + + Задание = ЗапуститьИОтменитьНативноеЗадание("Рекурсия"); + ПроверитьЧтоЗаданиеОтменено(Задание); + +КонецПроцедуры + +Процедура ТестДолжен_НеПерехватыватьОтменуВПопыткеНативногоМодуля() Экспорт + + Перехват = Новый Массив; + Задание = ЗапуститьИОтменитьНативноеЗадание("ЦиклВПопытке", Перехват); + + ПроверитьЧтоЗаданиеОтменено(Задание); + юТест.ПроверитьРавенство(0, Перехват.Количество(), "Отмена не должна перехватываться в Попытка"); + +КонецПроцедуры + +Процедура ЦиклДоОтмены(Сигнал) Экспорт + Сигнал.Добавить(Истина); + Пока Истина Цикл + Приостановить(10); + КонецЦикла; +КонецПроцедуры + +Процедура ЖдатьДругоеЗадание(Сигнал, Параметры) Экспорт + + Цель = Параметры.Цель; + Задания = Новый Массив; + Задания.Добавить(Цель); + + Если Параметры.Способ = "ОжидатьЗавершенияЗадач" Тогда + // Из задания ждутся только его вложенные задания + ПараметрыВложенного = Новый Массив; + ПараметрыВложенного.Добавить(Новый Массив); + ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЦиклДоОтмены", ПараметрыВложенного); + КонецЕсли; + + Сигнал.Добавить(Истина); + Если Параметры.Способ = "ОжидатьЗавершения" Тогда + Цель.ОжидатьЗавершения(); + ИначеЕсли Параметры.Способ = "ОжидатьВсе" Тогда + ФоновыеЗадания.ОжидатьВсе(Задания); + ИначеЕсли Параметры.Способ = "ОжидатьЛюбое" Тогда + ФоновыеЗадания.ОжидатьЛюбое(Задания); + ИначеЕсли Параметры.Способ = "ОжидатьЗавершенияЗадач" Тогда + ФоновыеЗадания.ОжидатьЗавершенияЗадач(); + КонецЕсли; + +КонецПроцедуры + +Процедура ПроверитьОтменуОжиданияДругогоЗадания(Способ) + + Цель = ЗапуститьСоСигналом(ЭтотОбъект, "ЦиклДоОтмены"); + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ЖдатьДругоеЗадание", Новый Структура("Цель, Способ", Цель, Способ)); + + Ждущее.Отменить(); + Остановилось = Ждущее.ОжидатьЗавершения(10000); + Цель.Отменить(); + + юТест.ПроверитьИстину(Остановилось, "Ожидание другого задания должно прерываться отменой"); + ПроверитьЧтоЗаданиеОтменено(Ждущее); + ПроверитьЧтоЗаданиеОтменено(Цель); + +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьЗавершенияПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьЗавершения"); +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьВсеПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьВсе"); +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьЛюбоеПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьЛюбое"); +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжидатьЗавершенияЗадачПриОтмене() Экспорт + ПроверитьОтменуОжиданияДругогоЗадания("ОжидатьЗавершенияЗадач"); +КонецПроцедуры + +Процедура ДержатьБлокировку(Сигнал, Блокировка) Экспорт + + Блокировка.Заблокировать(); + Попытка + Сигнал.Добавить(Истина); + Пока Истина Цикл + Приостановить(10); + КонецЦикла; + Исключение + Блокировка.Разблокировать(); + ВызватьИсключение; + КонецПопытки; + +КонецПроцедуры + +Процедура ВзятьБлокировку(Сигнал, Блокировка) Экспорт + Сигнал.Добавить(Истина); + Блокировка.Заблокировать(); + Блокировка.Разблокировать(); +КонецПроцедуры + +Процедура ВзятьБлокировкуИУпасть(Сигнал, Блокировка) Экспорт + Сигнал.Добавить(Истина); + Блокировка.Заблокировать(); + ВызватьИсключение "Ошибка при захваченной блокировке"; +КонецПроцедуры + +Процедура ПроверитьЧтоБлокировкаСвободна(Блокировка) + + Параметры = Новый Массив; + Параметры.Добавить(Новый Массив); + Параметры.Добавить(Блокировка); + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВзятьБлокировку", Параметры); + Освободилась = Задание.ОжидатьЗавершения(5000); + Задание.Отменить(); + + юТест.ПроверитьИстину(Освободилась, "Блокировка должна быть свободна"); + +КонецПроцедуры + +Процедура ТестДолжен_ПрерватьОжиданиеБлокировкиПриОтмене() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ВзятьБлокировку", Блокировка); + + Ждущее.Отменить(); + Остановилось = Ждущее.ОжидатьЗавершения(10000); + Держатель.Отменить(); + + юТест.ПроверитьИстину(Остановилось, "Ожидание блокировки должно прерываться отменой"); + ПроверитьЧтоЗаданиеОтменено(Ждущее); + ПроверитьЧтоЗаданиеОтменено(Держатель); + +КонецПроцедуры + +Процедура ТестДолжен_ОсвободитьБлокировкуОтмененногоЗадания() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + + Держатель.Отменить(); + ПроверитьЧтоЗаданиеОтменено(Держатель); + + ПроверитьЧтоБлокировкаСвободна(Блокировка); + +КонецПроцедуры + +Процедура ТестДолжен_ОсвободитьБлокировкуЗаданияЗавершившегосяСОшибкой() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Задание = ЗапуститьСоСигналом(ЭтотОбъект, "ВзятьБлокировкуИУпасть", Блокировка); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.ЗавершеноАварийно, Задание.Состояние); + ПроверитьЧтоБлокировкаСвободна(Блокировка); + +КонецПроцедуры + +Функция ЗапуститьВложенноеИДождатьсяВсехЗадач() Экспорт + + Вложенное = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураБезПараметров"); + ФоновыеЗадания.ОжидатьЗавершенияЗадач(); + + Результат = Новый Структура; + Результат.Вставить("СостояниеВложенного", Вложенное.Состояние); + Результат.Вставить("ТекущееВРеестре", ФоновыеЗадания.ПолучитьТекущее() <> Неопределено); + + Возврат Результат; + +КонецФункции + +Процедура ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяСебя() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач"); + Завершилось = Задание.ОжидатьЗавершения(10000); + Задание.Отменить(); + + юТест.ПроверитьИстину(Завершилось, "Задание не должно ждать само себя"); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Результат.СостояниеВложенного); + юТест.ПроверитьИстину(Задание.Результат.ТекущееВРеестре, "Текущее задание не должно удаляться из реестра"); + +КонецПроцедуры + +Функция ЗапуститьДочернееИДождаться() Экспорт + + Дочернее = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач"); + Возврат Дочернее.ОжидатьЗавершения(5000); + +КонецФункции + +Процедура ТестДолжен_ОжидатьЗавершенияЗадачИзЗаданияНеОжидаяРодителя() Экспорт + + // Родитель ждет дочернее задание, а оно - свои вложенные: родителя ждать нельзя + Родитель = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьДочернееИДождаться"); + Завершилось = Родитель.ОжидатьЗавершения(10000); + + юТест.ПроверитьИстину(Завершилось, "Родитель не завершился"); + юТест.ПроверитьИстину(Родитель.Результат, "Дочернее задание ждало родителя"); + +КонецПроцедуры + +Процедура ТестДолжен_ОжидатьЗавершенияЗадачИзСоседнихЗаданий() Экспорт + + Задания = Новый Массив; + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач")); + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗапуститьВложенноеИДождатьсяВсехЗадач")); + + юТест.ПроверитьИстину(ФоновыеЗадания.ОжидатьВсе(Задания, 10000), "Соседние задания ждали друг друга"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗавершитьАварийноЗаданиеСОшибкойВстроенногоОбъекта() Экспорт + + // Метод встроенного объекта бросает исключение .NET, а не исключение сценария + Путь = ОбъединитьПути(КаталогВременныхФайлов(), Строка(Новый УникальныйИдентификатор), "нет.txt"); + Задание = ФоновыеЗадания.Выполнить(Новый Файл(Путь), "Размер"); + Задание.ОжидатьЗавершения(5000); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.ЗавершеноАварийно, Задание.Состояние); + юТест.ПроверитьНеРавенство(Неопределено, Задание.ИнформацияОбОшибке); + +КонецПроцедуры + +Функция ЗаблокироватьСТаймаутом(Сигнал, Параметры) Экспорт + + Сигнал.Добавить(Истина); + Захвачена = Параметры.Блокировка.Заблокировать(Параметры.Таймаут); + Если Захвачена Тогда + Параметры.Блокировка.Разблокировать(); + КонецЕсли; + + Возврат Захвачена; + +КонецФункции + +Процедура ТестДолжен_ЗахватитьСвободнуюБлокировкуСТаймаутом() Экспорт + + Блокировка = Новый БлокировкаРесурса; + юТест.ПроверитьИстину(Блокировка.Заблокировать(100)); + Блокировка.Разблокировать(); + +КонецПроцедуры + +Процедура ТестДолжен_ВернутьЛожьПоТаймаутуБлокировки() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + + // В основном потоке ожидание без отмены + Начало = ТекущаяУниверсальнаяДатаВМиллисекундах(); + ЗахваченаВОсновномПотоке = Блокировка.Заблокировать(300); + Прошло = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало; + + // В фоновом задании ожидание с проверкой отмены + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ЗаблокироватьСТаймаутом", + Новый Структура("Блокировка, Таймаут", Блокировка, 300)); + Ждущее.ОжидатьЗавершения(5000); + + Держатель.Отменить(); + ПроверитьЧтоЗаданиеОтменено(Держатель); + + юТест.ПроверитьЛожь(ЗахваченаВОсновномПотоке); + юТест.ПроверитьБольшеИлиРавно(Прошло, 250, "Ожидание должно длиться не меньше таймаута"); + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Ждущее.Состояние); + юТест.ПроверитьРавенство(Ложь, Ждущее.Результат); + +КонецПроцедуры + +Процедура ТестДолжен_ЗахватитьБлокировкуДоИстеченияТаймаута() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Держатель = ЗапуститьСоСигналом(ЭтотОбъект, "ДержатьБлокировку", Блокировка); + Ждущее = ЗапуститьСоСигналом(ЭтотОбъект, "ЗаблокироватьСТаймаутом", + Новый Структура("Блокировка, Таймаут", Блокировка, 10000)); + + Держатель.Отменить(); + + юТест.ПроверитьИстину(Ждущее.ОжидатьЗавершения(5000), "Ожидающее задание должно получить блокировку"); + юТест.ПроверитьРавенство(Истина, Ждущее.Результат); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапретитьОтрицательныйТаймаутБлокировки() Экспорт + + Блокировка = Новый БлокировкаРесурса; + Попытка + Блокировка.Заблокировать(-1); + Исключение + Возврат; + КонецПопытки; + + ВызватьИсключение "Ожидали исключение на отрицательном таймауте"; + +КонецПроцедуры