Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
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
22 changes: 22 additions & 0 deletions src/OneScript.StandardLibrary/StandardGlobalContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,28 @@ public void RunGarbageCollection()
GC.WaitForPendingFinalizers();
}

/// <summary>
/// Возвращает идентификатор потока исполнения, в котором выполняется текущий код.
///
/// Каждая независимая единица исполнения bsl-кода получает собственный идентификатор:
/// основной скрипт, каждое фоновое задание и каждый обрабатываемый запрос веб-сервера.
/// Идентификаторы уникальны в пределах запущенного движка и не переиспользуются.
///
/// Метод предназначен для библиотек, которым нужно хранить данные в разрезе единицы
/// исполнения (аналог thread-local хранилища). В отличие от идентификатора фонового задания,
/// значение определено во всех контекстах, в том числе при обработке запросов веб-сервера,
/// где фоновое задание отсутствует.
///
/// Идентификатор не наследуется: фоновое задание, запущенное из текущего потока исполнения,
/// получит собственное значение.
/// </summary>
/// <returns>Число. Идентификатор текущего потока исполнения.</returns>
[ContextMethod("ИдентификаторПотокаИсполнения", "ExecutionThreadId")]
public int ExecutionThreadId(IBslProcess process)
{
return process.VirtualThreadId;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Замечание резонное. Я вообще предполагал метод ТекущийПоток() у которого были бы свойства, в т.ч. например соответствие, которое будет работать как набор тредлокалов и которое принудительно диспоузится вместе со всеми элементами в конце процесса. Не настаиваю, просто такой подход позволил бы вместо id процесса использовать собственно инстанс процесса.

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.

Сделал по вашему предложению, ИдентификаторПотокаИсполнения() убрал.

Теперь ТекущийПоток() возвращает ПотокИсполнения со свойствами Идентификатор и Данные:

ТекущийПоток().Данные.Вставить("спан", Спан);
...
Спан = ТекущийПоток().Данные.Получить("спан");

Экземпляр привязан к bsl-процессу через ConditionalWeakTable, поэтому в пределах одной единицы исполнения ТекущийПоток() всегда отдаёт один и тот же объект, а запись исчезает вместе с процессом сама, даже если владелец забыл его завершить.

Завершают поток явно владельцы процесса: менеджер фоновых заданий по завершении задания и веб-сервер по окончании обработки запроса (через Dispose того самого scoped-сервиса из соседнего треда). При завершении соответствие очищается, а значения, поддерживающие IDisposable, освобождаются — как вы и описывали.

Побочно это закрывает и исходное замечание про переполнение: ключом для хранения состояния служит сам объект потока, а не число, так что виток Int32 уже ничего не ломает. Идентификатор остался только для диагностики и журналирования, о чём написано в его документации.

Тесты в tests/tasks.os: уникальность потока, изоляция Данных между одновременными фоновыми заданиями и освобождение данных с принудительным Dispose элементов. Писал их до реализации — тест на освобождение сначала падал с Сравниваемые значения (0; 1) не равны, то есть данные переживали задание.

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.

Дополню: к ТекущийПоток() добавилось событие завершения потока — оно закрывает то, чего одних только данных не хватало.

Освобождение Данных добирается лишь до значений с IDisposable среды CLR. Прикладным библиотекам этого мало: соединение с БД в entity — обычный BSL-объект, и узнать о конце единицы исполнения ему было неоткуда, кроме опроса списка фоновых заданий, который не видит ни запросов веб-сервера, ни последствий ФоновыеЗадания.Очистить().

Подписка идёт штатным механизмом, движку для неё ничего доделывать не пришлось:

ДобавитьОбработчик ТекущийПоток().ПриЗавершении, ЭтотОбъект.ВернутьСоединениеВПул;

Событие поднимается до очистки данных, поэтому обработчик ещё видит их содержимое. Ошибка обработчика наружу не выпускается: у фонового задания завершение идёт в finally и затёрло бы исходную ошибку, у веб-сервера выполняется уже после отправки ответа.

Попутно пришлось добавить IEventProcessor.RemoveAllHandlers: реестр подписок DefaultEventProcessor держит источник до конца работы движка, а поток исполнения живёт лишь до конца своей единицы исполнения — без снятия подписок каждый обработанный запрос оставлял бы в реестре запись навсегда. Метод объявлен с пустой реализацией по умолчанию, чтобы не ломать сторонние процессоры событий.

Схему обкатал на двух своих библиотеках. В opentelemetry стек контекстов переехал в ТекущийПоток().Данные: ушли синхронизированная карта, ручная сборка мусора и опрос заданий, а шесть одновременных запросов перестали читать чужие значения и падать с Index is out of range на общем массиве. В entity пул соединений перешёл с опроса на подписку: исчезли Работает(), ПотокАктивен и повторы на Collection was modified, а брошенное соединение возвращается сразу по завершении потока, а не при следующем исчерпании предела.

}

/// <summary>
/// Приостанавливает выполнение скрипта.
/// </summary>
Expand Down
25 changes: 22 additions & 3 deletions src/OneScript.Web.Server/WebServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ private void ConfigureApp()

_app.Use((context, next) =>
{
var process = _executionContext.Services.Resolve<IBslProcessFactory>().NewProcess();
context.Items.Add(typeof(IBslProcess), process);
GetOrCreateProcess(context);
return next();
});

