-
Notifications
You must be signed in to change notification settings - Fork 119
Функция ТекущийПоток(): данные и событие завершения потока исполнения #1725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
6c2da66
3b5d116
3cb707c
d633efe
e9cadbc
1e5c1d0
e6df864
eb5a996
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| /*---------------------------------------------------------- | ||
| This Source Code Form is subject to the terms of the | ||
| Mozilla Public License, v.2.0. If a copy of the MPL | ||
| was not distributed with this file, You can obtain one | ||
| at http://mozilla.org/MPL/2.0/. | ||
| ----------------------------------------------------------*/ | ||
|
|
||
| using System; | ||
| using System.Linq; | ||
| using System.Runtime.CompilerServices; | ||
| using OneScript.Contexts; | ||
| using OneScript.Execution; | ||
| using OneScript.StandardLibrary.Collections; | ||
| using ScriptEngine; | ||
| using ScriptEngine.Machine; | ||
| using ScriptEngine.Machine.Contexts; | ||
|
|
||
| namespace OneScript.StandardLibrary.Threads | ||
| { | ||
| /// <summary> | ||
| /// Поток исполнения bsl-кода. | ||
| /// | ||
| /// Потоком исполнения является каждая независимая единица исполнения: основной скрипт, | ||
| /// каждое фоновое задание и каждый обрабатываемый запрос веб-сервера. Получить поток | ||
| /// исполнения текущего кода можно функцией ТекущийПоток(). | ||
| /// | ||
| /// Свойство Данные представляет собой хранилище, живущее ровно столько же, сколько живёт | ||
| /// сам поток исполнения. Оно предназначено для библиотек, которым нужно хранить состояние | ||
| /// в разрезе единицы исполнения (аналог thread-local хранилища). | ||
| /// | ||
| /// По завершении потока исполнения поднимается событие ПриЗавершении (оно же OnTermination), | ||
| /// на которое можно подписаться штатным способом: | ||
| /// | ||
| /// ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ВернутьСоединениеВПул; | ||
| /// | ||
| /// Обработчик вызывается до очистки данных потока, поэтому ещё видит их содержимое. | ||
| /// Это позволяет владельцам ресурсов узнавать о конце единицы исполнения вместо того, | ||
| /// чтобы опрашивать список фоновых заданий. | ||
| /// </summary> | ||
| [ContextClass("ПотокИсполнения", "ExecutionThread")] | ||
| public sealed class ExecutionThreadContext : AutoContext<ExecutionThreadContext>, IDisposable | ||
| { | ||
| /// <summary> | ||
| /// Имена события завершения потока исполнения. Событие поднимается под обоими именами, | ||
| /// поэтому подписаться можно как на русское, так и на английское. | ||
| /// </summary> | ||
| private static readonly string[] TerminationEventNames = { "ПриЗавершении", "OnTermination" }; | ||
|
|
||
| private static readonly ConditionalWeakTable<IBslProcess, ExecutionThreadContext> Threads = new(); | ||
|
|
||
| private ExecutionThreadContext(IBslProcess process) | ||
| { | ||
| Identifier = process.VirtualThreadId; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Идентификатор потока исполнения. Предназначен для диагностики и журналирования. | ||
| /// | ||
| /// Идентификаторы выдаются последовательно и в пределах запущенного движка не повторяются, | ||
| /// пока счётчик не исчерпает диапазон Int32. Для хранения состояния в разрезе потока | ||
| /// исполнения используйте свойство Данные, а не идентификатор в качестве ключа. | ||
| /// </summary> | ||
| /// <value>Число. Идентификатор потока исполнения.</value> | ||
| [ContextProperty("Идентификатор", "Id", CanWrite = false)] | ||
| public int Identifier { get; } | ||
|
|
||
| /// <summary> | ||
| /// Хранилище данных потока исполнения, аналог набора thread-local переменных. | ||
| /// | ||
| /// Соответствие создаётся вместе с потоком исполнения и не разделяется с другими потоками. | ||
| /// В конце потока исполнения соответствие очищается, а его значения, поддерживающие | ||
| /// интерфейс IDisposable среды CLR, принудительно освобождаются. | ||
| /// </summary> | ||
| /// <value>Соответствие. Данные потока исполнения.</value> | ||
| [ContextProperty("Данные", "Data", CanWrite = false)] | ||
| public MapImpl Data { get; } = new MapImpl(); | ||
|
|
||
| /// <summary> | ||
| /// Возвращает поток исполнения указанного bsl-процесса, создавая его при первом обращении. | ||
| /// Для одного процесса всегда возвращается один и тот же экземпляр. | ||
| /// </summary> | ||
| internal static ExecutionThreadContext Of(IBslProcess process) | ||
| { | ||
| return Threads.GetValue(process, p => new ExecutionThreadContext(p)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Завершает поток исполнения процесса, освобождая его данные. | ||
| /// | ||
| /// Вызывается владельцем процесса, когда процесс отработал: менеджером фоновых заданий | ||
| /// по завершении задания и веб-сервером по окончании обработки запроса. Если поток | ||
| /// исполнения не создавался, метод ничего не делает. | ||
|
Comment on lines
+90
to
+92
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Верно, спасибо. Исправлено в Тест добавлен, До правки тест падал на |
||
| /// </summary> | ||
| public static void Release(IBslProcess process) | ||
| { | ||
| if (process == null) | ||
| return; | ||
|
|
||
| if (!Threads.TryGetValue(process, out var thread)) | ||
| return; | ||
|
|
||
| thread.RaiseTerminationEvent(process); | ||
|
|
||
| Threads.Remove(process); | ||
| thread.Dispose(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Поднимает событие завершения потока исполнения. | ||
| /// | ||
| /// Событие поднимается до очистки данных, поэтому обработчик ещё видит всё, что поток | ||
| /// в них положил, и может, например, вернуть занятые ресурсы владельцу. | ||
| /// | ||
| /// Ошибка обработчика не выпускается наружу: поток завершается уже после того, как | ||
| /// код единицы исполнения отработал, и ронять на этом её результат нельзя. У фонового | ||
| /// задания завершение идёт в блоке finally и затёрло бы исходную ошибку, у веб-сервера | ||
| /// оно выполняется после отправки ответа. | ||
| /// </summary> | ||
| private void RaiseTerminationEvent(IBslProcess process) | ||
| { | ||
| var eventProcessor = process.Services.TryResolve<IEventProcessor>(); | ||
| if (eventProcessor == null) | ||
| return; | ||
|
|
||
| try | ||
| { | ||
| foreach (var eventName in TerminationEventNames) | ||
| { | ||
| try | ||
| { | ||
| eventProcessor.HandleEvent(this, eventName, Array.Empty<IValue>(), process); | ||
| } | ||
| catch (Exception exception) | ||
| { | ||
| SystemLogger.Write( | ||
| $"WARNING! Error in execution thread termination handler '{eventName}': {exception.Message}"); | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| // Реестр подписок держит источник до конца работы движка, а поток исполнения | ||
| // живёт лишь до конца своей единицы исполнения. Без снятия подписок каждый | ||
| // завершившийся поток оставался бы в реестре навсегда. | ||
| eventProcessor.RemoveAllHandlers(this); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Освобождает данные потока исполнения. | ||
| /// | ||
| /// Каждое значение освобождается независимо: ошибка на одном не мешает освободить | ||
| /// остальные и не выпускается наружу. Поток завершается уже после того, как код | ||
| /// единицы исполнения отработал, и ронять на этом её результат нельзя. | ||
| /// | ||
| /// Значения снимаются в отдельный список до начала освобождения: освобождаемое значение | ||
| /// может изменить эти же данные, и перебор живой карты сорвался бы на следующем шаге - | ||
| /// уже вне защиты, окружающей само освобождение. | ||
| /// </summary> | ||
| public void Dispose() | ||
| { | ||
| try | ||
| { | ||
| foreach (var item in Data.ToArray()) | ||
| { | ||
| if (item.Value is not IDisposable disposable) | ||
| continue; | ||
|
|
||
| try | ||
| { | ||
| disposable.Dispose(); | ||
| } | ||
| catch (Exception exception) | ||
| { | ||
| SystemLogger.Write( | ||
| $"WARNING! Error releasing execution thread data '{item.Key}': {exception.Message}"); | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| Data.Clear(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /*---------------------------------------------------------- | ||
| This Source Code Form is subject to the terms of the | ||
| Mozilla Public License, v.2.0. If a copy of the MPL | ||
| was not distributed with this file, You can obtain one | ||
| at http://mozilla.org/MPL/2.0/. | ||
| ----------------------------------------------------------*/ | ||
| using System; | ||
| using OneScript.Execution; | ||
| using OneScript.StandardLibrary.Threads; | ||
|
|
||
| namespace OneScript.Web.Server | ||
| { | ||
| /// <summary> | ||
| /// Scoped-сервис, хранящий bsl-процесс, который обслуживает текущий запрос. | ||
| /// | ||
| /// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса, | ||
| /// включая обработчик исключений, видит один и тот же ИдентификаторПотокаИсполнения. | ||
| /// Процесс создаётся при первом обращении: запросы, не дошедшие до bsl-кода, | ||
| /// процесс не создают. | ||
| /// | ||
| /// Процесс намеренно не хранится в HttpContext.Items: Items доступны из bsl-кода | ||
| /// как Контекст.Данные и остаются полностью прикладными. | ||
| /// | ||
| /// Область сервисов запроса освобождается вместе с запросом, поэтому здесь же | ||
| /// заканчивается поток исполнения запроса и освобождаются его данные. | ||
| /// </summary> | ||
| internal sealed class RequestBslProcess : IDisposable | ||
| { | ||
| private readonly IBslProcessFactory _processFactory; | ||
| private readonly object _lock = new object(); | ||
|
|
||
| private IBslProcess _process; | ||
|
|
||
| public RequestBslProcess(IBslProcessFactory processFactory) | ||
| { | ||
| _processFactory = processFactory; | ||
| } | ||
|
|
||
| public IBslProcess Process | ||
| { | ||
| get | ||
| { | ||
| if (_process != null) | ||
| return _process; | ||
|
|
||
| lock (_lock) | ||
| { | ||
| return _process ??= _processFactory.NewProcess(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| // Процесс создаётся лениво, поэтому ради освобождения его создавать не нужно | ||
| ExecutionThreadContext.Release(_process); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Оба места нашли верно, проверил.
ConsoleContext.cs:274исправлен вeb5a9968— завершение потока добавлено вfinallyвокруг вызова обработчика Ctrl+C.С
ExceptionInfoFactory.cs:60так не выйдет: он живёт вScriptEngine, аExecutionThreadContext— вOneScript.StandardLibrary, и ссылка между ними односторонняя (StandardLibrary → ScriptEngine). Позвать завершение оттуда физически нечем.То есть остаётся ровно та развилка, которую вы и назвали: либо централизовать жизненный цикл на границе процесса, либо оставить это место незакрытым. Вижу два варианта централизации:
OneScript.Core, напримерIBslProcessLifetimeс методомFinished(IBslProcess), регистрируемый как enumerable.StandardLibraryрегистрирует реализацию, которая зовётRelease, а владельцы процессов в движке уведомляют слушателей. Абстракция маленькая, но владельцы всё равно должны её звать.IBslProcessосвобождаемым и завершать его владельцами черезusing. Честнее по смыслу и закрывает вообще все места разом, но это изменение публичного интерфейса, который реализуют ещёForbiddenBslProcessи тестовые заглушки.Сам выбирать не стал: это уже третье изменение формы API в этом PR, и решать, по-моему, автору движка. Пока оставил как есть и фиксирую здесь, чтобы не потерялось.
Замечу для полноты картины: утечки памяти в незакрытых местах нет — поток привязан к процессу через
ConditionalWeakTableи исчезает вместе с ним. Теряется только детерминированность:ПриЗавершениине наступает и значения сIDisposableне освобождаются сразу. Для разового преобразования в строку это вряд ли критично, но контракт действительно нарушается.