diff --git a/src/OneScript.Core/Execution/IBslProcess.cs b/src/OneScript.Core/Execution/IBslProcess.cs index 2ef3b4233..297f11f63 100644 --- a/src/OneScript.Core/Execution/IBslProcess.cs +++ b/src/OneScript.Core/Execution/IBslProcess.cs @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the #nullable enable +using System; using OneScript.Contexts; using OneScript.DependencyInjection; using OneScript.Values; @@ -15,7 +16,10 @@ This Source Code Form is subject to the terms of the namespace OneScript.Execution { /// - /// Готовый к исполнению bsl-процесс, с настроенным окружением + /// Готовый к исполнению bsl-процесс, с настроенным окружением. + /// + /// Процесс освобождается тем, кто его создал, когда единица исполнения отработала. + /// Освобождение завершает поток исполнения процесса. /// public interface IBslProcess { @@ -33,7 +37,18 @@ public interface IBslProcess /// Сервисы текущего процесса /// public IServiceContainer Services { get; } - + public int VirtualThreadId { get; } + + /** + * Обертка текущего процесса в терминах BSL. + * Может отсутствовать, полностью определяется хост-приложением процесса. + */ + public IRuntimeContextInstance? BslWrapper + { + get => null; + // ReSharper disable once ValueParameterNotUsed + set { } + } } } \ No newline at end of file diff --git a/src/OneScript.StandardLibrary/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/ExecutionThreadContext.cs new file mode 100644 index 000000000..0b8f56952 --- /dev/null +++ b/src/OneScript.StandardLibrary/ExecutionThreadContext.cs @@ -0,0 +1,133 @@ +/*---------------------------------------------------------- +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; +using System.Linq; +using OneScript.Contexts; +using OneScript.Execution; +using OneScript.StandardLibrary.Collections; +using ScriptEngine; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace OneScript.StandardLibrary +{ + /// + /// Поток исполнения bsl-кода. + /// + /// Потоком исполнения является каждая независимая единица исполнения: основной скрипт, + /// каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. Получить поток + /// исполнения текущего кода можно функцией ТекущийПоток(). + /// + /// Свойство Данные представляет собой хранилище, живущее ровно столько же, сколько живёт + /// сам поток исполнения. Оно предназначено для библиотек, которым нужно хранить состояние + /// в разрезе единицы исполнения (аналог thread-local хранилища). + /// + /// По завершении потока исполнения поднимается событие ПриЗавершении (оно же OnTermination), + /// на которое можно подписаться штатным способом: + /// + /// ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ВернутьСоединениеВПул; + /// + /// Обработчик вызывается до очистки данных потока, поэтому ещё видит их содержимое. + /// Это позволяет владельцам ресурсов узнавать о конце единицы исполнения вместо того, + /// чтобы опрашивать список фоновых заданий. + /// + [ContextClass("ПотокИсполнения", "ExecutionThread")] + public sealed class ExecutionThreadContext : AutoContext, IDisposable + { + private readonly IBslProcess _process; + + private bool _isDisposed; + + private ExecutionThreadContext(IBslProcess process) + { + _process = process; + } + + /// + /// Идентификатор потока исполнения. Предназначен для диагностики и журналирования. + /// + /// Идентификаторы выдаются последовательно и в пределах запущенного движка не повторяются, + /// пока счётчик не исчерпает диапазон Int32. Для хранения состояния в разрезе потока + /// исполнения используйте свойство Данные, а не идентификатор в качестве ключа. + /// + /// Число. Идентификатор потока исполнения. + [ContextProperty("Идентификатор", "Id")] + public int Identifier => _process.VirtualThreadId; + + /// + /// Хранилище данных потока исполнения, аналог набора thread-local переменных. + /// + /// Соответствие создаётся вместе с потоком исполнения и не разделяется с другими потоками. + /// В конце потока исполнения соответствие очищается, а его значения, поддерживающие + /// интерфейс IDisposable среды CLR, принудительно освобождаются. + /// + /// Соответствие. Данные потока исполнения. + [ContextProperty("Данные", "Data")] + public MapImpl Data { get; } = new MapImpl(); + + /// + /// Возвращает поток исполнения указанного bsl-процесса, создавая его при первом обращении. + /// Для одного процесса всегда возвращается один и тот же экземпляр. + /// + /// Созданный поток остаётся на процессе и освобождается вместе с ним. + /// + internal static ExecutionThreadContext Of(IBslProcess process) + { + if (process.BslWrapper is ExecutionThreadContext wrapper) + return wrapper; + + lock (process) + { + if (process.BslWrapper is ExecutionThreadContext created) + return created; + + if (process.BslWrapper != null) + throw new InvalidOperationException($"BslWrapper for process is not {nameof(ExecutionThreadContext)}: {process.BslWrapper.GetType()}"); + + var thread = new ExecutionThreadContext(process); + process.BslWrapper = thread; + + return thread; + } + } + + public void Dispose() + { + // Пока идёт завершение, поток ещё числится за процессом, и обработчик вправе + // добраться до него через ТекущийПоток(). Если он при этом освободит процесс, + // завершение не должно пойти по второму кругу. + if (_isDisposed) + return; + + _isDisposed = true; + + try + { + foreach (var item in Data.ToArray()) + { + if (item.Value is not IDisposable disposable) + continue; + + try + { + disposable.Dispose(); + } + catch (Exception exception) + { + SystemLogger.Write( + $"WARNING! Error releasing execution thread data '{item.Key}': {exception.Message}"); + } + } + } + finally + { + Data.Clear(); + } + } + } +} diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index f12754f12..e29661cf4 100644 --- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs +++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs @@ -75,6 +75,28 @@ public void RunGarbageCollection() GC.WaitForPendingFinalizers(); } + /// + /// Возвращает поток исполнения, в котором выполняется текущий код. + /// + /// Отдельным потоком исполнения является каждая независимая единица исполнения bsl-кода: + /// основной скрипт, каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. + /// Для одной единицы исполнения всегда возвращается один и тот же объект, поэтому его + /// свойство Данные можно использовать как набор thread-local переменных. + /// + /// В отличие от идентификатора фонового задания, поток исполнения определён во всех + /// контекстах, в том числе при обработке запросов веб-сервера, где фоновое задание + /// отсутствует. + /// + /// Поток исполнения не наследуется: фоновое задание, запущенное из текущего потока + /// исполнения, получит собственный поток с собственными данными. + /// + /// ПотокИсполнения. Поток исполнения текущего кода. + [ContextMethod("ТекущийПоток", "CurrentThread")] + public ExecutionThreadContext CurrentThread(IBslProcess process) + { + return ExecutionThreadContext.Of(process); + } + /// /// Приостанавливает выполнение скрипта. /// diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs index bb0367539..c3d253ce2 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs @@ -1,4 +1,4 @@ -/*---------------------------------------------------------- +/*---------------------------------------------------------- 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 diff --git a/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs b/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs index 24549e84b..6c283db1e 100644 --- a/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs +++ b/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs @@ -114,6 +114,14 @@ public void RemoveHandler( } } + public void RemoveAllHandlers(IRuntimeContextInstance eventSource) + { + lock (_subscriptionLock) + { + _registeredHandlers.Remove(eventSource); + } + } + public void HandleEvent(IRuntimeContextInstance eventSource, string eventName, IValue[] eventArgs, IBslProcess process) { diff --git a/src/ScriptEngine/BslProcess.cs b/src/ScriptEngine/BslProcess.cs index 65b8c9de2..4d7deeb06 100644 --- a/src/ScriptEngine/BslProcess.cs +++ b/src/ScriptEngine/BslProcess.cs @@ -18,10 +18,13 @@ namespace ScriptEngine { internal class BslProcess : IBslProcess { + private static readonly string[] TerminationEventNames = { "ПриЗавершении", "OnTermination" }; + private readonly IExecutorProvider[] _executorProviders; private readonly IDictionary _bslExecutorsByModule; private bool _isRunning; + private bool _disposed; public BslProcess(int id, ExecutionContext context, IEnumerable executorProviders) { @@ -55,10 +58,51 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe { if (notifyExecutors) { + RaiseTerminationEvent(); + if (BslWrapper is IDisposable disposable) + { + disposable.Dispose(); + } + Array.ForEach(_executorProviders, e => e.AfterProcessExit(this)); + Services.Dispose(); _isRunning = false; } } } + + public IRuntimeContextInstance BslWrapper { get; set; } + + private void RaiseTerminationEvent() + { + if (BslWrapper == null) + return; + + var eventProcessor = Services.TryResolve(); + if (eventProcessor == null) + return; + + try + { + foreach (var eventName in TerminationEventNames) + { + try + { + eventProcessor.HandleEvent(BslWrapper, eventName, Array.Empty(), this); + } + catch (Exception exception) + { + SystemLogger.Write( + $"WARNING! Error in execution thread termination handler '{eventName}': {exception.Message}"); + } + } + } + finally + { + // Процессор событий держит источник, пока подписки не сняты. Потоков исполнения + // много и живут они недолго, поэтому без явного снятия реестр рос бы бесконечно. + eventProcessor.RemoveAllHandlers(BslWrapper); + } + } } } \ No newline at end of file diff --git a/src/ScriptEngine/Machine/IEventProcessor.cs b/src/ScriptEngine/Machine/IEventProcessor.cs index 9c667fd79..ab936356f 100644 --- a/src/ScriptEngine/Machine/IEventProcessor.cs +++ b/src/ScriptEngine/Machine/IEventProcessor.cs @@ -25,5 +25,17 @@ void RemoveHandler( string handlerMethod); void HandleEvent(IRuntimeContextInstance eventSource, string eventName, IValue[] eventArgs, IBslProcess process); + + /// + /// Снимает все подписки на события указанного источника. + /// + /// Нужен источникам, которые живут меньше самого процессора событий: без этого реестр + /// подписок удерживает такой источник до конца работы движка. Реализация по умолчанию + /// ничего не делает, чтобы не ломать сторонние процессоры событий. + /// + /// Источник, подписки на который нужно снять. + void RemoveAllHandlers(IRuntimeContextInstance eventSource) + { + } } } \ No newline at end of file diff --git a/src/Tests/OneScript.Core.Tests/BslProcessTests.cs b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs new file mode 100644 index 000000000..eb022261b --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs @@ -0,0 +1,73 @@ +/*---------------------------------------------------------- +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; +using FluentAssertions; +using ScriptEngine.Hosting; +using Xunit; + +namespace OneScript.Core.Tests +{ + /// + /// Регистрируется как scoped, поэтому живёт ровно столько же, сколько область сервисов процесса. + /// + public sealed class ScopedDisposableProbe : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + + public class BslProcessTests + { + private static ScriptEngine.ScriptingEngine CreateEngineWithScopedProbe() + { + var builder = DefaultEngineBuilder.Create().SetDefaultOptions(); + builder.Services.RegisterScoped(); + + var engine = builder.Build(); + engine.Initialize(); + + return engine; + } + + [Fact] + public void ProcessReleasesItsServiceScope() + { + var engine = CreateEngineWithScopedProbe(); + + var process = engine.NewProcess(); + var scoped = process.Services.Resolve(); + + scoped.IsDisposed.Should().BeFalse("процесс ещё работает"); + + engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), "f = 1", process); + + scoped.IsDisposed.Should().BeTrue("процесс владеет своей областью сервисов"); + } + + [Fact] + public void ProcessDoesNotReleaseScopeOfAnotherProcess() + { + var engine = CreateEngineWithScopedProbe(); + + var first = engine.NewProcess(); + var second = engine.NewProcess(); + + var firstScoped = first.Services.Resolve(); + var secondScoped = second.Services.Resolve(); + + firstScoped.Should().NotBeSameAs(secondScoped, "у каждого процесса своя область сервисов"); + + secondScoped.IsDisposed.Should().BeFalse("освобождение одного процесса не трогает другой"); + } + } +} diff --git a/src/Tests/OneScript.Core.Tests/EventProcessorTests.cs b/src/Tests/OneScript.Core.Tests/EventProcessorTests.cs new file mode 100644 index 000000000..37f78d5d8 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/EventProcessorTests.cs @@ -0,0 +1,85 @@ +/*---------------------------------------------------------- +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; +using FluentAssertions; +using OneScript.Execution; +using OneScript.StandardLibrary.Collections; +using OneScript.Values; +using ScriptEngine.HostedScript; +using ScriptEngine.Hosting; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class EventProcessorTests + { + private const string HandlerScript = @" + Перем Вызовов Экспорт; + + Процедура Обработчик() Экспорт + Вызовов = Вызовов + 1; + КонецПроцедуры + + Вызовов = 0;"; + + private static (UserScriptContextInstance Handler, IBslProcess Process) CreateHandler() + { + var engine = DefaultEngineBuilder.Create().SetDefaultOptions().Build(); + engine.Initialize(); + + var process = engine.NewProcess(); + var handler = engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), HandlerScript, process); + + return (handler, process); + } + + private static decimal CallCount(UserScriptContextInstance handler) + { + var propertyIndex = handler.GetPropertyNumber("Вызовов"); + return (decimal)(BslNumericValue)handler.GetPropValue(propertyIndex); + } + + [Fact] + public void RemoveAllHandlers_UnsubscribesEverythingOfTheSource() + { + var (handler, process) = CreateHandler(); + var eventSource = new ArrayImpl(); + IEventProcessor processor = new DefaultEventProcessor(); + + processor.AddHandler(eventSource, "ПриЗавершении", handler, "Обработчик"); + processor.HandleEvent(eventSource, "ПриЗавершении", Array.Empty(), process); + + CallCount(handler).Should().Be(1, "подписка должна работать до её снятия"); + + processor.RemoveAllHandlers(eventSource); + processor.HandleEvent(eventSource, "ПриЗавершении", Array.Empty(), process); + + CallCount(handler).Should().Be(1, "после снятия подписок обработчик вызываться не должен"); + } + + [Fact] + public void RemoveAllHandlers_KeepsSubscriptionsOfOtherSources() + { + var (handler, process) = CreateHandler(); + var releasedSource = new ArrayImpl(); + var aliveSource = new ArrayImpl(); + IEventProcessor processor = new DefaultEventProcessor(); + + processor.AddHandler(releasedSource, "ПриЗавершении", handler, "Обработчик"); + processor.AddHandler(aliveSource, "ПриЗавершении", handler, "Обработчик"); + + processor.RemoveAllHandlers(releasedSource); + processor.HandleEvent(aliveSource, "ПриЗавершении", Array.Empty(), process); + + CallCount(handler).Should().Be(1, "снятие подписок одного источника не трогает другие"); + } + } +} diff --git a/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj b/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj index f1708dd92..cba21a483 100644 --- a/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj +++ b/src/Tests/OneScript.Core.Tests/OneScript.Core.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs index f5c18cb9c..848b2a9f1 100644 --- a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs +++ b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs @@ -26,4 +26,9 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe public IServiceContainer Services { get; set; } public int VirtualThreadId => 0; + + public void Dispose() + { + Services.Dispose(); + } } \ No newline at end of file diff --git a/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs b/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs index 531d7fd3b..1bcbe677a 100644 --- a/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs +++ b/src/Tests/OneScript.Dynamic.Tests/NativeSdoTests.cs @@ -94,7 +94,7 @@ private IBslProcess GetProcess(IServiceContainer serviceContainer) { return new NativeExecutableTestProcess { - Services = serviceContainer + Services = serviceContainer.CreateScope() }; } diff --git a/tests/tasks.os b/tests/tasks.os index 99f79d9b2..d39f48ed4 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -5,6 +5,9 @@ Перем СобытиеВызвано; +Перем ЗавершениеПотокаЗафиксировано; +Перем ЭлементовВДанныхПриЗавершении; + //////////////////////////////////////////////////////////////////// // Программный интерфейс @@ -24,6 +27,13 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоВозвращаетсяРезультатДелегата"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоРаботаетБлокировка"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоКодМожетОпределитьИДЗадания"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьУникальностьПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьИзоляциюДанныхПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ВызватьОбработчикЗавершенияПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_НеРонятьЗаданиеИзЗаОшибкиВОбработчикеЗавершения"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьОстальныеДанныеПриОшибкеОсвобождения"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьДанныеПотокаОсновногоСкрипта"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных"); @@ -276,6 +286,328 @@ КонецПроцедуры +Функция ВернутьИдентификаторПотокаИсполнения() Экспорт + + Приостановить(500); + Возврат ТекущийПоток().Идентификатор; + +КонецФункции + +Процедура ТестДолжен_ПроверитьУникальностьПотокаИсполнения() Экспорт + + ИдОсновногоПотока = ТекущийПоток().Идентификатор; + юТест.ПроверитьРавенство(ИдОсновногоПотока, ТекущийПоток().Идентификатор, + "Идентификатор потока исполнения должен быть одинаковым при повторном вызове"); + + МассивЗаданий = Новый Массив; + Для Сч = 1 По 4 Цикл + МассивЗаданий.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВернутьИдентификаторПотокаИсполнения")); + КонецЦикла; + + ФоновыеЗадания.ОжидатьВсе(МассивЗаданий); + + УникальныеИдентификаторы = Новый Соответствие; + Для Каждого Задание Из МассивЗаданий Цикл + юТест.ПроверитьНеРавенство(ИдОсновногоПотока, Задание.Результат, + "Фоновое задание должно получить собственный поток исполнения"); + УникальныеИдентификаторы.Вставить(Задание.Результат, Истина); + КонецЦикла; + + юТест.ПроверитьРавенство(МассивЗаданий.Количество(), УникальныеИдентификаторы.Количество(), + "Идентификаторы одновременных фоновых заданий должны различаться"); + +КонецПроцедуры + +Функция ЗаписатьВДанныеПотокаИЗабрать(Знач Метка) Экспорт + + ТекущийПоток().Данные.Вставить("метка", Метка); + Приостановить(500); + + // Данные основного потока не должны быть видны, а соседние задания не должны затирать наши + Результат = Новый Структура("Метка, ЕстьЧужое"); + Результат.Метка = ТекущийПоток().Данные.Получить("метка"); + Результат.ЕстьЧужое = (ТекущийПоток().Данные.Получить("изОсновногоПотока") <> Неопределено); + + Возврат Результат; + +КонецФункции + +Процедура ТестДолжен_ПроверитьИзоляциюДанныхПотокаИсполнения() Экспорт + + ТекущийПоток().Данные.Вставить("изОсновногоПотока", Истина); + + юТест.ПроверитьРавенство(Истина, ТекущийПоток().Данные.Получить("изОсновногоПотока"), + "ТекущийПоток() должен возвращать один и тот же экземпляр в пределах потока исполнения"); + + МассивЗаданий = Новый Массив; + Для Сч = 1 По 4 Цикл + Параметры = Новый Массив; + Параметры.Добавить(Сч); + МассивЗаданий.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗаписатьВДанныеПотокаИЗабрать", Параметры)); + КонецЦикла; + + ФоновыеЗадания.ОжидатьВсе(МассивЗаданий); + + Для Сч = 1 По МассивЗаданий.Количество() Цикл + Результат = МассивЗаданий[Сч - 1].Результат; + юТест.ПроверитьРавенство(Сч, Результат.Метка, + "Данные потока исполнения не должны затираться соседними фоновыми заданиями"); + юТест.ПроверитьРавенство(Ложь, Результат.ЕстьЧужое, + "Фоновое задание не должно видеть данные основного потока исполнения"); + КонецЦикла; + + ТекущийПоток().Данные.Удалить("изОсновногоПотока"); + +КонецПроцедуры + +Функция ПоложитьВДанныеПотокаОсвобождаемыйОбъект() Экспорт + + ИмяФайла = ПолучитьИмяВременногоФайла("tmp"); + ФайловыйПоток = Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + + ТекущийПоток().Данные.Вставить("файл", ФайловыйПоток); + + // Возвращаем и сам поток исполнения, и объект - чтобы проверить их состояние после завершения задания + Возврат Новый Структура("ПотокИсполнения, ФайловыйПоток, ИмяФайла", + ТекущийПоток(), ФайловыйПоток, ИмяФайла); + +КонецФункции + +Процедура ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПоложитьВДанныеПотокаОсвобождаемыйОбъект"); + Задание.ОжидатьЗавершения(); + + Результат = Задание.Результат; + юТест.ПроверитьНеРавенство(Неопределено, Результат, "Задание должно вернуть результат"); + + юТест.ПроверитьРавенство(0, Результат.ПотокИсполнения.Данные.Количество(), + "Данные потока исполнения должны очищаться по завершении фонового задания"); + + ИсключениеБыло = Ложь; + Попытка + Результат.ФайловыйПоток.ТекущаяПозиция(); + Исключение + ИсключениеБыло = Истина; + КонецПопытки; + + юТест.ПроверитьРавенство(Истина, ИсключениеБыло, + "Значения данных потока исполнения должны принудительно освобождаться"); + + УдалитьФайлы(Результат.ИмяФайла); + +КонецПроцедуры + +Процедура ПриЗавершенииПотокаЗадания() Экспорт + + ЗавершениеПотокаЗафиксировано = Истина; + // Обработчик должен успевать увидеть данные потока до их очистки + ЭлементовВДанныхПриЗавершении = ТекущийПоток().Данные.Количество(); + +КонецПроцедуры + +Функция ПодписатьсяНаЗавершениеПотока() Экспорт + + ТекущийПоток().Данные.Вставить("ресурс", "значение"); + ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаЗадания; + Возврат ТекущийПоток(); + +КонецФункции + +Процедура ТестДолжен_ВызватьОбработчикЗавершенияПотокаИсполнения() Экспорт + + ЗавершениеПотокаЗафиксировано = Ложь; + ЭлементовВДанныхПриЗавершении = -1; + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПодписатьсяНаЗавершениеПотока"); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(Истина, ЗавершениеПотокаЗафиксировано, + "Обработчик ПриЗавершении должен вызываться по завершении потока исполнения"); + + юТест.ПроверитьРавенство(1, ЭлементовВДанныхПриЗавершении, + "Обработчик должен видеть данные потока до их очистки"); + + юТест.ПроверитьРавенство(0, Задание.Результат.Данные.Количество(), + "После обработчика данные потока должны быть очищены"); + +КонецПроцедуры + +Процедура ПриЗавершенииПотокаСИсключением() Экспорт + ВызватьИсключение "Ошибка внутри обработчика завершения"; +КонецПроцедуры + +Функция ПодписатьсяНаПадающийОбработчик() Экспорт + ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаСИсключением; + Возврат "задание отработало"; +КонецФункции + +Процедура ТестДолжен_НеРонятьЗаданиеИзЗаОшибкиВОбработчикеЗавершения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПодписатьсяНаПадающийОбработчик"); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство("задание отработало", Задание.Результат, + "Ошибка в обработчике завершения не должна ломать результат задания"); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, + "Ошибка в обработчике завершения не должна менять состояние задания"); + +КонецПроцедуры + +// Создаёт значение, освобождение которого завершается ошибкой. +// +// ЗаписьДанных при освобождении сбрасывает буфер в нижележащий поток, а он уже закрыт. +// +// Параметры: +// ИмяФайла - Строка - Файл, поверх которого строится значение. +// +Функция ПадающийПриОсвобожденииРесурс(Знач ИмяФайла) + + ФайловыйПоток = Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + Писатель = Новый ЗаписьДанных(ФайловыйПоток); + Писатель.ЗаписатьСтроку("данные, которые придётся сбрасывать при закрытии"); + ФайловыйПоток.Закрыть(); + + Возврат Писатель; + +КонецФункции + +Функция ПоложитьВДанныеПадающийИИсправныйРесурсы() Экспорт + + ФайлПадающего = ПолучитьИмяВременногоФайла("tmp"); + ФайлИсправного = ПолучитьИмяВременногоФайла("tmp"); + + Исправный = Новый ФайловыйПоток(ФайлИсправного, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + + ТекущийПоток().Данные.Вставить("падающий", ПадающийПриОсвобожденииРесурс(ФайлПадающего)); + ТекущийПоток().Данные.Вставить("исправный", Исправный); + + // Пути возвращаются вызывающему: закрытие потоков файлы не удаляет + Возврат Новый Структура("Поток, Исправный, Файлы", ТекущийПоток(), Исправный, + ОбщегоНазначенияМассивИз(ФайлПадающего, ФайлИсправного)); + +КонецФункции + +// Собирает массив из переданных значений. +// +Функция ОбщегоНазначенияМассивИз(Знач Первое, Знач Второе) + + Результат = Новый Массив; + Результат.Добавить(Первое); + Результат.Добавить(Второе); + + Возврат Результат; + +КонецФункции + +// Ошибка освобождения одного значения не должна ни ронять задание, ни оставлять +// неосвобождёнными остальные значения. +// +Процедура ТестДолжен_ОсвободитьОстальныеДанныеПриОшибкеОсвобождения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПоложитьВДанныеПадающийИИсправныйРесурсы"); + Задание.ОжидатьЗавершения(); + + Результат = Задание.Результат; + + Попытка + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, + "Ошибка освобождения значения не должна менять состояние задания"); + + юТест.ПроверитьНеРавенство(Неопределено, Результат, "Задание должно вернуть результат"); + + юТест.ПроверитьРавенство(0, Результат.Поток.Данные.Количество(), + "Данные потока должны очищаться даже при ошибке освобождения одного из значений"); + + ИсправныйОсвобожден = Ложь; + Попытка + Результат.Исправный.ТекущаяПозиция(); + Исключение + ИсправныйОсвобожден = Истина; + КонецПопытки; + + юТест.ПроверитьРавенство(Истина, ИсправныйОсвобожден, + "Исправное значение должно освобождаться независимо от ошибки на соседнем"); + + Исключение + УдалитьВременныеФайлы(Результат); + ВызватьИсключение; + КонецПопытки; + + УдалитьВременныеФайлы(Результат); + +КонецПроцедуры + +// Удаляет временные файлы, созданные заданием: закрытие потоков их не удаляет. +// +Процедура УдалитьВременныеФайлы(Знач Результат) + + Если Результат = Неопределено Тогда + Возврат; + КонецЕсли; + + Для Каждого ИмяФайла Из Результат.Файлы Цикл + Если Новый Файл(ИмяФайла).Существует() Тогда + УдалитьФайлы(ИмяФайла); + КонецЕсли; + КонецЦикла; + +КонецПроцедуры + +// Поток исполнения основного скрипта тоже завершается: обработчик ПриЗавершении должен +// сработать по окончании скрипта, а не остаться невызванным до конца процесса. +// +// Проверяется дочерним oscript: изнутри самого скрипта момент его завершения не наблюдаем. +// Дочерний процесс подписывается на завершение своего потока и пишет из обработчика файл-метку, +// родитель дожидается завершения и смотрит на метку. +// +Процедура ТестДолжен_ОсвободитьДанныеПотокаОсновногоСкрипта() Экспорт + + ФайлМетки = ПолучитьИмяВременногоФайла("txt"); + ФайлСкрипта = ПолучитьИмяВременногоФайла("os"); + + ТекстСкрипта = Новый ЗаписьТекста(ФайлСкрипта, КодировкаТекста.UTF8); + ТекстСкрипта.ЗаписатьСтроку("Процедура ПриЗавершении() Экспорт"); + ТекстСкрипта.ЗаписатьСтроку(" Запись = Новый ЗаписьТекста(""" + СтрЗаменить(ФайлМетки, "\", "\\") + """);"); + ТекстСкрипта.ЗаписатьСтроку(" Запись.ЗаписатьСтроку(""элементов="" + ТекущийПоток().Данные.Количество());"); + ТекстСкрипта.ЗаписатьСтроку(" Запись.Закрыть();"); + ТекстСкрипта.ЗаписатьСтроку("КонецПроцедуры"); + ТекстСкрипта.ЗаписатьСтроку("ТекущийПоток().Данные.Вставить(""ключ"", ""значение"");"); + ТекстСкрипта.ЗаписатьСтроку("ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершении;"); + ТекстСкрипта.Закрыть(); + + Попытка + + Процесс = СоздатьПроцесс("dotnet """ + КаталогПрограммы() + "/oscript.dll"" """ + ФайлСкрипта + """"); + Процесс.Запустить(); + Процесс.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(0, Процесс.КодВозврата, "Дочерний скрипт должен отработать без ошибки"); + + юТест.ПроверитьРавенство(Истина, Новый Файл(ФайлМетки).Существует(), + "Обработчик ПриЗавершении должен вызываться и для потока основного скрипта"); + + Чтение = Новый ЧтениеТекста(ФайлМетки); + Метка = СокрЛП(Чтение.ПрочитатьСтроку()); + Чтение.Закрыть(); + + юТест.ПроверитьРавенство("элементов=1", Метка, + "Обработчик должен видеть данные потока основного скрипта до их очистки"); + + Исключение + УдалитьФайлы(ФайлСкрипта); + ВызватьИсключение; + КонецПопытки; + + УдалитьФайлы(ФайлСкрипта); + Если Новый Файл(ФайлМетки).Существует() Тогда + УдалитьФайлы(ФайлМетки); + КонецЕсли; + +КонецПроцедуры + Процедура ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов() Экспорт Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураСИсключением");