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", Метка,
+ "Обработчик должен видеть данные потока основного скрипта до их очистки");
+
+ Исключение
+ УдалитьФайлы(ФайлСкрипта);
+ ВызватьИсключение;
+ КонецПопытки;
+
+ УдалитьФайлы(ФайлСкрипта);
+ Если Новый Файл(ФайлМетки).Существует() Тогда
+ УдалитьФайлы(ФайлМетки);
+ КонецЕсли;
+
+КонецПроцедуры
+
Процедура ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов() Экспорт
Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураСИсключением");