Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/OneScript.StandardLibrary/StandardGlobalContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,6 +76,28 @@ public void RunGarbageCollection()
GC.WaitForPendingFinalizers();
}

/// <summary>
/// Возвращает поток исполнения, в котором выполняется текущий код.
///
/// Отдельным потоком исполнения является каждая независимая единица исполнения bsl-кода:
/// основной скрипт, каждое фоновое задание и каждый обрабатываемый запрос веб-сервера.
/// Для одной единицы исполнения всегда возвращается один и тот же объект, поэтому его
/// свойство Данные можно использовать как набор thread-local переменных.
///
/// В отличие от идентификатора фонового задания, поток исполнения определён во всех
/// контекстах, в том числе при обработке запросов веб-сервера, где фоновое задание
/// отсутствует.
///
/// Поток исполнения не наследуется: фоновое задание, запущенное из текущего потока
/// исполнения, получит собственный поток с собственными данными.
/// </summary>
/// <returns>ПотокИсполнения. Поток исполнения текущего кода.</returns>
[ContextMethod("ТекущийПоток", "CurrentThread")]
public ExecutionThreadContext CurrentThread(IBslProcess process)
{
return ExecutionThreadContext.Of(process);
Comment on lines +96 to +98

Copy link
Copy Markdown
Collaborator Author

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). Позвать завершение оттуда физически нечем.

То есть остаётся ровно та развилка, которую вы и назвали: либо централизовать жизненный цикл на границе процесса, либо оставить это место незакрытым. Вижу два варианта централизации:

  1. Слушатель в OneScript.Core, например IBslProcessLifetime с методом Finished(IBslProcess), регистрируемый как enumerable. StandardLibrary регистрирует реализацию, которая зовёт Release, а владельцы процессов в движке уведомляют слушателей. Абстракция маленькая, но владельцы всё равно должны её звать.
  2. Сделать IBslProcess освобождаемым и завершать его владельцами через using. Честнее по смыслу и закрывает вообще все места разом, но это изменение публичного интерфейса, который реализуют ещё ForbiddenBslProcess и тестовые заглушки.

Сам выбирать не стал: это уже третье изменение формы API в этом PR, и решать, по-моему, автору движка. Пока оставил как есть и фиксирую здесь, чтобы не потерялось.

Замечу для полноты картины: утечки памяти в незакрытых местах нет — поток привязан к процессу через ConditionalWeakTable и исчезает вместе с ним. Теряется только детерминированность: ПриЗавершении не наступает и значения с IDisposable не освобождаются сразу. Для разового преобразования в строку это вряд ли критично, но контракт действительно нарушается.

}

/// <summary>
/// Приостанавливает выполнение скрипта.
/// </summary>
Expand Down
13 changes: 11 additions & 2 deletions src/OneScript.StandardLibrary/Tasks/BackgroundTasksManager.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -52,7 +53,15 @@ public BackgroundTask Execute(IRuntimeContextInstance target, string methodName,
var worker = new Task(() =>
{
var process = _runtimeContext.Services.Resolve<IBslProcessFactory>().NewProcess();
task.ExecuteOnCurrentThread(process);
try
{
task.ExecuteOnCurrentThread(process);
}
finally
{
// Задание отработало, поток исполнения закончился - освобождаем его данные
ExecutionThreadContext.Release(process);
}

}, taskCreationOptions);

Expand Down
14 changes: 12 additions & 2 deletions src/OneScript.StandardLibrary/Text/ConsoleContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -272,12 +273,21 @@ private void ConsoleOnCancelKeyPress(object sender, ConsoleCancelEventArgs e)
return;

var process = _executionContext.Services.Resolve<IBslProcessFactory>().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();
}
}
Expand Down
186 changes: 186 additions & 0 deletions src/OneScript.StandardLibrary/Threads/ExecutionThreadContext.cs
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Верно, спасибо. Release действительно вызывался только для фоновых заданий и запросов веб-сервера, а Process.Start() в своём finally только уведомлял отладчик и останавливал движок.

Исправлено в e6df8647: завершение потока вызывается там же, в finally, до _engine.Dispose() — обработчику ПриЗавершении ещё нужно исполнять bsl-код.

Тест добавлен, tests/tasks.osТестДолжен_ОсвободитьДанныеПотокаОсновногоСкрипта. Момент завершения скрипта изнутри самого скрипта не наблюдаем, поэтому проверка идёт дочерним oscript: он кладёт значение в данные своего потока, подписывается на ПриЗавершении и пишет из обработчика файл-метку с количеством элементов; родитель дожидается завершения процесса и смотрит на метку. Гонок нет — процесс либо успел записать метку до выхода, либо нет.

До правки тест падал на Сравниваемые значения (True; False) — метки не было вовсе.

/// </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();
}
}
}
}
59 changes: 59 additions & 0 deletions src/OneScript.Web.Server/RequestBslProcess.cs
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);
}
}
}
Loading