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), + "По окончании обработки запроса должен сработать обработчик ПриЗавершении"); + + Исключение + Сервер.Остановить(); + МенеджерФоновыхЗаданий.Очистить(); + ВызватьИсключение; + КонецПопытки; + + Сервер.Остановить(); + МенеджерФоновыхЗаданий.Очистить(); + +КонецПроцедуры