diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs
index f12754f12..883ec1a00 100644
--- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs
+++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs
@@ -12,6 +12,7 @@ This Source Code Form is subject to the terms of the
using OneScript.Contexts;
using OneScript.Exceptions;
using OneScript.Execution;
+using OneScript.StandardLibrary.Threads;
using OneScript.Types;
using OneScript.Values;
using ScriptEngine.Machine;
@@ -75,6 +76,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..8bad8516a 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
@@ -16,6 +16,7 @@ This Source Code Form is subject to the terms of the
using OneScript.Exceptions;
using OneScript.Execution;
using OneScript.StandardLibrary.Collections;
+using OneScript.StandardLibrary.Threads;
using OneScript.Types;
using OneScript.Values;
using ScriptEngine.Machine;
@@ -52,7 +53,15 @@ public BackgroundTask Execute(IRuntimeContextInstance target, string methodName,
var worker = new Task(() =>
{
var process = _runtimeContext.Services.Resolve().NewProcess();
- task.ExecuteOnCurrentThread(process);
+ try
+ {
+ task.ExecuteOnCurrentThread(process);
+ }
+ finally
+ {
+ // Задание отработало, поток исполнения закончился - освобождаем его данные
+ ExecutionThreadContext.Release(process);
+ }
}, taskCreationOptions);
diff --git a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs
index 2ebe5f7c3..8a1435638 100644
--- a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs
+++ b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs
@@ -11,6 +11,7 @@ This Source Code Form is subject to the terms of the
using OneScript.Exceptions;
using OneScript.Execution;
using OneScript.StandardLibrary.Binary;
+using OneScript.StandardLibrary.Threads;
using OneScript.Values;
using ScriptEngine.Machine;
using ScriptEngine.Machine.Contexts;
@@ -272,12 +273,21 @@ private void ConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs e)
return;
var process = _executionContext.Services.Resolve().NewProcess();
-
+
var cancelVar = Variable.Create(BslBooleanValue.False, "Cancel");
var reference = Variable.CreateReference(cancelVar, "Cancel");
var args = new IValue[] { reference };
- eventProcessor.HandleEvent(this, ConsoleCancelKeyEvent, args, process);
+ try
+ {
+ eventProcessor.HandleEvent(this, ConsoleCancelKeyEvent, args, process);
+ }
+ finally
+ {
+ // Обработчик отработал в собственном потоке исполнения - завершаем его
+ ExecutionThreadContext.Release(process);
+ }
+
e.Cancel = reference.Value.AsBoolean();
}
}
diff --git a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs
new file mode 100644
index 000000000..5fb67a4b9
--- /dev/null
+++ b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs
@@ -0,0 +1,186 @@
+/*----------------------------------------------------------
+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 System.Runtime.CompilerServices;
+using OneScript.Contexts;
+using OneScript.Execution;
+using OneScript.StandardLibrary.Collections;
+using ScriptEngine;
+using ScriptEngine.Machine;
+using ScriptEngine.Machine.Contexts;
+
+namespace OneScript.StandardLibrary.Threads
+{
+ ///
+ /// Поток исполнения bsl-кода.
+ ///
+ /// Потоком исполнения является каждая независимая единица исполнения: основной скрипт,
+ /// каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. Получить поток
+ /// исполнения текущего кода можно функцией ТекущийПоток().
+ ///
+ /// Свойство Данные представляет собой хранилище, живущее ровно столько же, сколько живёт
+ /// сам поток исполнения. Оно предназначено для библиотек, которым нужно хранить состояние
+ /// в разрезе единицы исполнения (аналог thread-local хранилища).
+ ///
+ /// По завершении потока исполнения поднимается событие ПриЗавершении (оно же OnTermination),
+ /// на которое можно подписаться штатным способом:
+ ///
+ /// ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ВернутьСоединениеВПул;
+ ///
+ /// Обработчик вызывается до очистки данных потока, поэтому ещё видит их содержимое.
+ /// Это позволяет владельцам ресурсов узнавать о конце единицы исполнения вместо того,
+ /// чтобы опрашивать список фоновых заданий.
+ ///
+ [ContextClass("ПотокИсполнения", "ExecutionThread")]
+ public sealed class ExecutionThreadContext : AutoContext, IDisposable
+ {
+ ///
+ /// Имена события завершения потока исполнения. Событие поднимается под обоими именами,
+ /// поэтому подписаться можно как на русское, так и на английское.
+ ///
+ private static readonly string[] TerminationEventNames = { "ПриЗавершении", "OnTermination" };
+
+ private static readonly ConditionalWeakTable Threads = new();
+
+ private ExecutionThreadContext(IBslProcess process)
+ {
+ Identifier = process.VirtualThreadId;
+ }
+
+ ///
+ /// Идентификатор потока исполнения. Предназначен для диагностики и журналирования.
+ ///
+ /// Идентификаторы выдаются последовательно и в пределах запущенного движка не повторяются,
+ /// пока счётчик не исчерпает диапазон Int32. Для хранения состояния в разрезе потока
+ /// исполнения используйте свойство Данные, а не идентификатор в качестве ключа.
+ ///
+ /// Число. Идентификатор потока исполнения.
+ [ContextProperty("Идентификатор", "Id", CanWrite = false)]
+ public int Identifier { get; }
+
+ ///
+ /// Хранилище данных потока исполнения, аналог набора thread-local переменных.
+ ///
+ /// Соответствие создаётся вместе с потоком исполнения и не разделяется с другими потоками.
+ /// В конце потока исполнения соответствие очищается, а его значения, поддерживающие
+ /// интерфейс IDisposable среды CLR, принудительно освобождаются.
+ ///
+ /// Соответствие. Данные потока исполнения.
+ [ContextProperty("Данные", "Data", CanWrite = false)]
+ public MapImpl Data { get; } = new MapImpl();
+
+ ///
+ /// Возвращает поток исполнения указанного bsl-процесса, создавая его при первом обращении.
+ /// Для одного процесса всегда возвращается один и тот же экземпляр.
+ ///
+ internal static ExecutionThreadContext Of(IBslProcess process)
+ {
+ return Threads.GetValue(process, p => new ExecutionThreadContext(p));
+ }
+
+ ///
+ /// Завершает поток исполнения процесса, освобождая его данные.
+ ///
+ /// Вызывается владельцем процесса, когда процесс отработал: менеджером фоновых заданий
+ /// по завершении задания и веб-сервером по окончании обработки запроса. Если поток
+ /// исполнения не создавался, метод ничего не делает.
+ ///
+ public static void Release(IBslProcess process)
+ {
+ if (process == null)
+ return;
+
+ if (!Threads.TryGetValue(process, out var thread))
+ return;
+
+ thread.RaiseTerminationEvent(process);
+
+ Threads.Remove(process);
+ thread.Dispose();
+ }
+
+ ///
+ /// Поднимает событие завершения потока исполнения.
+ ///
+ /// Событие поднимается до очистки данных, поэтому обработчик ещё видит всё, что поток
+ /// в них положил, и может, например, вернуть занятые ресурсы владельцу.
+ ///
+ /// Ошибка обработчика не выпускается наружу: поток завершается уже после того, как
+ /// код единицы исполнения отработал, и ронять на этом её результат нельзя. У фонового
+ /// задания завершение идёт в блоке finally и затёрло бы исходную ошибку, у веб-сервера
+ /// оно выполняется после отправки ответа.
+ ///
+ private void RaiseTerminationEvent(IBslProcess process)
+ {
+ var eventProcessor = process.Services.TryResolve();
+ if (eventProcessor == null)
+ return;
+
+ try
+ {
+ foreach (var eventName in TerminationEventNames)
+ {
+ try
+ {
+ eventProcessor.HandleEvent(this, eventName, Array.Empty(), process);
+ }
+ catch (Exception exception)
+ {
+ SystemLogger.Write(
+ $"WARNING! Error in execution thread termination handler '{eventName}': {exception.Message}");
+ }
+ }
+ }
+ finally
+ {
+ // Реестр подписок держит источник до конца работы движка, а поток исполнения
+ // живёт лишь до конца своей единицы исполнения. Без снятия подписок каждый
+ // завершившийся поток оставался бы в реестре навсегда.
+ eventProcessor.RemoveAllHandlers(this);
+ }
+ }
+
+ ///
+ /// Освобождает данные потока исполнения.
+ ///
+ /// Каждое значение освобождается независимо: ошибка на одном не мешает освободить
+ /// остальные и не выпускается наружу. Поток завершается уже после того, как код
+ /// единицы исполнения отработал, и ронять на этом её результат нельзя.
+ ///
+ /// Значения снимаются в отдельный список до начала освобождения: освобождаемое значение
+ /// может изменить эти же данные, и перебор живой карты сорвался бы на следующем шаге -
+ /// уже вне защиты, окружающей само освобождение.
+ ///
+ public void Dispose()
+ {
+ 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.Web.Server/RequestBslProcess.cs b/src/OneScript.Web.Server/RequestBslProcess.cs
new file mode 100644
index 000000000..35d364a03
--- /dev/null
+++ b/src/OneScript.Web.Server/RequestBslProcess.cs
@@ -0,0 +1,59 @@
+/*----------------------------------------------------------
+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 OneScript.Execution;
+using OneScript.StandardLibrary.Threads;
+
+namespace OneScript.Web.Server
+{
+ ///
+ /// Scoped-сервис, хранящий bsl-процесс, который обслуживает текущий запрос.
+ ///
+ /// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса,
+ /// включая обработчик исключений, видит один и тот же ИдентификаторПотокаИсполнения.
+ /// Процесс создаётся при первом обращении: запросы, не дошедшие до bsl-кода,
+ /// процесс не создают.
+ ///
+ /// Процесс намеренно не хранится в HttpContext.Items: Items доступны из bsl-кода
+ /// как Контекст.Данные и остаются полностью прикладными.
+ ///
+ /// Область сервисов запроса освобождается вместе с запросом, поэтому здесь же
+ /// заканчивается поток исполнения запроса и освобождаются его данные.
+ ///
+ internal sealed class RequestBslProcess : IDisposable
+ {
+ private readonly IBslProcessFactory _processFactory;
+ private readonly object _lock = new object();
+
+ private IBslProcess _process;
+
+ public RequestBslProcess(IBslProcessFactory processFactory)
+ {
+ _processFactory = processFactory;
+ }
+
+ public IBslProcess Process
+ {
+ get
+ {
+ if (_process != null)
+ return _process;
+
+ lock (_lock)
+ {
+ return _process ??= _processFactory.NewProcess();
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ // Процесс создаётся лениво, поэтому ради освобождения его создавать не нужно
+ ExecutionThreadContext.Release(_process);
+ }
+ }
+}
diff --git a/src/OneScript.Web.Server/WebServer.cs b/src/OneScript.Web.Server/WebServer.cs
index 0dbe15af5..8ee683214 100644
--- a/src/OneScript.Web.Server/WebServer.cs
+++ b/src/OneScript.Web.Server/WebServer.cs
@@ -104,6 +104,9 @@ private void ConfigureApp()
builder.Services.Configure(builder.Configuration.GetSection("FormOptions"));
+ builder.Services.AddScoped(_ =>
+ new RequestBslProcess(_executionContext.Services.Resolve()));
+
_app = builder.Build();
if (_useStaticFiles)
@@ -121,13 +124,6 @@ private void ConfigureApp()
if (_useWebSockets)
_app.UseWebSockets();
- _app.Use((context, next) =>
- {
- var process = _executionContext.Services.Resolve().NewProcess();
- context.Items.Add(typeof(IBslProcess), process);
- return next();
- });
-
_middlewares.ForEach(middleware =>
{
_app.Use((context, next) =>
@@ -138,7 +134,7 @@ private void ConfigureApp()
new RequestDelegateWrapper(next)
};
- var process = (IBslProcess)context.Items[typeof(IBslProcess)];
+ var process = GetRequestProcess(context);
var methodNumber = middleware.Target.GetMethodNumber(middleware.MethodName);
middleware.Target.CallAsProcedure(methodNumber, args, process);
@@ -178,7 +174,10 @@ private void UseBslExceptionHandler()
var methodNumber = _exceptionHandler?.Target.GetMethodNumber(_exceptionHandler?.MethodName)
?? throw new InvalidOperationException();
- var process = _executionContext.Services.Resolve().NewProcess();
+ // UseExceptionHandler переиспользует область сервисов запроса, поэтому
+ // обработчик исключений получает тот же процесс, что и упавший обработчик
+ // запроса, и видит контекст исполнения, в котором возникла ошибка.
+ var process = GetRequestProcess(context);
try
{
@@ -197,6 +196,14 @@ private void UseBslExceptionHandler()
});
}
+ ///
+ /// Возвращает bsl-процесс, обслуживающий текущий запрос.
+ ///
+ private static IBslProcess GetRequestProcess(HttpContext context)
+ {
+ return context.RequestServices.GetRequiredService().Process;
+ }
+
private static void WriteExceptionToResponse(HttpContext httpContext, Exception ex)
{
httpContext.Response.StatusCode = 500;
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.HostedScript/Process.cs b/src/ScriptEngine.HostedScript/Process.cs
index 39a36d266..fa850f7cb 100644
--- a/src/ScriptEngine.HostedScript/Process.cs
+++ b/src/ScriptEngine.HostedScript/Process.cs
@@ -6,6 +6,7 @@ This Source Code Form is subject to the terms of the
----------------------------------------------------------*/
using System;
using OneScript.Execution;
+using OneScript.StandardLibrary.Threads;
using ScriptEngine.Machine;
namespace ScriptEngine.HostedScript
@@ -50,6 +51,10 @@ public int Start()
}
finally
{
+ // Поток исполнения основного скрипта закончился здесь. Освобождается до
+ // остановки движка: обработчику ПриЗавершении ещё нужно исполнять bsl-код.
+ ExecutionThreadContext.Release(_bslProcess);
+
_engine.Debugger.NotifyProcessExit(exitCode);
_engine.Dispose();
_engine = null;
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/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/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", Метка,
+ "Обработчик должен видеть данные потока основного скрипта до их очистки");
+
+ Исключение
+ УдалитьФайлы(ФайлСкрипта);
+ ВызватьИсключение;
+ КонецПопытки;
+
+ УдалитьФайлы(ФайлСкрипта);
+ Если Новый Файл(ФайлМетки).Существует() Тогда
+ УдалитьФайлы(ФайлМетки);
+ КонецЕсли;
+
+КонецПроцедуры
+
Процедура ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов() Экспорт
Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураСИсключением");
diff --git a/tests/webserver-thread.os b/tests/webserver-thread.os
new file mode 100644
index 000000000..c39f09675
--- /dev/null
+++ b/tests/webserver-thread.os
@@ -0,0 +1,151 @@
+///////////////////////////////////////////////////////////////////////
+//
+// Поток исполнения при обработке запросов веб-сервером.
+//
+// Каждый запрос обслуживается собственным потоком исполнения. Проверяется, что обработчик
+// исключений работает в потоке упавшего обработчика запроса и видит его данные, а по окончании
+// обработки запроса поток завершается с вызовом ПриЗавершении.
+//
+///////////////////////////////////////////////////////////////////////
+
+Перем юТест;
+
+// Число - Порт тестового веб-сервера.
+Перем ПортТестовогоСервера;
+// Число - Сколько раз сработал обработчик завершения потока запроса.
+Перем ЗавершенийЗафиксировано;
+// ВебСервер - Тестовый сервер. Виден фоновому заданию, которое его поднимает.
+Перем Сервер;
+
+Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт
+
+ юТест = ЮнитТестирование;
+
+ ВсеТесты = Новый Массив;
+ ВсеТесты.Добавить("ТестДолжен_ОбработатьИсключениеВПотокеЗапросаИЗавершитьЕго");
+
+ Возврат ВсеТесты;
+
+КонецФункции
+
+// Обработчик завершения потока запроса.
+//
+Процедура ПриЗавершенииПотокаЗапроса() Экспорт
+ ЗавершенийЗафиксировано = ЗавершенийЗафиксировано + 1;
+КонецПроцедуры
+
+Процедура ОбработчикЗапроса(Контекст, СледующийОбработчик) Экспорт
+
+ Если Контекст.Запрос.Путь = "/ping" Тогда
+ Контекст.Ответ.Записать("pong");
+ Возврат;
+ КонецЕсли;
+
+ ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаЗапроса;
+
+ ТекущийПоток().Данные.Вставить("метка", "метка-" + Контекст.ИдентификаторТрассировки);
+
+ ВызватьИсключение "Падаем намеренно";
+
+КонецПроцедуры
+
+Процедура ОбработчикИсключений(Контекст) Экспорт
+
+ Контекст.Ответ.КодСостояния = 500;
+ // Значение положил обработчик запроса. Виден оно здесь только если обработчик исключений
+ // работает в том же потоке исполнения.
+ Контекст.Ответ.Записать("" + ТекущийПоток().Данные.Получить("метка"));
+
+КонецПроцедуры
+
+Процедура Вебсервер() Экспорт
+
+ Сервер = Новый ВебСервер(ПортТестовогоСервера);
+ Сервер.ДобавитьОбработчикЗапросов(ЭтотОбъект, "ОбработчикЗапроса");
+ Сервер.ДобавитьОбработчикИсключений(ЭтотОбъект, "ОбработчикИсключений");
+ Сервер.Запустить();
+
+КонецПроцедуры
+
+// Дожидается готовности сервера опросом, а не фиксированной паузой: на нагруженной машине
+// подъём занимает непредсказуемое время, и пауза наугад делает тест нестабильным.
+//
+Функция ДождатьсяГотовностиСервера(Знач Соединение, Знач ПределОжидания = 30000)
+
+ Начало = ТекущаяУниверсальнаяДатаВМиллисекундах();
+
+ Пока ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало < ПределОжидания Цикл
+ Попытка
+ Ответ = Соединение.Получить(Новый HTTPЗапрос("/ping"));
+ Если Ответ.КодСостояния = 200 Тогда
+ Возврат Истина;
+ КонецЕсли;
+ Исключение
+ // Сервер ещё не поднялся - пробуем снова
+ КонецПопытки;
+ Приостановить(50);
+ КонецЦикла;
+
+ Возврат Ложь;
+
+КонецФункции
+
+// Дожидается, пока обработчик завершения потока отработает: область сервисов запроса
+// освобождается уже после отправки ответа, поэтому счётчик растёт чуть позже ответа.
+//
+Функция ДождатьсяЗавершенийПотоков(Знач Ожидается, Знач ПределОжидания = 30000)
+
+ Начало = ТекущаяУниверсальнаяДатаВМиллисекундах();
+
+ Пока ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало < ПределОжидания Цикл
+ Если ЗавершенийЗафиксировано >= Ожидается Тогда
+ Возврат Истина;
+ КонецЕсли;
+ Приостановить(50);
+ КонецЦикла;
+
+ Возврат Ложь;
+
+КонецФункции
+
+Процедура ТестДолжен_ОбработатьИсключениеВПотокеЗапросаИЗавершитьЕго() Экспорт
+
+ ПортТестовогоСервера = 8182;
+ ЗавершенийЗафиксировано = 0;
+
+ МенеджерФоновыхЗаданий = Новый МенеджерФоновыхЗаданий;
+ МенеджерФоновыхЗаданий.Выполнить(ЭтотОбъект, "Вебсервер");
+
+ Попытка
+
+ Соединение = Новый HTTPСоединение("http://127.0.0.1:" + ПортТестовогоСервера);
+
+ юТест.ПроверитьРавенство(Истина, ДождатьсяГотовностиСервера(Соединение),
+ "Тестовый веб-сервер должен подняться");
+
+ // Проверки готовности тоже создают потоки исполнения - считаем только целевой запрос
+ ЗавершенийЗафиксировано = 0;
+
+ Ответ = Соединение.Получить(Новый HTTPЗапрос("/"));
+
+ юТест.ПроверитьРавенство(500, Ответ.КодСостояния,
+ "Обработчик исключений должен отработать");
+
+ ТелоОтвета = Ответ.ПолучитьТелоКакСтроку();
+ юТест.ПроверитьРавенство(Истина, СтрНачинаетсяС(ТелоОтвета, "метка-"),
+ "Обработчик исключений должен видеть данные потока упавшего обработчика запроса,"
+ + " получено: " + ТелоОтвета);
+
+ юТест.ПроверитьРавенство(Истина, ДождатьсяЗавершенийПотоков(1),
+ "По окончании обработки запроса должен сработать обработчик ПриЗавершении");
+
+ Исключение
+ Сервер.Остановить();
+ МенеджерФоновыхЗаданий.Очистить();
+ ВызватьИсключение;
+ КонецПопытки;
+
+ Сервер.Остановить();
+ МенеджерФоновыхЗаданий.Очистить();
+
+КонецПроцедуры