Expand Down Expand Up @@ -178,7 +177,11 @@ private void UseBslExceptionHandler()
var methodNumber = _exceptionHandler?.Target.GetMethodNumber(_exceptionHandler?.MethodName)
?? throw new InvalidOperationException();

var process = _executionContext.Services.Resolve<IBslProcessFactory>().NewProcess();
// Обработчик исключений работает в том же процессе, что и обработчик запроса,
// поэтому видит контекст исполнения, в котором возникла ошибка.
// Собственный процесс создаётся только если исключение возникло до того,
// как процесс запроса был создан (например, в middleware статических файлов).

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.

Верно, и вы точнее кролика: веб-сокеты действительно были единственным middleware между UseExceptionHandler и созданием процесса.

Сейчас это неактуально с обеих сторон. Процесс выдаёт scoped-сервис и создаётся лениво, а область сервисов запроса переиспользуется UseExceptionHandler, поэтому запасное создание процесса не нужно вовсе — фолбэк и комментарий удалены.

var process = GetOrCreateProcess(context);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

try
{
Expand All @@ -197,6 +200,22 @@ private void UseBslExceptionHandler()
});
}

/// <summary>
/// Возвращает bsl-процесс, обслуживающий текущий запрос, создавая его при первом обращении.
/// Один запрос всегда обслуживается одним процессом, поэтому весь bsl-код запроса
/// видит один и тот же ИдентификаторПотокаИсполнения.
/// </summary>
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<IBslProcessFactory>().NewProcess();
context.Items[typeof(IBslProcess)] = process;

return process;
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Здесь кролик абсолютно прав. Лучше не класть это в словарь запроса, оставив его полностью прикладным. Надо сделать scoped сервис, который уже делает получение или создание процесса. Кролик предлагает Features, но я не знаю что это и где может стрельнуть, никогда не пользовался.

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.

Сделал scoped-сервисом, Features не трогал.

RequestBslProcess регистрируется в контейнере веб-приложения как scoped и создаёт процесс при первом обращении; получение — context.RequestServices.GetRequiredService<RequestBslProcess>().Process. Ни Items, ни Features для передачи процесса больше не используются, словарь запроса остался полностью прикладным.

Два побочных эффекта, оба в плюс:

  • Middleware, которое раньше создавало процесс заранее, удалено. Процесс теперь создаётся лениво, так что запросы, не дошедшие до bsl-кода (статика, 404), процесс не создают вообще.
  • Область сервисов запроса переиспользуется UseExceptionHandler, поэтому обработчик исключений получает тот же процесс, что и упавший обработчик запроса, без всякого запасного создания. Заодно отпало замечание кролика про статические файлы — комментарий, к которому оно относилось, исчез вместе с фолбэком.

Проверял на 4 одновременных запросах: обработчик исключений в каждом читает из ТекущийПоток().Данные ровно тот «спан», который положил его же обработчик запроса, без пересечений между запросами.


private static void WriteExceptionToResponse(HttpContext httpContext, Exception ex)
{
httpContext.Response.StatusCode = 500;
Expand Down
33 changes: 33 additions & 0 deletions tests/tasks.os
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоВозвращаетсяРезультатДелегата");
ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоРаботаетБлокировка");
ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоКодМожетОпределитьИДЗадания");
ВсеТесты.Добавить("ТестДолжен_ПроверитьУникальностьИдентификатораПотокаИсполнения");
ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьТекущее");
ВсеТесты.Добавить("ТестДолжен_ПроверитьПотокобезопасностьПолучитьФоновыеЗадания");
ВсеТесты.Добавить("ТестДолжен_ПроверитьПоискТекущегоСредиМножестваЗавершенных");
Expand Down Expand Up @@ -276,6 +277,38 @@

КонецПроцедуры

Функция ВернутьИдентификаторПотокаИсполнения() Экспорт

Приостановить(500);
Возврат ИдентификаторПотокаИсполнения();

КонецФункции

Процедура ТестДолжен_ПроверитьУникальностьИдентификатораПотокаИсполнения() Экспорт

ИдОсновногоПотока = ИдентификаторПотокаИсполнения();
юТест.ПроверитьРавенство(ИдОсновногоПотока, ИдентификаторПотокаИсполнения(),
"Идентификатор потока исполнения должен быть одинаковым при повторном вызове");

МассивЗаданий = Новый Массив;
Для Сч = 1 По 4 Цикл
МассивЗаданий.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ВернутьИдентификаторПотокаИсполнения"));
КонецЦикла;

ФоновыеЗадания.ОжидатьВсе(МассивЗаданий);

УникальныеИдентификаторы = Новый Соответствие;
Для Каждого Задание Из МассивЗаданий Цикл
юТест.ПроверитьНеРавенство(ИдОсновногоПотока, Задание.Результат,
"Фоновое задание должно получить собственный идентификатор потока исполнения");
УникальныеИдентификаторы.Вставить(Задание.Результат, Истина);
КонецЦикла;

юТест.ПроверитьРавенство(МассивЗаданий.Количество(), УникальныеИдентификаторы.Количество(),
"Идентификаторы потоков исполнения одновременных фоновых заданий должны различаться");

КонецПроцедуры

Процедура ТестДолжен_ПроверитьЧтоВИнформацииОбОшибкеЕстьСтекВызовов() Экспорт

Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПроцедураСИсключением");
Expand Down