From 6c2da665d3732f40ad9b56302199e0797323cc1a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 06:31:20 +0000 Subject: [PATCH 01/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=98=D0=B4=D0=B5=D0=BD=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=9F=D0=BE=D1=82=D0=BE=D0=BA=D0=B0=D0=98?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Библиотекам, которым нужно хранить данные в разрезе единицы исполнения (аналог thread-local хранилища), до сих пор приходилось использовать ФоновыеЗадания.ПолучитьТекущее(). Внутри обработчика запроса веб-сервера этот способ не работает: фоновое задание там отсутствует, метод возвращает Неопределено, и все одновременно обрабатываемые запросы получают один и тот же ключ, совпадающий с ключом основного потока. Движок уже присваивает каждой единице исполнения уникальный IBslProcess.VirtualThreadId: отдельный процесс создаётся для основного скрипта, для каждого фонового задания и для каждого запроса веб-сервера. Значение просто не было доступно из BSL. Функция возвращает этот идентификатор, получая процесс через штатную инъекцию IBslProcess первым параметром контекстного метода, поэтому работает и в стековой машине, и в нативном компиляторе. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N2E2kuK7qYqo2tNr8agbM7 --- .../StandardGlobalContext.cs | 22 +++++++++++++ tests/tasks.os | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index f12754f12..23a9f2897 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("ИдентификаторПотокаИсполнения", "ExecutionThreadId")] + public int ExecutionThreadId(IBslProcess process) + { + return process.VirtualThreadId; + } + /// /// Приостанавливает выполнение скрипта. /// diff --git a/tests/tasks.os b/tests/tasks.os index 99f79d9b2..cb5252961 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -24,6 +24,7 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоВозвращаетсяРезультатДелегата"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоРаботаетБлокировка"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоКодМожетОпределитьИДЗадания"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьУникальностьИдентификатораПотокаИсполнения"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных"); @@ -276,6 +277,38 @@ КонецПроцедуры +Функция ВернутьИдентификаторПотокаИсполнения() Экспорт + + Приостановить(500); + Возврат ИдентификаторПотокаИсполнения(); + +КонецФункции + +Процедура ТестДолжен_ПроверитьУникальностьИдентификатораПотокаИсполнения() Экспорт + + ИдОсновногоПотока = ИдентификаторПотокаИсполнения(); + юТест.ПроверитьРавенство(ИдОсновногоПотока, ИдентификаторПотокаИсполнения(), + "Идентификатор потока исполнения должен быть одинаковым при повторном вызове"); + + МассивЗаданий = Новый Массив; + Для Сч = 1 По 4 Цикл + МассивЗаданий.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВернутьИдентификаторПотокаИсполнения")); + КонецЦикла; + + ФоновыеЗадания.ОжидатьВсе(МассивЗаданий); + + УникальныеИдентификаторы = Новый Соответствие; + Для Каждого Задание Из МассивЗаданий Цикл + юТест.ПроверитьНеРавенство(ИдОсновногоПотока, Задание.Результат, + "Фоновое задание должно получить собственный идентификатор потока исполнения"); + УникальныеИдентификаторы.Вставить(Задание.Результат, Истина); + КонецЦикла; + + юТест.ПроверитьРавенство(МассивЗаданий.Количество(), УникальныеИдентификаторы.Количество(), + "Идентификаторы потоков исполнения одновременных фоновых заданий должны различаться"); + +КонецПроцедуры + Процедура ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов() Экспорт Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураСИсключением"); From 3b5d11649f3c02a6be4ba991cb4a588f2d5bcac3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 06:39:08 +0000 Subject: [PATCH 02/13] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2=D0=B5=D0=B1-=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=B2=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5?= =?UTF-8?q?=D1=81=D1=81=D0=B5=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UseBslExceptionHandler создавал собственный bsl-процесс вместо того, чтобы взять процесс запроса из HttpContext.Items, куда его кладёт middleware конвейера. Из-за этого обработчик исключений выполнялся в другой единице исполнения, чем упавший обработчик запроса, и не видел её ИдентификаторПотокаИсполнения. Процесс запроса теперь берётся из HttpContext.Items, а собственный создаётся только если исключение возникло раньше, чем процесс запроса (например, в middleware статических файлов). Получение процесса вынесено в GetOrCreateProcess и стало идемпотентным. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N2E2kuK7qYqo2tNr8agbM7 --- src/OneScript.Web.Server/WebServer.cs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/OneScript.Web.Server/WebServer.cs b/src/OneScript.Web.Server/WebServer.cs index 0dbe15af5..05c9bf770 100644 --- a/src/OneScript.Web.Server/WebServer.cs +++ b/src/OneScript.Web.Server/WebServer.cs @@ -123,8 +123,7 @@ private void ConfigureApp() _app.Use((context, next) => { - var process = _executionContext.Services.Resolve().NewProcess(); - context.Items.Add(typeof(IBslProcess), process); + GetOrCreateProcess(context); return next(); }); @@ -178,7 +177,11 @@ private void UseBslExceptionHandler() var methodNumber = _exceptionHandler?.Target.GetMethodNumber(_exceptionHandler?.MethodName) ?? throw new InvalidOperationException(); - var process = _executionContext.Services.Resolve().NewProcess(); + // Обработчик исключений работает в том же процессе, что и обработчик запроса, + // поэтому видит контекст исполнения, в котором возникла ошибка. + // Собственный процесс создаётся только если исключение возникло до того, + // как процесс запроса был создан (например, в middleware статических файлов). + var process = GetOrCreateProcess(context); try { @@ -197,6 +200,22 @@ private void UseBslExceptionHandler() }); } + /// + /// Возвращает bsl-процесс, обслуживающий текущий запрос, создавая его при первом обращении. + /// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса + /// видит один и тот же ИдентификаторПотокаИсполнения. + /// + private IBslProcess GetOrCreateProcess(HttpContext context) + { + if (context.Items.TryGetValue(typeof(IBslProcess), out var stored) && stored is IBslProcess existing) + return existing; + + var process = _executionContext.Services.Resolve().NewProcess(); + context.Items[typeof(IBslProcess)] = process; + + return process; + } + private static void WriteExceptionToResponse(HttpContext httpContext, Exception ex) { httpContext.Response.StatusCode = 500; From 3cb707cb4fc0664b3feaf9f61b1008b9b3278be9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 07:04:59 +0000 Subject: [PATCH 03/13] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=86=D0=B5=D1=81?= =?UTF-8?q?=D1=81=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D1=85?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=82=D1=81=D1=8F=20=D0=B2=20HttpConte?= =?UTF-8?q?xt.Features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По итогам ревью PR #1725. HttpContext.Items доступны из bsl как Контекст.Данные — это живая обёртка над тем же словарём (HttpContextWrapper.Data), поэтому обработчик запроса мог удалить оттуда процесс, которым сам же и исполняется. После Контекст.Данные.Очистить() обработчик исключений не находил процесс запроса и создавал новый, то есть работал в другой единице исполнения. Features из bsl не видны, что снимает проблему в корне. Заодно цикл middleware получает процесс через GetOrCreateProcess вместо сырого приведения значения из Items. Исправлен комментарий про запасное создание процесса: UseStaticFiles регистрируется раньше UseExceptionHandler, поэтому исключение из middleware статических файлов до обработчика не доходит. Между UseExceptionHandler и middleware процесса стоит UseWebSockets — вот оттуда исключение придёт при ещё не созданном процессе. Уточнена документация ИдентификаторПотокаИсполнения: счётчик процессов имеет тип Int32 и после исчерпания диапазона начинает выдавать значения заново, поэтому обещание "не переиспользуются" заменено описанием реальной границы и требованием освобождать записи завершившихся единиц исполнения. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N2E2kuK7qYqo2tNr8agbM7 --- .../StandardGlobalContext.cs | 6 +++++- src/OneScript.Web.Server/WebServer.cs | 16 +++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index 23a9f2897..392f91746 100644 --- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs +++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs @@ -80,7 +80,11 @@ public void RunGarbageCollection() /// /// Каждая независимая единица исполнения bsl-кода получает собственный идентификатор: /// основной скрипт, каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. - /// Идентификаторы уникальны в пределах запущенного движка и не переиспользуются. + /// Идентификаторы выдаются последовательно, начиная с единицы, и в пределах запущенного + /// движка не повторяются, пока счётчик не исчерпает диапазон Int32. После этого значения + /// начинают выдаваться заново и могут совпасть с идентификатором долгоживущей единицы + /// исполнения, поэтому хранилища, живущие всё время работы движка, должны освобождать + /// записи завершившихся единиц исполнения, а не накапливать их. /// /// Метод предназначен для библиотек, которым нужно хранить данные в разрезе единицы /// исполнения (аналог thread-local хранилища). В отличие от идентификатора фонового задания, diff --git a/src/OneScript.Web.Server/WebServer.cs b/src/OneScript.Web.Server/WebServer.cs index 05c9bf770..fff0fd461 100644 --- a/src/OneScript.Web.Server/WebServer.cs +++ b/src/OneScript.Web.Server/WebServer.cs @@ -137,7 +137,7 @@ private void ConfigureApp() new RequestDelegateWrapper(next) }; - var process = (IBslProcess)context.Items[typeof(IBslProcess)]; + var process = GetOrCreateProcess(context); var methodNumber = middleware.Target.GetMethodNumber(middleware.MethodName); middleware.Target.CallAsProcedure(methodNumber, args, process); @@ -179,8 +179,9 @@ private void UseBslExceptionHandler() // Обработчик исключений работает в том же процессе, что и обработчик запроса, // поэтому видит контекст исполнения, в котором возникла ошибка. - // Собственный процесс создаётся только если исключение возникло до того, - // как процесс запроса был создан (например, в middleware статических файлов). + // Собственный процесс создаётся только если исключение возникло раньше, чем + // процесс запроса: между UseExceptionHandler и middleware процесса стоит + // UseWebSockets. var process = GetOrCreateProcess(context); try @@ -204,14 +205,19 @@ private void UseBslExceptionHandler() /// Возвращает bsl-процесс, обслуживающий текущий запрос, создавая его при первом обращении. /// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса /// видит один и тот же ИдентификаторПотокаИсполнения. + /// + /// Процесс хранится в HttpContext.Features, а не в HttpContext.Items: Items доступны + /// из bsl-кода как Контекст.Данные, и обработчик запроса мог бы удалить оттуда процесс, + /// которым сам же и исполняется. /// private IBslProcess GetOrCreateProcess(HttpContext context) { - if (context.Items.TryGetValue(typeof(IBslProcess), out var stored) && stored is IBslProcess existing) + var existing = context.Features.Get(); + if (existing != null) return existing; var process = _executionContext.Services.Resolve().NewProcess(); - context.Items[typeof(IBslProcess)] = process; + context.Features.Set(process); return process; } From d633efeb83abd5caf6b81cb964c254c0d9dc8512 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 07:19:32 +0000 Subject: [PATCH 04/13] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=86=D0=B5=D1=81?= =?UTF-8?q?=D1=81=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B4=D0=B0=D1=91=D1=82=D1=81=D1=8F=20scoped-=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По замечанию мэйнтейнера в PR #1725: словарь запроса должен остаться полностью прикладным, а получением или созданием процесса должен заниматься scoped-сервис. RequestBslProcess регистрируется в контейнере веб-приложения как scoped и создаёт процесс при первом обращении. Область сервисов запроса живёт ровно столько же, сколько запрос, и переиспользуется UseExceptionHandler, поэтому обработчик исключений получает тот же процесс, что и упавший обработчик запроса. HttpContext.Items и HttpContext.Features для передачи процесса больше не используются. Middleware, создававшее процесс заранее, удалено: запросы, не дошедшие до bsl-кода, процесс теперь не создают. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N2E2kuK7qYqo2tNr8agbM7 --- src/OneScript.Web.Server/RequestBslProcess.cs | 48 +++++++++++++++++++ src/OneScript.Web.Server/WebServer.cs | 40 +++++----------- 2 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 src/OneScript.Web.Server/RequestBslProcess.cs diff --git a/src/OneScript.Web.Server/RequestBslProcess.cs b/src/OneScript.Web.Server/RequestBslProcess.cs new file mode 100644 index 000000000..ff6367950 --- /dev/null +++ b/src/OneScript.Web.Server/RequestBslProcess.cs @@ -0,0 +1,48 @@ +/*---------------------------------------------------------- +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 OneScript.Execution; + +namespace OneScript.Web.Server +{ + /// + /// Scoped-сервис, хранящий bsl-процесс, который обслуживает текущий запрос. + /// + /// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса, + /// включая обработчик исключений, видит один и тот же ИдентификаторПотокаИсполнения. + /// Процесс создаётся при первом обращении: запросы, не дошедшие до bsl-кода, + /// процесс не создают. + /// + /// Процесс намеренно не хранится в HttpContext.Items: Items доступны из bsl-кода + /// как Контекст.Данные и остаются полностью прикладными. + /// + internal sealed class RequestBslProcess + { + 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(); + } + } + } + } +} diff --git a/src/OneScript.Web.Server/WebServer.cs b/src/OneScript.Web.Server/WebServer.cs index fff0fd461..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,12 +124,6 @@ private void ConfigureApp() if (_useWebSockets) _app.UseWebSockets(); - _app.Use((context, next) => - { - GetOrCreateProcess(context); - return next(); - }); - _middlewares.ForEach(middleware => { _app.Use((context, next) => @@ -137,7 +134,7 @@ private void ConfigureApp() new RequestDelegateWrapper(next) }; - var process = GetOrCreateProcess(context); + var process = GetRequestProcess(context); var methodNumber = middleware.Target.GetMethodNumber(middleware.MethodName); middleware.Target.CallAsProcedure(methodNumber, args, process); @@ -177,12 +174,10 @@ private void UseBslExceptionHandler() var methodNumber = _exceptionHandler?.Target.GetMethodNumber(_exceptionHandler?.MethodName) ?? throw new InvalidOperationException(); - // Обработчик исключений работает в том же процессе, что и обработчик запроса, - // поэтому видит контекст исполнения, в котором возникла ошибка. - // Собственный процесс создаётся только если исключение возникло раньше, чем - // процесс запроса: между UseExceptionHandler и middleware процесса стоит - // UseWebSockets. - var process = GetOrCreateProcess(context); + // UseExceptionHandler переиспользует область сервисов запроса, поэтому + // обработчик исключений получает тот же процесс, что и упавший обработчик + // запроса, и видит контекст исполнения, в котором возникла ошибка. + var process = GetRequestProcess(context); try { @@ -202,24 +197,11 @@ private void UseBslExceptionHandler() } /// - /// Возвращает bsl-процесс, обслуживающий текущий запрос, создавая его при первом обращении. - /// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса - /// видит один и тот же ИдентификаторПотокаИсполнения. - /// - /// Процесс хранится в HttpContext.Features, а не в HttpContext.Items: Items доступны - /// из bsl-кода как Контекст.Данные, и обработчик запроса мог бы удалить оттуда процесс, - /// которым сам же и исполняется. + /// Возвращает bsl-процесс, обслуживающий текущий запрос. /// - private IBslProcess GetOrCreateProcess(HttpContext context) + private static IBslProcess GetRequestProcess(HttpContext context) { - var existing = context.Features.Get(); - if (existing != null) - return existing; - - var process = _executionContext.Services.Resolve().NewProcess(); - context.Features.Set(process); - - return process; + return context.RequestServices.GetRequiredService().Process; } private static void WriteExceptionToResponse(HttpContext httpContext, Exception ex) From e9cadbc178f565610b640e6588bcbbcacc931597 Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Tue, 25 Aug 2026 10:09:24 +0200 Subject: [PATCH 05/13] =?UTF-8?q?=D0=A2=D0=B5=D0=BA=D1=83=D1=89=D0=B8?= =?UTF-8?q?=D0=B9=D0=9F=D0=BE=D1=82=D0=BE=D0=BA()=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20=D0=98=D0=B4=D0=B5=D0=BD=D1=82=D0=B8=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=9F=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=98=D1=81=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По предложению мэйнтейнера в PR #1725: вместо идентификатора процесса отдавать наружу сам поток исполнения, у которого есть соответствие, работающее как набор thread-local переменных и принудительно освобождаемое вместе со всеми элементами в конце процесса. Добавлен класс ПотокИсполнения со свойствами Идентификатор и Данные. Экземпляр привязан к bsl-процессу через ConditionalWeakTable, поэтому ТекущийПоток() в пределах одной единицы исполнения всегда возвращает один и тот же объект, а запись исчезает вместе с процессом. Владельцы процесса завершают поток исполнения явно: менеджер фоновых заданий по завершении задания, веб-сервер - по окончании обработки запроса, через Dispose scoped-сервиса RequestBslProcess. При завершении соответствие очищается, а значения, поддерживающие IDisposable, освобождаются. Такой подход снимает и замечание про переполнение счётчика: ключом для хранения состояния служит сам объект потока, а не число. Глобальная функция ИдентификаторПотокаИсполнения() удалена, идентификатор доступен как ТекущийПоток().Идентификатор. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014ZfXJeyPJxZxtooYQzTt7s --- .../StandardGlobalContext.cs | 31 +++--- .../Tasks/BackgroundTasksManager.cs | 13 ++- .../Threads/ExecutionThreadContext.cs | 98 +++++++++++++++++++ src/OneScript.Web.Server/RequestBslProcess.cs | 15 ++- tests/tasks.os | 96 ++++++++++++++++-- 5 files changed, 225 insertions(+), 28 deletions(-) create mode 100644 src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index 392f91746..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; @@ -76,29 +77,25 @@ public void RunGarbageCollection() } /// - /// Возвращает идентификатор потока исполнения, в котором выполняется текущий код. + /// Возвращает поток исполнения, в котором выполняется текущий код. /// - /// Каждая независимая единица исполнения bsl-кода получает собственный идентификатор: + /// Отдельным потоком исполнения является каждая независимая единица исполнения bsl-кода: /// основной скрипт, каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. - /// Идентификаторы выдаются последовательно, начиная с единицы, и в пределах запущенного - /// движка не повторяются, пока счётчик не исчерпает диапазон Int32. После этого значения - /// начинают выдаваться заново и могут совпасть с идентификатором долгоживущей единицы - /// исполнения, поэтому хранилища, живущие всё время работы движка, должны освобождать - /// записи завершившихся единиц исполнения, а не накапливать их. + /// Для одной единицы исполнения всегда возвращается один и тот же объект, поэтому его + /// свойство Данные можно использовать как набор thread-local переменных. /// - /// Метод предназначен для библиотек, которым нужно хранить данные в разрезе единицы - /// исполнения (аналог thread-local хранилища). В отличие от идентификатора фонового задания, - /// значение определено во всех контекстах, в том числе при обработке запросов веб-сервера, - /// где фоновое задание отсутствует. + /// В отличие от идентификатора фонового задания, поток исполнения определён во всех + /// контекстах, в том числе при обработке запросов веб-сервера, где фоновое задание + /// отсутствует. /// - /// Идентификатор не наследуется: фоновое задание, запущенное из текущего потока исполнения, - /// получит собственное значение. + /// Поток исполнения не наследуется: фоновое задание, запущенное из текущего потока + /// исполнения, получит собственный поток с собственными данными. /// - /// Число. Идентификатор текущего потока исполнения. - [ContextMethod("ИдентификаторПотокаИсполнения", "ExecutionThreadId")] - public int ExecutionThreadId(IBslProcess process) + /// ПотокИсполнения. Поток исполнения текущего кода. + [ContextMethod("ТекущийПоток", "CurrentThread")] + public ExecutionThreadContext CurrentThread(IBslProcess process) { - return process.VirtualThreadId; + 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/Threads/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs new file mode 100644 index 000000000..311047ecf --- /dev/null +++ b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs @@ -0,0 +1,98 @@ +/*---------------------------------------------------------- +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.Runtime.CompilerServices; +using OneScript.Contexts; +using OneScript.Execution; +using OneScript.StandardLibrary.Collections; +using ScriptEngine.Machine.Contexts; + +namespace OneScript.StandardLibrary.Threads +{ + /// + /// Поток исполнения bsl-кода. + /// + /// Потоком исполнения является каждая независимая единица исполнения: основной скрипт, + /// каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. Получить поток + /// исполнения текущего кода можно функцией ТекущийПоток(). + /// + /// Свойство Данные представляет собой хранилище, живущее ровно столько же, сколько живёт + /// сам поток исполнения. Оно предназначено для библиотек, которым нужно хранить состояние + /// в разрезе единицы исполнения (аналог thread-local хранилища). + /// + [ContextClass("ПотокИсполнения", "ExecutionThread")] + public sealed class ExecutionThreadContext : AutoContext, IDisposable + { + 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; + + Threads.Remove(process); + thread.Dispose(); + } + + public void Dispose() + { + foreach (var item in Data) + { + (item.Value as IDisposable)?.Dispose(); + } + + Data.Clear(); + } + } +} diff --git a/src/OneScript.Web.Server/RequestBslProcess.cs b/src/OneScript.Web.Server/RequestBslProcess.cs index ff6367950..35d364a03 100644 --- a/src/OneScript.Web.Server/RequestBslProcess.cs +++ b/src/OneScript.Web.Server/RequestBslProcess.cs @@ -1,10 +1,12 @@ -/*---------------------------------------------------------- +/*---------------------------------------------------------- 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 { @@ -18,8 +20,11 @@ namespace OneScript.Web.Server /// /// Процесс намеренно не хранится в HttpContext.Items: Items доступны из bsl-кода /// как Контекст.Данные и остаются полностью прикладными. + /// + /// Область сервисов запроса освобождается вместе с запросом, поэтому здесь же + /// заканчивается поток исполнения запроса и освобождаются его данные. /// - internal sealed class RequestBslProcess + internal sealed class RequestBslProcess : IDisposable { private readonly IBslProcessFactory _processFactory; private readonly object _lock = new object(); @@ -44,5 +49,11 @@ public IBslProcess Process } } } + + public void Dispose() + { + // Процесс создаётся лениво, поэтому ради освобождения его создавать не нужно + ExecutionThreadContext.Release(_process); + } } } diff --git a/tests/tasks.os b/tests/tasks.os index cb5252961..a32acb377 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -24,7 +24,9 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоВозвращаетсяРезультатДелегата"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоРаботаетБлокировка"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоКодМожетОпределитьИДЗадания"); - ВсеТесты.Добавить("ТестДолжен_ПроверитьУникальностьИдентификатораПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьУникальностьПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьИзоляциюДанныхПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных"); @@ -280,14 +282,14 @@ Функция ВернутьИдентификаторПотокаИсполнения() Экспорт Приостановить(500); - Возврат ИдентификаторПотокаИсполнения(); + Возврат ТекущийПоток().Идентификатор; КонецФункции -Процедура ТестДолжен_ПроверитьУникальностьИдентификатораПотокаИсполнения() Экспорт +Процедура ТестДолжен_ПроверитьУникальностьПотокаИсполнения() Экспорт - ИдОсновногоПотока = ИдентификаторПотокаИсполнения(); - юТест.ПроверитьРавенство(ИдОсновногоПотока, ИдентификаторПотокаИсполнения(), + ИдОсновногоПотока = ТекущийПоток().Идентификатор; + юТест.ПроверитьРавенство(ИдОсновногоПотока, ТекущийПоток().Идентификатор, "Идентификатор потока исполнения должен быть одинаковым при повторном вызове"); МассивЗаданий = Новый Массив; @@ -300,12 +302,92 @@ УникальныеИдентификаторы = Новый Соответствие; Для Каждого Задание Из МассивЗаданий Цикл юТест.ПроверитьНеРавенство(ИдОсновногоПотока, Задание.Результат, - "Фоновое задание должно получить собственный идентификатор потока исполнения"); + "Фоновое задание должно получить собственный поток исполнения"); УникальныеИдентификаторы.Вставить(Задание.Результат, Истина); КонецЦикла; юТест.ПроверитьРавенство(МассивЗаданий.Количество(), УникальныеИдентификаторы.Количество(), - "Идентификаторы потоков исполнения одновременных фоновых заданий должны различаться"); + "Идентификаторы одновременных фоновых заданий должны различаться"); + +КонецПроцедуры + +Функция ЗаписатьВДанныеПотокаИЗабрать(Знач Метка) Экспорт + + ТекущийПоток().Данные.Вставить("метка", Метка); + Приостановить(500); + + // Данные основного потока не должны быть видны, а соседние задания не должны затирать наши + Результат = Новый Структура("Метка, ЕстьЧужое"); + Результат.Метка = ТекущийПоток().Данные.Получить("метка"); + Результат.ЕстьЧужое = (ТекущийПоток().Данные.Получить("изОсновногоПотока") <> Неопределено); + + Возврат Результат; + +КонецФункции + +Процедура ТестДолжен_ПроверитьИзоляциюДанныхПотокаИсполнения() Экспорт + + ТекущийПоток().Данные.Вставить("изОсновногоПотока", Истина); + + юТест.ПроверитьРавенство(Истина, ТекущийПоток().Данные.Получить("изОсновногоПотока"), + "ТекущийПоток() должен возвращать один и тот же экземпляр в пределах потока исполнения"); + + МассивЗаданий = Новый Массив; + Для Сч = 1 По 4 Цикл + Параметры = Новый Массив; + Параметры.Добавить(Сч); + МассивЗаданий.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗаписатьВДанныеПотокаИЗабрать", Параметры)); + КонецЦикла; + + ФоновыеЗадания.ОжидатьВсе(МассивЗаданий); + + Для Сч = 1 По МассивЗаданий.Количество() Цикл + Результат = МассивЗаданий[Сч - 1].Результат; + юТест.ПроверитьРавенство(Сч, Результат.Метка, + "Данные потока исполнения не должны затираться соседними фоновыми заданиями"); + юТест.ПроверитьРавенство(Ложь, Результат.ЕстьЧужое, + "Фоновое задание не должно видеть данные основного потока исполнения"); + КонецЦикла; + + ТекущийПоток().Данные.Удалить("изОсновногоПотока"); + +КонецПроцедуры + +Функция ПоложитьВДанныеПотокаОсвобождаемыйОбъект() Экспорт + + ИмяФайла = ПолучитьИмяВременногоФайла("tmp"); + ФайловыйПоток = Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + + ТекущийПоток().Данные.Вставить("файл", ФайловыйПоток); + + // Возвращаем и сам поток исполнения, и объект - чтобы проверить их состояние после завершения задания + Возврат Новый Структура("ПотокИсполнения, ФайловыйПоток, ИмяФайла", + ТекущийПоток(), ФайловыйПоток, ИмяФайла); + +КонецФункции + +Процедура ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПоложитьВДанныеПотокаОсвобождаемыйОбъект"); + Задание.ОжидатьЗавершения(); + + Результат = Задание.Результат; + юТест.ПроверитьНеРавенство(Неопределено, Результат, "Задание должно вернуть результат"); + + юТест.ПроверитьРавенство(0, Результат.ПотокИсполнения.Данные.Количество(), + "Данные потока исполнения должны очищаться по завершении фонового задания"); + + ИсключениеБыло = Ложь; + Попытка + Результат.ФайловыйПоток.ТекущаяПозиция(); + Исключение + ИсключениеБыло = Истина; + КонецПопытки; + + юТест.ПроверитьРавенство(Истина, ИсключениеБыло, + "Значения данных потока исполнения должны принудительно освобождаться"); + + УдалитьФайлы(Результат.ИмяФайла); КонецПроцедуры From 1e5c1d0c54153f2f09fd146bb693f2be3742c8cd Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Tue, 25 Aug 2026 11:19:36 +0200 Subject: [PATCH 06/13] =?UTF-8?q?=D0=A1=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=D0=B0=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Освобождение данных потока добирается только до значений, реализующих IDisposable среды CLR. Библиотекам этого мало: соединение с БД или занятая в пуле запись - это BSL-объект, и узнать о конце единицы исполнения им было неоткуда, кроме опроса списка фоновых заданий, который не видит ни запросов веб-сервера, ни последствий ФоновыеЗадания.Очистить(). Теперь по завершении потока исполнения поднимается событие ПриЗавершении (оно же OnTermination), на которое подписываются штатным ДобавитьОбработчик: ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ВернутьСоединение; Событие поднимается до очистки данных, поэтому обработчик ещё видит всё, что поток в них положил. Ошибка обработчика наружу не выпускается: у фонового задания завершение идёт в блоке finally и затёрло бы исходную ошибку, у веб-сервера выполняется после отправки ответа. Реестр подписок DefaultEventProcessor удерживает источник до конца работы движка, а поток исполнения живёт лишь до конца своей единицы исполнения. Поэтому в IEventProcessor добавлен RemoveAllHandlers, снимающий подписки источника, и завершение потока его вызывает. Метод объявлен с пустой реализацией по умолчанию, чтобы не ломать сторонние процессоры событий. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014ZfXJeyPJxZxtooYQzTt7s --- .../Threads/ExecutionThreadContext.cs | 60 +++++++++++++ .../DefaultEventProcessor.cs | 8 ++ src/ScriptEngine/Machine/IEventProcessor.cs | 12 +++ .../EventProcessorTests.cs | 85 +++++++++++++++++++ .../OneScript.Core.Tests.csproj | 1 + tests/tasks.os | 62 ++++++++++++++ 6 files changed, 228 insertions(+) create mode 100644 src/Tests/OneScript.Core.Tests/EventProcessorTests.cs diff --git a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs index 311047ecf..ad27a8e65 100644 --- a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs +++ b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs @@ -10,6 +10,8 @@ This Source Code Form is subject to the terms of the using OneScript.Contexts; using OneScript.Execution; using OneScript.StandardLibrary.Collections; +using ScriptEngine; +using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; namespace OneScript.StandardLibrary.Threads @@ -24,10 +26,25 @@ namespace OneScript.StandardLibrary.Threads /// Свойство Данные представляет собой хранилище, живущее ровно столько же, сколько живёт /// сам поток исполнения. Оно предназначено для библиотек, которым нужно хранить состояние /// в разрезе единицы исполнения (аналог 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) @@ -81,10 +98,53 @@ public static void Release(IBslProcess process) 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() { foreach (var item in Data) 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/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 a32acb377..1e64e368b 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -5,6 +5,9 @@ Перем СобытиеВызвано; +Перем ЗавершениеПотокаЗафиксировано; +Перем ЭлементовВДанныхПриЗавершении; + //////////////////////////////////////////////////////////////////// // Программный интерфейс @@ -27,6 +30,8 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьУникальностьПотокаИсполнения"); ВсеТесты.Добавить("ТестДолжен_ПроверитьИзоляциюДанныхПотокаИсполнения"); ВсеТесты.Добавить("ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_ВызватьОбработчикЗавершенияПотокаИсполнения"); + ВсеТесты.Добавить("ТестДолжен_НеРонятьЗаданиеИзЗаОшибкиВОбработчикеЗавершения"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных"); @@ -391,6 +396,63 @@ КонецПроцедуры +Процедура ПриЗавершенииПотокаЗадания() Экспорт + + ЗавершениеПотокаЗафиксировано = Истина; + // Обработчик должен успевать увидеть данные потока до их очистки + ЭлементовВДанныхПриЗавершении = ТекущийПоток().Данные.Количество(); + +КонецПроцедуры + +Функция ПодписатьсяНаЗавершениеПотока() Экспорт + + ТекущийПоток().Данные.Вставить("ресурс", "значение"); + ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаЗадания; + Возврат ТекущийПоток(); + +КонецФункции + +Процедура ТестДолжен_ВызватьОбработчикЗавершенияПотокаИсполнения() Экспорт + + ЗавершениеПотокаЗафиксировано = Ложь; + ЭлементовВДанныхПриЗавершении = -1; + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПодписатьсяНаЗавершениеПотока"); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство(Истина, ЗавершениеПотокаЗафиксировано, + "Обработчик ПриЗавершении должен вызываться по завершении потока исполнения"); + + юТест.ПроверитьРавенство(1, ЭлементовВДанныхПриЗавершении, + "Обработчик должен видеть данные потока до их очистки"); + + юТест.ПроверитьРавенство(0, Задание.Результат.Данные.Количество(), + "После обработчика данные потока должны быть очищены"); + +КонецПроцедуры + +Процедура ПриЗавершенииПотокаСИсключением() Экспорт + ВызватьИсключение "Ошибка внутри обработчика завершения"; +КонецПроцедуры + +Функция ПодписатьсяНаПадающийОбработчик() Экспорт + ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаСИсключением; + Возврат "задание отработало"; +КонецФункции + +Процедура ТестДолжен_НеРонятьЗаданиеИзЗаОшибкиВОбработчикеЗавершения() Экспорт + + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПодписатьсяНаПадающийОбработчик"); + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьРавенство("задание отработало", Задание.Результат, + "Ошибка в обработчике завершения не должна ломать результат задания"); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, + "Ошибка в обработчике завершения не должна менять состояние задания"); + +КонецПроцедуры + Процедура ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов() Экспорт Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураСИсключением"); From e6df8647851a959396719d57b4d6fd501d3294aa Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Tue, 25 Aug 2026 14:56:25 +0200 Subject: [PATCH 07/13] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D1=80=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=D0=BC=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три пробела, найденные Copilot в PR #1725. Поток исполнения основного скрипта не завершался: Release вызывался только для фоновых заданий и запросов веб-сервера, а Process.Start лишь останавливал движок. Из-за этого у основного скрипта не наступало ПриЗавершении, данные не очищались, а значения с IDisposable не освобождались детерминированно. Теперь поток завершается в блоке finally основного процесса, до остановки движка - обработчику ещё нужно исполнять bsl-код. Освобождение данных не было изолировано по значениям: ошибка на первом же прерывала цикл, остальные значения оставались неосвобождёнными, карта не очищалась, а исключение выходило из блока finally фонового задания и превращало ОжидатьЗавершения в AggregateException. Теперь каждое значение освобождается независимо, ошибка уходит предупреждением в лог, а очистка карты идёт через finally. Гарантии веб-пути не были покрыты автотестом - проверялись только вручную. Добавлен tests/webserver-thread.os: запрос кладёт значение в данные своего потока, падает, обработчик исключений читает то же значение и возвращает его в ответе, после чего проверяется срабатывание ПриЗавершении. Готовность сервера ждётся опросом, а не фиксированной паузой. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014ZfXJeyPJxZxtooYQzTt7s --- .../Threads/ExecutionThreadContext.cs | 31 +++- src/ScriptEngine.HostedScript/Process.cs | 5 + tests/tasks.os | 113 ++++++++++++- tests/webserver-thread.os | 151 ++++++++++++++++++ 4 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 tests/webserver-thread.os diff --git a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs index ad27a8e65..32f1950c3 100644 --- a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs +++ b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs @@ -145,14 +145,37 @@ private void RaiseTerminationEvent(IBslProcess process) } } + /// + /// Освобождает данные потока исполнения. + /// + /// Каждое значение освобождается независимо: ошибка на одном не мешает освободить + /// остальные и не выпускается наружу. Поток завершается уже после того, как код + /// единицы исполнения отработал, и ронять на этом её результат нельзя. + /// public void Dispose() { - foreach (var item in Data) + try { - (item.Value as IDisposable)?.Dispose(); - } + foreach (var item in Data) + { + if (item.Value is not IDisposable disposable) + continue; - Data.Clear(); + 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/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/tests/tasks.os b/tests/tasks.os index 1e64e368b..ca3ee3634 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -32,6 +32,8 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьОсвобождениеДанныхПотокаИсполнения"); ВсеТесты.Добавить("ТестДолжен_ВызватьОбработчикЗавершенияПотокаИсполнения"); ВсеТесты.Добавить("ТестДолжен_НеРонятьЗаданиеИзЗаОшибкиВОбработчикеЗавершения"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьОстальныеДанныеПриОшибкеОсвобождения"); + ВсеТесты.Добавить("ТестДолжен_ОсвободитьДанныеПотокаОсновногоСкрипта"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных"); @@ -450,7 +452,116 @@ юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, "Ошибка в обработчике завершения не должна менять состояние задания"); - + +КонецПроцедуры + +// Создаёт значение, освобождение которого завершается ошибкой. +// +// ЗаписьДанных при освобождении сбрасывает буфер в нижележащий поток, а он уже закрыт. +// +Функция ПадающийПриОсвобожденииРесурс() + + ФайловыйПоток = Новый ФайловыйПоток(ПолучитьИмяВременногоФайла("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), + "По окончании обработки запроса должен сработать обработчик ПриЗавершении"); + + Исключение + Сервер.Остановить(); + МенеджерФоновыхЗаданий.Очистить(); + ВызватьИсключение; + КонецПопытки; + + Сервер.Остановить(); + МенеджерФоновыхЗаданий.Очистить(); + +КонецПроцедуры From eb5a99684ff96827d1e8a343b0e79bba475ab7b5 Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Tue, 25 Aug 2026 15:09:19 +0200 Subject: [PATCH 08/13] =?UTF-8?q?=D0=98=D0=B7=D0=BE=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BE=D1=81=D0=B2=D0=BE=D0=B1=D0=BE=D0=B6=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA=D0=B0=20=D0=B8=20=D1=83=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=BA=D0=B0=20=D0=B7=D0=B0=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По второй порции замечаний ревью PR #1725. Освобождение перебирало живую карту данных: освобождаемое значение может изменить эти же данные, и следующий шаг перебора сорвался бы уже вне защиты, окружающей само освобождение, - ошибка вышла бы из Release. Значения снимаются в отдельный список до начала освобождения. Обработчик Ctrl+C в ConsoleContext создавал процесс и исполнял в нем bsl-код, не завершая поток исполнения. Завершение добавлено в finally. Тест на изоляцию ошибок освобождения оставлял после себя два временных файла: закрытие потоков файлы не удаляет. Пути возвращаются заданием и удаляются после проверок. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014ZfXJeyPJxZxtooYQzTt7s --- .../Text/ConsoleContext.cs | 14 +++- .../Threads/ExecutionThreadContext.cs | 7 +- tests/tasks.os | 82 ++++++++++++++----- 3 files changed, 81 insertions(+), 22 deletions(-) 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 index 32f1950c3..5fb67a4b9 100644 --- a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs +++ b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs @@ -6,6 +6,7 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; +using System.Linq; using System.Runtime.CompilerServices; using OneScript.Contexts; using OneScript.Execution; @@ -151,12 +152,16 @@ private void RaiseTerminationEvent(IBslProcess process) /// Каждое значение освобождается независимо: ошибка на одном не мешает освободить /// остальные и не выпускается наружу. Поток завершается уже после того, как код /// единицы исполнения отработал, и ронять на этом её результат нельзя. + /// + /// Значения снимаются в отдельный список до начала освобождения: освобождаемое значение + /// может изменить эти же данные, и перебор живой карты сорвался бы на следующем шаге - + /// уже вне защиты, окружающей само освобождение. /// public void Dispose() { try { - foreach (var item in Data) + foreach (var item in Data.ToArray()) { if (item.Value is not IDisposable disposable) continue; diff --git a/tests/tasks.os b/tests/tasks.os index ca3ee3634..d39f48ed4 100644 --- a/tests/tasks.os +++ b/tests/tasks.os @@ -459,10 +459,12 @@ // // ЗаписьДанных при освобождении сбрасывает буфер в нижележащий поток, а он уже закрыт. // -Функция ПадающийПриОсвобожденииРесурс() +// Параметры: +// ИмяФайла - Строка - Файл, поверх которого строится значение. +// +Функция ПадающийПриОсвобожденииРесурс(Знач ИмяФайла) - ФайловыйПоток = Новый ФайловыйПоток(ПолучитьИмяВременногоФайла("tmp"), - РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + ФайловыйПоток = Новый ФайловыйПоток(ИмяФайла, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); Писатель = Новый ЗаписьДанных(ФайловыйПоток); Писатель.ЗаписатьСтроку("данные, которые придётся сбрасывать при закрытии"); ФайловыйПоток.Закрыть(); @@ -473,13 +475,29 @@ Функция ПоложитьВДанныеПадающийИИсправныйРесурсы() Экспорт - Исправный = Новый ФайловыйПоток(ПолучитьИмяВременногоФайла("tmp"), - РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + ФайлПадающего = ПолучитьИмяВременногоФайла("tmp"); + ФайлИсправного = ПолучитьИмяВременногоФайла("tmp"); - ТекущийПоток().Данные.Вставить("падающий", ПадающийПриОсвобожденииРесурс()); + Исправный = Новый ФайловыйПоток(ФайлИсправного, РежимОткрытияФайла.Создать, ДоступКФайлу.Запись); + + ТекущийПоток().Данные.Вставить("падающий", ПадающийПриОсвобожденииРесурс(ФайлПадающего)); ТекущийПоток().Данные.Вставить("исправный", Исправный); - Возврат Новый Структура("Поток, Исправный", ТекущийПоток(), Исправный); + // Пути возвращаются вызывающему: закрытие потоков файлы не удаляет + Возврат Новый Структура("Поток, Исправный, Файлы", ТекущийПоток(), Исправный, + ОбщегоНазначенияМассивИз(ФайлПадающего, ФайлИсправного)); + +КонецФункции + +// Собирает массив из переданных значений. +// +Функция ОбщегоНазначенияМассивИз(Знач Первое, Знач Второе) + + Результат = Новый Массив; + Результат.Добавить(Первое); + Результат.Добавить(Второе); + + Возврат Результат; КонецФункции @@ -491,24 +509,50 @@ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПоложитьВДанныеПадающийИИсправныйРесурсы"); Задание.ОжидатьЗавершения(); - юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, - "Ошибка освобождения значения не должна менять состояние задания"); - Результат = Задание.Результат; - юТест.ПроверитьНеРавенство(Неопределено, Результат, "Задание должно вернуть результат"); - - юТест.ПроверитьРавенство(0, Результат.Поток.Данные.Количество(), - "Данные потока должны очищаться даже при ошибке освобождения одного из значений"); - ИсправныйОсвобожден = Ложь; Попытка - Результат.Исправный.ТекущаяПозиция(); + + юТест.ПроверитьРавенство(СостояниеФоновогоЗадания.Завершено, Задание.Состояние, + "Ошибка освобождения значения не должна менять состояние задания"); + + юТест.ПроверитьНеРавенство(Неопределено, Результат, "Задание должно вернуть результат"); + + юТест.ПроверитьРавенство(0, Результат.Поток.Данные.Количество(), + "Данные потока должны очищаться даже при ошибке освобождения одного из значений"); + + ИсправныйОсвобожден = Ложь; + Попытка + Результат.Исправный.ТекущаяПозиция(); + Исключение + ИсправныйОсвобожден = Истина; + КонецПопытки; + + юТест.ПроверитьРавенство(Истина, ИсправныйОсвобожден, + "Исправное значение должно освобождаться независимо от ошибки на соседнем"); + Исключение - ИсправныйОсвобожден = Истина; + УдалитьВременныеФайлы(Результат); + ВызватьИсключение; КонецПопытки; - юТест.ПроверитьРавенство(Истина, ИсправныйОсвобожден, - "Исправное значение должно освобождаться независимо от ошибки на соседнем"); + УдалитьВременныеФайлы(Результат); + +КонецПроцедуры + +// Удаляет временные файлы, созданные заданием: закрытие потоков их не удаляет. +// +Процедура УдалитьВременныеФайлы(Знач Результат) + + Если Результат = Неопределено Тогда + Возврат; + КонецЕсли; + + Для Каждого ИмяФайла Из Результат.Файлы Цикл + Если Новый Файл(ИмяФайла).Существует() Тогда + УдалитьФайлы(ИмяФайла); + КонецЕсли; + КонецЦикла; КонецПроцедуры From 4ee603559f0acbc3b914b52e1ef63bbd4f322ce8 Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Thu, 27 Aug 2026 17:04:46 +0200 Subject: [PATCH 09/13] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=86=D0=B5=D1=81?= =?UTF-8?q?=D1=81=20=D1=81=D0=B0=D0=BC=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=88=D0=B0=D0=B5=D1=82=20=D1=81=D0=B2=D0=BE=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=BA=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По замечаниям мэйнтейнера в PR #1725. Связь процесса с его bsl-объектом ПотокИсполнения жила в статической ConditionalWeakTable. За такой коллекцией надо следить, а сам класс - из запчастей компилятора, не прикладной. Теперь процесс носит поток исполнения в своём свойстве и завершает его сам: IBslProcess стал освобождаемым, а владельцы процессов перешли на using вместо явного вызова завершения. Статическая коллекция и метод Release удалены. Тип свойства - новый интерфейс IBslExecutionThread в OneScript.Core. Через него ПотокИсполнения виден движку, который не знает и не может знать про OneScript.StandardLibrary. Отдельный интерфейс вместо IDisposable выбран потому, что ОсвободитьОбъект приводит значение к IDisposable: с ним из bsl можно было бы завершить живой поток вызовом ОсвободитьОбъект(ТекущийПоток()). Поток снимается с процесса после завершения, а не до: обработчик ПриЗавершении вправе обратиться к ТекущийПоток() и должен получить свой поток, а не новый и пустой. Защита от повторного завершения перенесена внутрь потока - так она ловит и случай, когда обработчик сам освободит процесс. Событие поднимает сам поток исполнения, своим именем-константой - так же, как это делает ConsoleContext со своим CancelKeyPressed. Процесс поток держит полем, по образцу ConsoleContext с ExecutionContext. Попутно закрыто место, до которого раньше было не дотянуться: ExceptionInfoFactory живёт в ScriptEngine и вызвать завершение из StandardLibrary не мог, а освободить процесс - может. Убран CanWrite = false у свойств без сеттера: запись и так запрещена отсутствием сеттера, а в стандартной библиотеке так объявлены 650 свойств из 658. Переписаны формулировки в комментариях. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014ZfXJeyPJxZxtooYQzTt7s --- .../Execution/ForbiddenBslProcess.cs | 14 +++ .../Execution/IBslExecutionThread.cs | 25 +++++ src/OneScript.Core/Execution/IBslProcess.cs | 19 +++- .../Tasks/BackgroundTasksManager.cs | 13 +-- .../Text/ConsoleContext.cs | 10 +- .../Threads/ExecutionThreadContext.cs | 93 ++++++++++--------- src/OneScript.Web.Server/RequestBslProcess.cs | 8 +- src/ScriptEngine.HostedScript/Process.cs | 7 +- src/ScriptEngine/BslProcess.cs | 14 +++ .../Machine/ExceptionInfoFactory.cs | 12 ++- .../NativeExecutableTestProcess.cs | 8 ++ 11 files changed, 146 insertions(+), 77 deletions(-) create mode 100644 src/OneScript.Core/Execution/IBslExecutionThread.cs diff --git a/src/OneScript.Core/Execution/ForbiddenBslProcess.cs b/src/OneScript.Core/Execution/ForbiddenBslProcess.cs index 73fb76973..3bd587080 100644 --- a/src/OneScript.Core/Execution/ForbiddenBslProcess.cs +++ b/src/OneScript.Core/Execution/ForbiddenBslProcess.cs @@ -31,5 +31,19 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe public IServiceContainer Services => throw new NotSupportedException("BslProcess required"); public int VirtualThreadId => -1; + + public IBslExecutionThread ExecutionThread + { + get => throw new NotSupportedException("BslProcess required"); + set => throw new NotSupportedException("BslProcess required"); + } + + /// + /// Освобождать нечего: в этом процессе bsl-код не исполняется, а сам он - разделяемый + /// экземпляр, который переживает любого вызывающего. + /// + public void Dispose() + { + } } } \ No newline at end of file diff --git a/src/OneScript.Core/Execution/IBslExecutionThread.cs b/src/OneScript.Core/Execution/IBslExecutionThread.cs new file mode 100644 index 000000000..61dbee6e6 --- /dev/null +++ b/src/OneScript.Core/Execution/IBslExecutionThread.cs @@ -0,0 +1,25 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +#nullable enable + +namespace OneScript.Execution +{ + /// + /// Bsl-обёртка потока исполнения процесса. + /// + /// Процесс носит обёртку с собой и завершает её, когда освобождается сам. Что за обёртка + /// и что она хранит, процессу знать не нужно. + /// + public interface IBslExecutionThread + { + /// + /// Завершает поток исполнения. + /// + void Terminate(); + } +} diff --git a/src/OneScript.Core/Execution/IBslProcess.cs b/src/OneScript.Core/Execution/IBslProcess.cs index 2ef3b4233..d25316a3d 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,9 +16,12 @@ This Source Code Form is subject to the terms of the namespace OneScript.Execution { /// - /// Готовый к исполнению bsl-процесс, с настроенным окружением + /// Готовый к исполнению bsl-процесс, с настроенным окружением. + /// + /// Процесс освобождается тем, кто его создал, когда единица исполнения отработала. + /// Освобождение завершает поток исполнения процесса. /// - public interface IBslProcess + public interface IBslProcess : IDisposable { /// /// Запустить метод в текущем процессе @@ -33,7 +37,16 @@ public interface IBslProcess /// Сервисы текущего процесса /// public IServiceContainer Services { get; } - + public int VirtualThreadId { get; } + + /// + /// Bsl-обёртка потока исполнения этого процесса. + /// + /// Процесс носит её с собой и завершает вместе с собой. Создаёт обёртку тот, кто её + /// понимает - стандартная библиотека при первом обращении к ТекущийПоток(). Пока к потоку + /// не обращались, здесь null, и завершать нечего. + /// + public IBslExecutionThread? ExecutionThread { get; set; } } } \ No newline at end of file diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs index 8bad8516a..efe1f3249 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs @@ -52,16 +52,9 @@ public BackgroundTask Execute(IRuntimeContextInstance target, string methodName, var taskCreationOptions = longRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None; var worker = new Task(() => { - var process = _runtimeContext.Services.Resolve().NewProcess(); - try - { - task.ExecuteOnCurrentThread(process); - } - finally - { - // Задание отработало, поток исполнения закончился - освобождаем его данные - ExecutionThreadContext.Release(process); - } + // Задание отработало - процесс освобождается вместе со своим потоком исполнения + using var process = _runtimeContext.Services.Resolve().NewProcess(); + task.ExecuteOnCurrentThread(process); }, taskCreationOptions); diff --git a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs index 8a1435638..bff9377bc 100644 --- a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs +++ b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs @@ -272,21 +272,15 @@ private void ConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs e) if (eventProcessor == null) 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 }; - try + // Обработчик исполняется в собственном процессе, который здесь же и заканчивается + using (var process = _executionContext.Services.Resolve().NewProcess()) { 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 index 5fb67a4b9..8230e5b7f 100644 --- a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs +++ b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs @@ -7,7 +7,6 @@ This Source Code Form is subject to the terms of the using System; using System.Linq; -using System.Runtime.CompilerServices; using OneScript.Contexts; using OneScript.Execution; using OneScript.StandardLibrary.Collections; @@ -38,7 +37,7 @@ namespace OneScript.StandardLibrary.Threads /// чтобы опрашивать список фоновых заданий. /// [ContextClass("ПотокИсполнения", "ExecutionThread")] - public sealed class ExecutionThreadContext : AutoContext, IDisposable + public sealed class ExecutionThreadContext : AutoContext, IBslExecutionThread { /// /// Имена события завершения потока исполнения. Событие поднимается под обоими именами, @@ -46,10 +45,13 @@ public sealed class ExecutionThreadContext : AutoContext /// private static readonly string[] TerminationEventNames = { "ПриЗавершении", "OnTermination" }; - private static readonly ConditionalWeakTable Threads = new(); + private readonly IBslProcess _process; + + private bool _terminated; private ExecutionThreadContext(IBslProcess process) { + _process = process; Identifier = process.VirtualThreadId; } @@ -61,7 +63,7 @@ private ExecutionThreadContext(IBslProcess process) /// исполнения используйте свойство Данные, а не идентификатор в качестве ключа. /// /// Число. Идентификатор потока исполнения. - [ContextProperty("Идентификатор", "Id", CanWrite = false)] + [ContextProperty("Идентификатор", "Id")] public int Identifier { get; } /// @@ -72,53 +74,46 @@ private ExecutionThreadContext(IBslProcess process) /// интерфейс IDisposable среды CLR, принудительно освобождаются. /// /// Соответствие. Данные потока исполнения. - [ContextProperty("Данные", "Data", CanWrite = false)] + [ContextProperty("Данные", "Data")] 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) + internal static ExecutionThreadContext Of(IBslProcess process) { - if (process == null) - return; + if (process.ExecutionThread is ExecutionThreadContext existing) + return existing; - if (!Threads.TryGetValue(process, out var thread)) - return; + lock (process) + { + if (process.ExecutionThread is ExecutionThreadContext created) + return created; - thread.RaiseTerminationEvent(process); + var thread = new ExecutionThreadContext(process); + process.ExecutionThread = thread; - Threads.Remove(process); - thread.Dispose(); + return thread; + } } /// - /// Поднимает событие завершения потока исполнения. + /// Оповещает подписчиков о завершении потока и снимает их подписки. /// - /// Событие поднимается до очистки данных, поэтому обработчик ещё видит всё, что поток - /// в них положил, и может, например, вернуть занятые ресурсы владельцу. + /// Событие поднимается до очистки данных: обработчик ещё видит всё, что поток успел в них + /// положить. Именно так пул соединений забирает обратно соединение, которое отработавший + /// код не освободил сам. /// - /// Ошибка обработчика не выпускается наружу: поток завершается уже после того, как - /// код единицы исполнения отработал, и ронять на этом её результат нельзя. У фонового - /// задания завершение идёт в блоке finally и затёрло бы исходную ошибку, у веб-сервера - /// оно выполняется после отправки ответа. + /// Ошибка обработчика наружу не выходит, только в лог. К этому моменту код единицы + /// исполнения уже отработал: у фонового задания завершение идёт в блоке finally и + /// затёрло бы исходную ошибку, у веб-сервера - после отправки ответа. /// - private void RaiseTerminationEvent(IBslProcess process) + private void RaiseTerminationEvent() { - var eventProcessor = process.Services.TryResolve(); + var eventProcessor = _process.Services.TryResolve(); if (eventProcessor == null) return; @@ -128,7 +123,7 @@ private void RaiseTerminationEvent(IBslProcess process) { try { - eventProcessor.HandleEvent(this, eventName, Array.Empty(), process); + eventProcessor.HandleEvent(this, eventName, Array.Empty(), _process); } catch (Exception exception) { @@ -139,26 +134,34 @@ private void RaiseTerminationEvent(IBslProcess process) } finally { - // Реестр подписок держит источник до конца работы движка, а поток исполнения - // живёт лишь до конца своей единицы исполнения. Без снятия подписок каждый - // завершившийся поток оставался бы в реестре навсегда. + // Процессор событий держит источник, пока подписки не сняты. Потоков исполнения + // много и живут они недолго, поэтому без явного снятия реестр рос бы бесконечно. eventProcessor.RemoveAllHandlers(this); } } /// - /// Освобождает данные потока исполнения. + /// Завершает поток исполнения: оповещает подписчиков и освобождает данные. /// - /// Каждое значение освобождается независимо: ошибка на одном не мешает освободить - /// остальные и не выпускается наружу. Поток завершается уже после того, как код - /// единицы исполнения отработал, и ронять на этом её результат нельзя. + /// Вызывается процессом, когда тот освобождается. /// - /// Значения снимаются в отдельный список до начала освобождения: освобождаемое значение - /// может изменить эти же данные, и перебор живой карты сорвался бы на следующем шаге - - /// уже вне защиты, окружающей само освобождение. + /// Каждое значение освобождается отдельно, ошибка на одном не мешает остальным и наружу + /// не выходит. Значения перебираются по копии: освобождаемое значение вправе изменить + /// эти же данные, а перебор живой карты сорвался бы на следующем шаге - причём мимо + /// защиты, которой окружено само освобождение. /// - public void Dispose() + public void Terminate() { + // Пока идёт завершение, поток ещё числится за процессом, и обработчик вправе + // добраться до него через ТекущийПоток(). Если он при этом освободит процесс, + // завершение не должно пойти по второму кругу. + if (_terminated) + return; + + _terminated = true; + + RaiseTerminationEvent(); + try { foreach (var item in Data.ToArray()) diff --git a/src/OneScript.Web.Server/RequestBslProcess.cs b/src/OneScript.Web.Server/RequestBslProcess.cs index 35d364a03..b9eaf242f 100644 --- a/src/OneScript.Web.Server/RequestBslProcess.cs +++ b/src/OneScript.Web.Server/RequestBslProcess.cs @@ -6,7 +6,6 @@ This Source Code Form is subject to the terms of the ----------------------------------------------------------*/ using System; using OneScript.Execution; -using OneScript.StandardLibrary.Threads; namespace OneScript.Web.Server { @@ -18,11 +17,8 @@ namespace OneScript.Web.Server /// Процесс создаётся при первом обращении: запросы, не дошедшие до bsl-кода, /// процесс не создают. /// - /// Процесс намеренно не хранится в HttpContext.Items: Items доступны из bsl-кода - /// как Контекст.Данные и остаются полностью прикладными. - /// /// Область сервисов запроса освобождается вместе с запросом, поэтому здесь же - /// заканчивается поток исполнения запроса и освобождаются его данные. + /// освобождается и процесс - вместе со своим потоком исполнения. /// internal sealed class RequestBslProcess : IDisposable { @@ -53,7 +49,7 @@ public IBslProcess Process public void Dispose() { // Процесс создаётся лениво, поэтому ради освобождения его создавать не нужно - ExecutionThreadContext.Release(_process); + _process?.Dispose(); } } } diff --git a/src/ScriptEngine.HostedScript/Process.cs b/src/ScriptEngine.HostedScript/Process.cs index fa850f7cb..f182b79d7 100644 --- a/src/ScriptEngine.HostedScript/Process.cs +++ b/src/ScriptEngine.HostedScript/Process.cs @@ -6,7 +6,6 @@ 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 @@ -51,9 +50,9 @@ public int Start() } finally { - // Поток исполнения основного скрипта закончился здесь. Освобождается до - // остановки движка: обработчику ПриЗавершении ещё нужно исполнять bsl-код. - ExecutionThreadContext.Release(_bslProcess); + // Основной скрипт отработал. Процесс освобождается до остановки движка: + // обработчику ПриЗавершении ещё нужно исполнять bsl-код. + _bslProcess.Dispose(); _engine.Debugger.NotifyProcessExit(exitCode); _engine.Dispose(); diff --git a/src/ScriptEngine/BslProcess.cs b/src/ScriptEngine/BslProcess.cs index 65b8c9de2..f1428afa4 100644 --- a/src/ScriptEngine/BslProcess.cs +++ b/src/ScriptEngine/BslProcess.cs @@ -37,6 +37,20 @@ public BslProcess(int id, ExecutionContext context, IEnumerable + /// Завершает процесс: единица исполнения отработала, и её поток исполнения больше не нужен. + /// + /// Поток снимается с процесса после завершения, а не до: обработчик ПриЗавершении вправе + /// обратиться к ТекущийПоток() и должен получить свой поток, а не новый и пустой. + /// + public void Dispose() + { + ExecutionThread?.Terminate(); + ExecutionThread = null; + } + public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMethodInfo method, IValue[] arguments) { var notifyExecutors = !_isRunning; diff --git a/src/ScriptEngine/Machine/ExceptionInfoFactory.cs b/src/ScriptEngine/Machine/ExceptionInfoFactory.cs index bcbd04ed3..2085f52e9 100644 --- a/src/ScriptEngine/Machine/ExceptionInfoFactory.cs +++ b/src/ScriptEngine/Machine/ExceptionInfoFactory.cs @@ -57,9 +57,19 @@ public Exception Raise(object raiseValue) { ExceptionInfoContext { IsErrorTemplate: true } excInfo => new ParametrizedRuntimeException(excInfo.Description, excInfo.Parameters, excInfo.InnerException), - UserScriptContextInstance userContext => new RuntimeException(userContext.ToString(_processFactory.NewProcess())), + UserScriptContextInstance userContext => new RuntimeException(PresentationOf(userContext)), _ => new RuntimeException(raiseValue.ToString()) }; } + + /// + /// Получает представление объекта: у него может быть собственное ПриПреобразованииВСтроку, + /// а это bsl-код, которому нужен процесс. Процесс здесь и заканчивается. + /// + private string PresentationOf(UserScriptContextInstance userContext) + { + using var process = _processFactory.NewProcess(); + return userContext.ToString(process); + } } } \ No newline at end of file diff --git a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs index f5c18cb9c..b8eee074a 100644 --- a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs +++ b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs @@ -26,4 +26,12 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe public IServiceContainer Services { get; set; } public int VirtualThreadId => 0; + + public IBslExecutionThread ExecutionThread { get; set; } + + public void Dispose() + { + ExecutionThread?.Terminate(); + ExecutionThread = null; + } } \ No newline at end of file From 12861a5552421e556e09bdca076e0348dbcbac66 Mon Sep 17 00:00:00 2001 From: Nikita Fedkin Date: Thu, 27 Aug 2026 17:19:52 +0200 Subject: [PATCH 10/13] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=86=D0=B5=D1=81?= =?UTF-8?q?=D1=81=20=D0=BE=D1=81=D0=B2=D0=BE=D0=B1=D0=BE=D0=B6=D0=B4=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20=D1=81=D0=B2=D0=BE=D1=8E=20=D0=BE=D0=B1=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D1=8C=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BslProcess создаёт себе область сервисов в конструкторе, но не освобождал её никогда: scoped-регистрации процесса жили до конца работы движка. Теперь, когда процесс стал освобождаемым, у этого появилось место. Область освобождается последней: завершение потока исполнения обращается к ней за процессором событий. Освобождение сделано идемпотентным. Проверено, что дочерний контейнер TinyIoC не заденет чужое: родительские фабрики он берёт транзитом через GetParentObjectFactory и в собственный реестр не кладёт, а Dispose обходит только его. Второй тест это фиксирует - освобождение одного процесса не трогает область другого. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014ZfXJeyPJxZxtooYQzTt7s --- src/ScriptEngine/BslProcess.cs | 13 +++- .../OneScript.Core.Tests/BslProcessTests.cs | 74 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/Tests/OneScript.Core.Tests/BslProcessTests.cs diff --git a/src/ScriptEngine/BslProcess.cs b/src/ScriptEngine/BslProcess.cs index f1428afa4..74ef27eb8 100644 --- a/src/ScriptEngine/BslProcess.cs +++ b/src/ScriptEngine/BslProcess.cs @@ -22,6 +22,7 @@ internal class BslProcess : IBslProcess private readonly IDictionary _bslExecutorsByModule; private bool _isRunning; + private bool _disposed; public BslProcess(int id, ExecutionContext context, IEnumerable executorProviders) { @@ -40,15 +41,25 @@ public BslProcess(int id, ExecutionContext context, IEnumerable - /// Завершает процесс: единица исполнения отработала, и её поток исполнения больше не нужен. + /// Завершает процесс: единица исполнения отработала. /// /// Поток снимается с процесса после завершения, а не до: обработчик ПриЗавершении вправе /// обратиться к ТекущийПоток() и должен получить свой поток, а не новый и пустой. + /// + /// Область сервисов освобождается последней: завершение потока обращается к ней за + /// процессором событий. /// public void Dispose() { + if (_disposed) + return; + + _disposed = true; + ExecutionThread?.Terminate(); ExecutionThread = null; + + Services.Dispose(); } public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMethodInfo method, IValue[] arguments) diff --git a/src/Tests/OneScript.Core.Tests/BslProcessTests.cs b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs new file mode 100644 index 000000000..304c271fc --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs @@ -0,0 +1,74 @@ +/*---------------------------------------------------------- +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("процесс ещё работает"); + + process.Dispose(); + + 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, "у каждого процесса своя область сервисов"); + + first.Dispose(); + + secondScoped.IsDisposed.Should().BeFalse("освобождение одного процесса не трогает другой"); + } + } +} From e58c3d1966df3eed41bc030cc3c64a92fe9d672a Mon Sep 17 00:00:00 2001 From: EvilBeaver Date: Sun, 30 Aug 2026 15:40:34 +0300 Subject: [PATCH 11/13] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=20=D0=BA=20Bsl=D0=9F=D1=80=D0=BE=D1=86=D0=B5=D1=81=D1=81=D1=83?= =?UTF-8?q?,=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD=D1=82=20=D1=81=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=82=D0=B5=D0=BA=D0=B0=D1=8E=D1=89=D0=B8?= =?UTF-8?q?=D0=BC=20=D0=B2=D1=80=D0=B0=D0=BF=D0=BF=D0=B5=D1=80=D0=BE=D0=BC?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Execution/ForbiddenBslProcess.cs | 14 ---- .../Execution/IBslExecutionThread.cs | 25 ------ src/OneScript.Core/Execution/IBslProcess.cs | 20 +++-- .../Tasks/BackgroundTasksManager.cs | 4 +- .../Text/ConsoleContext.cs | 10 +-- .../Threads/ExecutionThreadContext.cs | 84 ++++--------------- src/OneScript.Web.Server/RequestBslProcess.cs | 55 ------------ src/OneScript.Web.Server/WebServer.cs | 25 ++---- src/ScriptEngine.HostedScript/Process.cs | 4 - src/ScriptEngine/BslProcess.cs | 67 +++++++++------ src/ScriptEngine/BslProcessFactory.cs | 8 +- .../Machine/ExceptionInfoFactory.cs | 12 +-- .../OneScript.Core.Tests/BslProcessTests.cs | 4 - .../NativeExecutableTestProcess.cs | 5 +- .../OneScript.Dynamic.Tests/NativeSdoTests.cs | 2 +- 15 files changed, 91 insertions(+), 248 deletions(-) delete mode 100644 src/OneScript.Core/Execution/IBslExecutionThread.cs delete mode 100644 src/OneScript.Web.Server/RequestBslProcess.cs diff --git a/src/OneScript.Core/Execution/ForbiddenBslProcess.cs b/src/OneScript.Core/Execution/ForbiddenBslProcess.cs index 3bd587080..73fb76973 100644 --- a/src/OneScript.Core/Execution/ForbiddenBslProcess.cs +++ b/src/OneScript.Core/Execution/ForbiddenBslProcess.cs @@ -31,19 +31,5 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe public IServiceContainer Services => throw new NotSupportedException("BslProcess required"); public int VirtualThreadId => -1; - - public IBslExecutionThread ExecutionThread - { - get => throw new NotSupportedException("BslProcess required"); - set => throw new NotSupportedException("BslProcess required"); - } - - /// - /// Освобождать нечего: в этом процессе bsl-код не исполняется, а сам он - разделяемый - /// экземпляр, который переживает любого вызывающего. - /// - public void Dispose() - { - } } } \ No newline at end of file diff --git a/src/OneScript.Core/Execution/IBslExecutionThread.cs b/src/OneScript.Core/Execution/IBslExecutionThread.cs deleted file mode 100644 index 61dbee6e6..000000000 --- a/src/OneScript.Core/Execution/IBslExecutionThread.cs +++ /dev/null @@ -1,25 +0,0 @@ -/*---------------------------------------------------------- -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/. -----------------------------------------------------------*/ - -#nullable enable - -namespace OneScript.Execution -{ - /// - /// Bsl-обёртка потока исполнения процесса. - /// - /// Процесс носит обёртку с собой и завершает её, когда освобождается сам. Что за обёртка - /// и что она хранит, процессу знать не нужно. - /// - public interface IBslExecutionThread - { - /// - /// Завершает поток исполнения. - /// - void Terminate(); - } -} diff --git a/src/OneScript.Core/Execution/IBslProcess.cs b/src/OneScript.Core/Execution/IBslProcess.cs index d25316a3d..297f11f63 100644 --- a/src/OneScript.Core/Execution/IBslProcess.cs +++ b/src/OneScript.Core/Execution/IBslProcess.cs @@ -21,7 +21,7 @@ namespace OneScript.Execution /// Процесс освобождается тем, кто его создал, когда единица исполнения отработала. /// Освобождение завершает поток исполнения процесса. /// - public interface IBslProcess : IDisposable + public interface IBslProcess { /// /// Запустить метод в текущем процессе @@ -40,13 +40,15 @@ public interface IBslProcess : IDisposable public int VirtualThreadId { get; } - /// - /// Bsl-обёртка потока исполнения этого процесса. - /// - /// Процесс носит её с собой и завершает вместе с собой. Создаёт обёртку тот, кто её - /// понимает - стандартная библиотека при первом обращении к ТекущийПоток(). Пока к потоку - /// не обращались, здесь null, и завершать нечего. - /// - public IBslExecutionThread? ExecutionThread { get; set; } + /** + * Обертка текущего процесса в терминах BSL. + * Может отсутствовать, полностью определяется хост-приложением процесса. + */ + public IRuntimeContextInstance? BslWrapper + { + get => null; + // ReSharper disable once ValueParameterNotUsed + set { } + } } } \ No newline at end of file diff --git a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs index efe1f3249..c3d253ce2 100644 --- a/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs +++ b/src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs @@ -16,7 +16,6 @@ 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,8 +51,7 @@ public BackgroundTask Execute(IRuntimeContextInstance target, string methodName, var taskCreationOptions = longRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None; var worker = new Task(() => { - // Задание отработало - процесс освобождается вместе со своим потоком исполнения - using var process = _runtimeContext.Services.Resolve().NewProcess(); + var process = _runtimeContext.Services.Resolve().NewProcess(); task.ExecuteOnCurrentThread(process); }, taskCreationOptions); diff --git a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs index bff9377bc..2ebe5f7c3 100644 --- a/src/OneScript.StandardLibrary/Text/ConsoleContext.cs +++ b/src/OneScript.StandardLibrary/Text/ConsoleContext.cs @@ -11,7 +11,6 @@ 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,16 +271,13 @@ private void ConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs e) if (eventProcessor == null) 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 }; - // Обработчик исполняется в собственном процессе, который здесь же и заканчивается - using (var process = _executionContext.Services.Resolve().NewProcess()) - { - eventProcessor.HandleEvent(this, ConsoleCancelKeyEvent, args, process); - } - + eventProcessor.HandleEvent(this, ConsoleCancelKeyEvent, args, process); e.Cancel = reference.Value.AsBoolean(); } } diff --git a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs index 8230e5b7f..2f1d0083e 100644 --- a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs +++ b/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs @@ -37,22 +37,15 @@ namespace OneScript.StandardLibrary.Threads /// чтобы опрашивать список фоновых заданий. /// [ContextClass("ПотокИсполнения", "ExecutionThread")] - public sealed class ExecutionThreadContext : AutoContext, IBslExecutionThread + public sealed class ExecutionThreadContext : AutoContext, IDisposable { - /// - /// Имена события завершения потока исполнения. Событие поднимается под обоими именами, - /// поэтому подписаться можно как на русское, так и на английское. - /// - private static readonly string[] TerminationEventNames = { "ПриЗавершении", "OnTermination" }; - private readonly IBslProcess _process; - private bool _terminated; + private bool _isDisposed; private ExecutionThreadContext(IBslProcess process) { _process = process; - Identifier = process.VirtualThreadId; } /// @@ -64,7 +57,7 @@ private ExecutionThreadContext(IBslProcess process) /// /// Число. Идентификатор потока исполнения. [ContextProperty("Идентификатор", "Id")] - public int Identifier { get; } + public int Identifier => _process.VirtualThreadId; /// /// Хранилище данных потока исполнения, аналог набора thread-local переменных. @@ -85,82 +78,33 @@ private ExecutionThreadContext(IBslProcess process) /// internal static ExecutionThreadContext Of(IBslProcess process) { - if (process.ExecutionThread is ExecutionThreadContext existing) - return existing; - + if (process.BslWrapper is ExecutionThreadContext wrapper) + return wrapper; + lock (process) { - if (process.ExecutionThread is ExecutionThreadContext created) + 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.ExecutionThread = thread; + process.BslWrapper = thread; return thread; } } - /// - /// Оповещает подписчиков о завершении потока и снимает их подписки. - /// - /// Событие поднимается до очистки данных: обработчик ещё видит всё, что поток успел в них - /// положить. Именно так пул соединений забирает обратно соединение, которое отработавший - /// код не освободил сам. - /// - /// Ошибка обработчика наружу не выходит, только в лог. К этому моменту код единицы - /// исполнения уже отработал: у фонового задания завершение идёт в блоке finally и - /// затёрло бы исходную ошибку, у веб-сервера - после отправки ответа. - /// - private void RaiseTerminationEvent() - { - 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 Terminate() + public void Dispose() { // Пока идёт завершение, поток ещё числится за процессом, и обработчик вправе // добраться до него через ТекущийПоток(). Если он при этом освободит процесс, // завершение не должно пойти по второму кругу. - if (_terminated) + if (_isDisposed) return; - _terminated = true; - - RaiseTerminationEvent(); + _isDisposed = true; try { diff --git a/src/OneScript.Web.Server/RequestBslProcess.cs b/src/OneScript.Web.Server/RequestBslProcess.cs deleted file mode 100644 index b9eaf242f..000000000 --- a/src/OneScript.Web.Server/RequestBslProcess.cs +++ /dev/null @@ -1,55 +0,0 @@ -/*---------------------------------------------------------- -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; - -namespace OneScript.Web.Server -{ - /// - /// Scoped-сервис, хранящий bsl-процесс, который обслуживает текущий запрос. - /// - /// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса, - /// включая обработчик исключений, видит один и тот же ИдентификаторПотокаИсполнения. - /// Процесс создаётся при первом обращении: запросы, не дошедшие до 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() - { - // Процесс создаётся лениво, поэтому ради освобождения его создавать не нужно - _process?.Dispose(); - } - } -} diff --git a/src/OneScript.Web.Server/WebServer.cs b/src/OneScript.Web.Server/WebServer.cs index 8ee683214..0dbe15af5 100644 --- a/src/OneScript.Web.Server/WebServer.cs +++ b/src/OneScript.Web.Server/WebServer.cs @@ -104,9 +104,6 @@ private void ConfigureApp() builder.Services.Configure(builder.Configuration.GetSection("FormOptions")); - builder.Services.AddScoped(_ => - new RequestBslProcess(_executionContext.Services.Resolve())); - _app = builder.Build(); if (_useStaticFiles) @@ -124,6 +121,13 @@ 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) => @@ -134,7 +138,7 @@ private void ConfigureApp() new RequestDelegateWrapper(next) }; - var process = GetRequestProcess(context); + var process = (IBslProcess)context.Items[typeof(IBslProcess)]; var methodNumber = middleware.Target.GetMethodNumber(middleware.MethodName); middleware.Target.CallAsProcedure(methodNumber, args, process); @@ -174,10 +178,7 @@ private void UseBslExceptionHandler() var methodNumber = _exceptionHandler?.Target.GetMethodNumber(_exceptionHandler?.MethodName) ?? throw new InvalidOperationException(); - // UseExceptionHandler переиспользует область сервисов запроса, поэтому - // обработчик исключений получает тот же процесс, что и упавший обработчик - // запроса, и видит контекст исполнения, в котором возникла ошибка. - var process = GetRequestProcess(context); + var process = _executionContext.Services.Resolve().NewProcess(); try { @@ -196,14 +197,6 @@ 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/Process.cs b/src/ScriptEngine.HostedScript/Process.cs index f182b79d7..39a36d266 100644 --- a/src/ScriptEngine.HostedScript/Process.cs +++ b/src/ScriptEngine.HostedScript/Process.cs @@ -50,10 +50,6 @@ public int Start() } finally { - // Основной скрипт отработал. Процесс освобождается до остановки движка: - // обработчику ПриЗавершении ещё нужно исполнять bsl-код. - _bslProcess.Dispose(); - _engine.Debugger.NotifyProcessExit(exitCode); _engine.Dispose(); _engine = null; diff --git a/src/ScriptEngine/BslProcess.cs b/src/ScriptEngine/BslProcess.cs index 74ef27eb8..4d7deeb06 100644 --- a/src/ScriptEngine/BslProcess.cs +++ b/src/ScriptEngine/BslProcess.cs @@ -18,6 +18,8 @@ namespace ScriptEngine { internal class BslProcess : IBslProcess { + private static readonly string[] TerminationEventNames = { "ПриЗавершении", "OnTermination" }; + private readonly IExecutorProvider[] _executorProviders; private readonly IDictionary _bslExecutorsByModule; @@ -38,30 +40,6 @@ public BslProcess(int id, ExecutionContext context, IEnumerable - /// Завершает процесс: единица исполнения отработала. - /// - /// Поток снимается с процесса после завершения, а не до: обработчик ПриЗавершении вправе - /// обратиться к ТекущийПоток() и должен получить свой поток, а не новый и пустой. - /// - /// Область сервисов освобождается последней: завершение потока обращается к ней за - /// процессором событий. - /// - public void Dispose() - { - if (_disposed) - return; - - _disposed = true; - - ExecutionThread?.Terminate(); - ExecutionThread = null; - - Services.Dispose(); - } - public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMethodInfo method, IValue[] arguments) { var notifyExecutors = !_isRunning; @@ -80,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/BslProcessFactory.cs b/src/ScriptEngine/BslProcessFactory.cs index 8d063ab74..76188e714 100644 --- a/src/ScriptEngine/BslProcessFactory.cs +++ b/src/ScriptEngine/BslProcessFactory.cs @@ -29,9 +29,15 @@ public IBslProcess NewProcess() { // Создаем новый контекст со всеми зависимостями var context = _services.Resolve(); + var processContext = new ExecutionContext( + context.TypeManager, + context.GlobalNamespace, + context.GlobalInstances, + context.Services.CreateScope()); + var executors = _services.ResolveEnumerable(); - return new BslProcess(Interlocked.Increment(ref _threadIdCounter), context, executors); + return new BslProcess(Interlocked.Increment(ref _threadIdCounter), processContext, executors); } } } \ No newline at end of file diff --git a/src/ScriptEngine/Machine/ExceptionInfoFactory.cs b/src/ScriptEngine/Machine/ExceptionInfoFactory.cs index 2085f52e9..bcbd04ed3 100644 --- a/src/ScriptEngine/Machine/ExceptionInfoFactory.cs +++ b/src/ScriptEngine/Machine/ExceptionInfoFactory.cs @@ -57,19 +57,9 @@ public Exception Raise(object raiseValue) { ExceptionInfoContext { IsErrorTemplate: true } excInfo => new ParametrizedRuntimeException(excInfo.Description, excInfo.Parameters, excInfo.InnerException), - UserScriptContextInstance userContext => new RuntimeException(PresentationOf(userContext)), + UserScriptContextInstance userContext => new RuntimeException(userContext.ToString(_processFactory.NewProcess())), _ => new RuntimeException(raiseValue.ToString()) }; } - - /// - /// Получает представление объекта: у него может быть собственное ПриПреобразованииВСтроку, - /// а это bsl-код, которому нужен процесс. Процесс здесь и заканчивается. - /// - private string PresentationOf(UserScriptContextInstance userContext) - { - using var process = _processFactory.NewProcess(); - return userContext.ToString(process); - } } } \ No newline at end of file diff --git a/src/Tests/OneScript.Core.Tests/BslProcessTests.cs b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs index 304c271fc..39e57abdd 100644 --- a/src/Tests/OneScript.Core.Tests/BslProcessTests.cs +++ b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs @@ -48,8 +48,6 @@ public void ProcessReleasesItsServiceScope() scoped.IsDisposed.Should().BeFalse("процесс ещё работает"); - process.Dispose(); - scoped.IsDisposed.Should().BeTrue("процесс владеет своей областью сервисов"); } @@ -66,8 +64,6 @@ public void ProcessDoesNotReleaseScopeOfAnotherProcess() firstScoped.Should().NotBeSameAs(secondScoped, "у каждого процесса своя область сервисов"); - first.Dispose(); - secondScoped.IsDisposed.Should().BeFalse("освобождение одного процесса не трогает другой"); } } diff --git a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs index b8eee074a..848b2a9f1 100644 --- a/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs +++ b/src/Tests/OneScript.Dynamic.Tests/NativeExecutableTestProcess.cs @@ -27,11 +27,8 @@ public BslValue Run(BslObjectValue target, IExecutableModule module, BslScriptMe public int VirtualThreadId => 0; - public IBslExecutionThread ExecutionThread { get; set; } - public void Dispose() { - ExecutionThread?.Terminate(); - ExecutionThread = null; + 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() }; } From 72844630859dbdc347a2de7a595dcd8d3c1f780b Mon Sep 17 00:00:00 2001 From: EvilBeaver Date: Sun, 30 Aug 2026 17:35:10 +0300 Subject: [PATCH 12/13] =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20Threads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Threads => }/ExecutionThreadContext.cs | 2 +- src/OneScript.StandardLibrary/StandardGlobalContext.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename src/OneScript.StandardLibrary/{Threads => }/ExecutionThreadContext.cs (99%) diff --git a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs b/src/OneScript.StandardLibrary/ExecutionThreadContext.cs similarity index 99% rename from src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs rename to src/OneScript.StandardLibrary/ExecutionThreadContext.cs index 2f1d0083e..0b8f56952 100644 --- a/src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs +++ b/src/OneScript.StandardLibrary/ExecutionThreadContext.cs @@ -14,7 +14,7 @@ This Source Code Form is subject to the terms of the using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; -namespace OneScript.StandardLibrary.Threads +namespace OneScript.StandardLibrary { /// /// Поток исполнения bsl-кода. diff --git a/src/OneScript.StandardLibrary/StandardGlobalContext.cs b/src/OneScript.StandardLibrary/StandardGlobalContext.cs index 883ec1a00..e29661cf4 100644 --- a/src/OneScript.StandardLibrary/StandardGlobalContext.cs +++ b/src/OneScript.StandardLibrary/StandardGlobalContext.cs @@ -12,7 +12,6 @@ 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; From e29b496e78b998ef7ca45376bcafb525dfccbcce Mon Sep 17 00:00:00 2001 From: EvilBeaver Date: Sun, 30 Aug 2026 18:55:49 +0300 Subject: [PATCH 13/13] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ScriptEngine/BslProcessFactory.cs | 8 +- .../OneScript.Core.Tests/BslProcessTests.cs | 5 +- tests/webserver-thread.os | 151 ------------------ 3 files changed, 5 insertions(+), 159 deletions(-) delete mode 100644 tests/webserver-thread.os diff --git a/src/ScriptEngine/BslProcessFactory.cs b/src/ScriptEngine/BslProcessFactory.cs index 76188e714..8d063ab74 100644 --- a/src/ScriptEngine/BslProcessFactory.cs +++ b/src/ScriptEngine/BslProcessFactory.cs @@ -29,15 +29,9 @@ public IBslProcess NewProcess() { // Создаем новый контекст со всеми зависимостями var context = _services.Resolve(); - var processContext = new ExecutionContext( - context.TypeManager, - context.GlobalNamespace, - context.GlobalInstances, - context.Services.CreateScope()); - var executors = _services.ResolveEnumerable(); - return new BslProcess(Interlocked.Increment(ref _threadIdCounter), processContext, executors); + return new BslProcess(Interlocked.Increment(ref _threadIdCounter), context, executors); } } } \ No newline at end of file diff --git a/src/Tests/OneScript.Core.Tests/BslProcessTests.cs b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs index 39e57abdd..eb022261b 100644 --- a/src/Tests/OneScript.Core.Tests/BslProcessTests.cs +++ b/src/Tests/OneScript.Core.Tests/BslProcessTests.cs @@ -47,7 +47,10 @@ public void ProcessReleasesItsServiceScope() var scoped = process.Services.Resolve(); scoped.IsDisposed.Should().BeFalse("процесс ещё работает"); - + + engine.AttachedScriptsFactory.LoadFromString( + engine.GetCompilerService(), "f = 1", process); + scoped.IsDisposed.Should().BeTrue("процесс владеет своей областью сервисов"); } diff --git a/tests/webserver-thread.os b/tests/webserver-thread.os deleted file mode 100644 index c39f09675..000000000 --- a/tests/webserver-thread.os +++ /dev/null @@ -1,151 +0,0 @@ -/////////////////////////////////////////////////////////////////////// -// -// Поток исполнения при обработке запросов веб-сервером. -// -// Каждый запрос обслуживается собственным потоком исполнения. Проверяется, что обработчик -// исключений работает в потоке упавшего обработчика запроса и видит его данные, а по окончании -// обработки запроса поток завершается с вызовом ПриЗавершении. -// -/////////////////////////////////////////////////////////////////////// - -Перем юТест; - -// Число - Порт тестового веб-сервера. -Перем ПортТестовогоСервера; -// Число - Сколько раз сработал обработчик завершения потока запроса. -Перем ЗавершенийЗафиксировано; -// ВебСервер - Тестовый сервер. Виден фоновому заданию, которое его поднимает. -Перем Сервер; - -Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт - - юТест = ЮнитТестирование; - - ВсеТесты = Новый Массив; - ВсеТесты.Добавить("ТестДолжен_ОбработатьИсключениеВПотокеЗапросаИЗавершитьЕго"); - - Возврат ВсеТесты; - -КонецФункции - -// Обработчик завершения потока запроса. -// -Процедура ПриЗавершенииПотокаЗапроса() Экспорт - ЗавершенийЗафиксировано = ЗавершенийЗафиксировано + 1; -КонецПроцедуры - -Процедура ОбработчикЗапроса(Контекст, СледующийОбработчик) Экспорт - - Если Контекст.Запрос.Путь = "/ping" Тогда - Контекст.Ответ.Записать("pong"); - Возврат; - КонецЕсли; - - ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ПриЗавершенииПотокаЗапроса; - - ТекущийПоток().Данные.Вставить("метка", "метка-" + Контекст.ИдентификаторТрассировки); - - ВызватьИсключение "Падаем намеренно"; - -КонецПроцедуры - -Процедура ОбработчикИсключений(Контекст) Экспорт - - Контекст.Ответ.КодСостояния = 500; - // Значение положил обработчик запроса. Виден оно здесь только если обработчик исключений - // работает в том же потоке исполнения. - Контекст.Ответ.Записать("" + ТекущийПоток().Данные.Получить("метка")); - -КонецПроцедуры - -Процедура Вебсервер() Экспорт - - Сервер = Новый ВебСервер(ПортТестовогоСервера); - Сервер.ДобавитьОбработчикЗапросов(ЭтотОбъект, "ОбработчикЗапроса"); - Сервер.ДобавитьОбработчикИсключений(ЭтотОбъект, "ОбработчикИсключений"); - Сервер.Запустить(); - -КонецПроцедуры - -// Дожидается готовности сервера опросом, а не фиксированной паузой: на нагруженной машине -// подъём занимает непредсказуемое время, и пауза наугад делает тест нестабильным. -// -Функция ДождатьсяГотовностиСервера(Знач Соединение, Знач ПределОжидания = 30000) - - Начало = ТекущаяУниверсальнаяДатаВМиллисекундах(); - - Пока ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало < ПределОжидания Цикл - Попытка - Ответ = Соединение.Получить(Новый HTTPЗапрос("/ping")); - Если Ответ.КодСостояния = 200 Тогда - Возврат Истина; - КонецЕсли; - Исключение - // Сервер ещё не поднялся - пробуем снова - КонецПопытки; - Приостановить(50); - КонецЦикла; - - Возврат Ложь; - -КонецФункции - -// Дожидается, пока обработчик завершения потока отработает: область сервисов запроса -// освобождается уже после отправки ответа, поэтому счётчик растёт чуть позже ответа. -// -Функция ДождатьсяЗавершенийПотоков(Знач Ожидается, Знач ПределОжидания = 30000) - - Начало = ТекущаяУниверсальнаяДатаВМиллисекундах(); - - Пока ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало < ПределОжидания Цикл - Если ЗавершенийЗафиксировано >= Ожидается Тогда - Возврат Истина; - КонецЕсли; - Приостановить(50); - КонецЦикла; - - Возврат Ложь; - -КонецФункции - -Процедура ТестДолжен_ОбработатьИсключениеВПотокеЗапросаИЗавершитьЕго() Экспорт - - ПортТестовогоСервера = 8182; - ЗавершенийЗафиксировано = 0; - - МенеджерФоновыхЗаданий = Новый МенеджерФоновыхЗаданий; - МенеджерФоновыхЗаданий.Выполнить(ЭтотОбъект, "Вебсервер"); - - Попытка - - Соединение = Новый HTTPСоединение("http://127.0.0.1:" + ПортТестовогоСервера); - - юТест.ПроверитьРавенство(Истина, ДождатьсяГотовностиСервера(Соединение), - "Тестовый веб-сервер должен подняться"); - - // Проверки готовности тоже создают потоки исполнения - считаем только целевой запрос - ЗавершенийЗафиксировано = 0; - - Ответ = Соединение.Получить(Новый HTTPЗапрос("/")); - - юТест.ПроверитьРавенство(500, Ответ.КодСостояния, - "Обработчик исключений должен отработать"); - - ТелоОтвета = Ответ.ПолучитьТелоКакСтроку(); - юТест.ПроверитьРавенство(Истина, СтрНачинаетсяС(ТелоОтвета, "метка-"), - "Обработчик исключений должен видеть данные потока упавшего обработчика запроса," - + " получено: " + ТелоОтвета); - - юТест.ПроверитьРавенство(Истина, ДождатьсяЗавершенийПотоков(1), - "По окончании обработки запроса должен сработать обработчик ПриЗавершении"); - - Исключение - Сервер.Остановить(); - МенеджерФоновыхЗаданий.Очистить(); - ВызватьИсключение; - КонецПопытки; - - Сервер.Остановить(); - МенеджерФоновыхЗаданий.Очистить(); - -КонецПроцедуры