From b35694aba8072709a35310c65fd22755dd2e624d Mon Sep 17 00:00:00 2001 From: sfaqer Date: Wed, 30 Sep 2026 20:20:24 +1000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=87=D0=B8=D0=BA=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=D1=8B=D0=B7=D1=8B=D0=B2=D0=B0=D1=8E=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=20=D0=BA=D0=BE=D0=BF=D0=B8=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ВызватьСобытие перебирал живой список обработчиков вне блокировки: подписка или отписка во время события, даже в том же потоке, роняла вызов с «Collection was modified». Теперь список копируется под блокировкой. Co-Authored-By: Claude Opus 5.5 --- .../DefaultEventProcessor.cs | 9 +- tests/events.os | 99 +++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs b/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs index 6c283db1e..86546165d 100644 --- a/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs +++ b/src/ScriptEngine.HostedScript/DefaultEventProcessor.cs @@ -8,6 +8,7 @@ This Source Code Form is subject to the terms of the using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using OneScript.Contexts; using OneScript.Exceptions; using OneScript.Execution; @@ -125,7 +126,7 @@ public void RemoveAllHandlers(IRuntimeContextInstance eventSource) public void HandleEvent(IRuntimeContextInstance eventSource, string eventName, IValue[] eventArgs, IBslProcess process) { - HandlersList handlersLocalCopy; + Handler[] handlersLocalCopy; lock (_subscriptionLock) { @@ -133,10 +134,14 @@ public void HandleEvent(IRuntimeContextInstance eventSource, string eventName, I if (!_registeredHandlers.TryGetValue(eventSource, out var handlers)) return; - if (!handlers.TryGetValue(eventName, out handlersLocalCopy)) + if (!handlers.TryGetValue(eventName, out var handlersList)) { return; } + + // Копия: обработчики могут подписываться и отписываться во время события, + // в том числе из других потоков + handlersLocalCopy = handlersList.ToArray(); } foreach (var handler in handlersLocalCopy) diff --git a/tests/events.os b/tests/events.os index 7d94060cd..4365d747d 100644 --- a/tests/events.os +++ b/tests/events.os @@ -2,6 +2,9 @@ Перем юТест; Перем СчетчикВызовов; Перем ПараметрыВызова; +Перем ИсточникОдноразовогоОбработчика; +Перем СобытиеНачалось; +Перем ОтпуститьСобытие; Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт @@ -17,6 +20,8 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьПодпискуНаСобытиеВВидеВыражения"); ВсеТесты.Добавить("ТестДолжен_ПроверитьПодпискуСОбработчикомВВидеВыражения"); ВсеТесты.Добавить("ТестДолжен_ПроверитьЧтоУИсточникаМожетБытьНесколькоСобытий"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьОтпискуОбработчикаВоВремяСобытия"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьПодпискуВоВремяСобытийИзФоновогоЗадания"); Возврат ВсеТесты; @@ -195,4 +200,98 @@ КонецПроцедуры +Процедура ОдноразовыйОбработчикСобытия(Параметр1, Параметр2) Экспорт + + УдалитьОбработчик ИсточникОдноразовогоОбработчика.МоеСобытие, ЭтотОбъект.ОдноразовыйОбработчикСобытия; + ОбработчикСобытия(Параметр1, Параметр2); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьОтпискуОбработчикаВоВремяСобытия() Экспорт + + ИсточникОдноразовогоОбработчика = Новый ТестСобытий; + ДобавитьОбработчик ИсточникОдноразовогоОбработчика.МоеСобытие, ЭтотОбъект.ОдноразовыйОбработчикСобытия; + ДобавитьОбработчик ИсточникОдноразовогоОбработчика.МоеСобытие, ЭтотОбъект.ОбработчикСобытия; + + МассивПараметров = Новый Массив; + МассивПараметров.Добавить("П1"); + МассивПараметров.Добавить("П2"); + + // Обработчик, отписавшийся во время события, не мешает вызвать остальные + ИсточникОдноразовогоОбработчика.СгенерироватьСобытие("МоеСобытие", МассивПараметров); + юТест.ПроверитьРавенство(2, СчетчикВызовов); + + ИсточникОдноразовогоОбработчика.СгенерироватьСобытие("МоеСобытие", МассивПараметров); + юТест.ПроверитьРавенство(3, СчетчикВызовов); + +КонецПроцедуры + +Процедура ВременныйОбработчикСобытия(Параметр1, Параметр2) Экспорт + ОбработчикСобытия(Параметр1, Параметр2); +КонецПроцедуры + +Процедура ОбработчикСобытияСОжиданием(Параметр1, Параметр2) Экспорт + + // Держит вызов события, пока основной поток меняет подписку + СобытиеНачалось = Истина; + Для Номер = 1 По 500 Цикл + Если ОтпуститьСобытие Тогда + Прервать; + КонецЕсли; + Приостановить(10); + КонецЦикла; + + // Иначе подписка поменялась бы уже после вызова события, и тест ничего не проверил + Если Не ОтпуститьСобытие Тогда + ВызватьИсключение "Не дождались изменения подписки во время события"; + КонецЕсли; + +КонецПроцедуры + +Процедура СгенерироватьСобытиеВЗадании(Источник) Экспорт + + МассивПараметров = Новый Массив; + МассивПараметров.Добавить("П1"); + МассивПараметров.Добавить("П2"); + + Источник.СгенерироватьСобытие("МоеСобытие", МассивПараметров); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьПодпискуВоВремяСобытийИзФоновогоЗадания() Экспорт + + СобытиеНачалось = Ложь; + ОтпуститьСобытие = Ложь; + + Источник = Новый ТестСобытий; + ДобавитьОбработчик Источник.МоеСобытие, ЭтотОбъект.ОбработчикСобытияСОжиданием; + ДобавитьОбработчик Источник.МоеСобытие, ЭтотОбъект.ОбработчикСобытия; + + Параметры = Новый Массив; + Параметры.Добавить(Источник); + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "СгенерироватьСобытиеВЗадании", Параметры); + + НачалосьДоИзмененияПодписки = Ложь; + Для Номер = 1 По 500 Цикл + Если СобытиеНачалось Тогда + НачалосьДоИзмененияПодписки = Истина; + Прервать; + КонецЕсли; + Приостановить(10); + КонецЦикла; + + // Событие еще вызывается в задании: подписка меняется посреди перебора обработчиков + ДобавитьОбработчик Источник.МоеСобытие, ЭтотОбъект.ВременныйОбработчикСобытия; + УдалитьОбработчик Источник.МоеСобытие, ЭтотОбъект.ВременныйОбработчикСобытия; + ОтпуститьСобытие = Истина; + Задание.ОжидатьЗавершения(); + + юТест.ПроверитьИстину(НачалосьДоИзмененияПодписки, "Событие должно было начаться до изменения подписки"); + Если Задание.ИнформацияОбОшибке <> Неопределено Тогда + ВызватьИсключение Задание.ИнформацияОбОшибке.ПодробноеОписаниеОшибки(); + КонецЕсли; + юТест.ПроверитьРавенство(1, СчетчикВызовов, "Обработчик, подписанный до события, вызывается один раз"); + +КонецПроцедуры + ПодключитьСценарий(ТекущийСценарий().Источник, "ТестСобытий"); \ No newline at end of file