fix: транзакционный путь ошибки инициализации контролов + throw; (#109)#114
Conversation
Make initialization of both visual control shells (TableControl, MbeTableEditorControl) transactional: HandleInitializationError now rolls back via CleanupRuntimeState before notifying the operator, and the catch site rethrows with `throw;` to preserve the original stack trace. A second init entry (put_DesignMode / OnFBLinkChanged) therefore retries from a clean slate instead of double-subscribing OnPermissionsChanged or leaking orphaned coordinator/presenter instances. Add an invariant comment at the MarkInitialized call site documenting why the flag is set before the tail steps and why those steps must not throw. The no-handle branch of TryReadFromPlc (TableControl only) now logs a faulted discarded ReceiveRecipeAsync task via the extracted, unit-tested TaskFaultLogger; the architecture invariant is recorded in architecture.md. Closes #109 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the implementation plan and the multi-agent verification findings: Problem 1's named trigger refuted, the real defect identified as non-transactional control init, and the chosen transactional-rollback fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EvaTheSalmon
left a comment
There was a problem hiding this comment.
Ревью прошло: все заявления в описании PR подтверждены по коду.
- Сборка чистая, 293/293 теста зелёные,
dotnet formatбез замечаний по затронутым файлам. CleanupRuntimeStateидемпотентен и не бросает (всё черезSafeDisposal.RunAll,ResetRuntimeFields— чистое обнуление полей), так чтоthrow;гарантированно доносит оригинальное исключение до движка.- Оба контрола расходятся ровно на документированную разницу
TryReadFromPlc— без скрытой дивергенции.
Одно мелкое замечание, не блокер:
TableControl.Lifecycle.cs:160-170 — ветка IsHandleCreated в TryReadFromPlc глотает фолт через catch { /* ignored */ }, тогда как no-handle ветка теперь логирует через TaskFaultLogger. Один и тот же сбой в одной ветке молчит, в другой логируется. Либо комментарий, почему глотание намеренное, либо унифицировать обе ветки на TaskFaultLogger.
Review on #114: the has-handle branch swallowed faults via an empty catch in an async void lambda while the no-handle branch logged through TaskFaultLogger. Both branches now share StartInitialPlcRead, which snapshots _presenter (the BeginInvoke-posted call runs after cleanup may have nulled the field) and observes the task via TaskFaultLogger. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Применено в 8182d70: обе ветки 293/293 зелёные, формат чистый. |
Closes #109.
Что сделано
Путь обработки ошибки инициализации обоих визуальных контролов (
TableControl,MbeTableEditorControl) сделан транзакционным:HandleInitializationErrorтеперь делаетLogCritical→CleanupRuntimeState()→MessageBox, после чего исключение пробрасывается на местеcatchчерезthrow;(а неthrow ex;). Каждая попытка инициализации теперь либо завершается полностью, либо полностью откатывается с безопасным повторным входом.Конкретные правки:
CleanupRuntimeState()перед пробросом — откат частично построенного состояния. Вызов сбрасывает_runtimeInitializedчерезResetRuntimeFields, снимает подписки и освобождает компоненты до того, как модальныйMessageBoxначнёт качать сообщения; верифицирован как идемпотентный и безопасный на частичном состоянии.throw;вместоthrow ex;в обоих контролах — сохраняется оригинальный стек-трейс. Проброс уходит за COM-границу в нативный движок через голые (без try/catch) платформенные тела, поэтому уничтоженный трейс был бы единственной диагностикой.TryReadFromPlc(толькоTableControl— в editor-вариантеTryReadFromPlcнет): логика вынесена в юнит-тестируемый хелперTaskFaultLogger.LogOnFault(OnlyOnFaulted,TaskScheduler.Default, лог распакованного inner exception). Обычный недостижимый-ПЛК случай уже логируется нижними слоями; хелпер закрывает лишь по-настоящему faulted-задачу, которую иначе не наблюдает никто.DisposeRuntimeComponentsобоих контролов (каждый в своёмSafeDisposal.RunAll) — бросок в одном Dispose больше не оставляет остальные компоненты подписанными на DI-singleton'ы; найдено ревью, тот же класс утечки, который закрывает PR.MarkInitialized()обоих контролов; инвариант транзакционной инициализации зафиксирован вDocs/architecture/architecture.md(раздел rendezvous protocol).Скорректированный анализ
Перед реализацией issue был проверён против кода, доков по архитектуре и декомпилированного SDK MasterSCADA 3.12. Анализ скорректирован, чтобы история issue не вводила в заблуждение:
_ = _presenter.ReceiveRecipeAsync()не может бросить синхронно — этоasync Task-метод, и компилятор захватывает все исключения тела (включая код до первогоawait) в возвращаемую задачу, а сбой отброшенной задачи до внешнегоcatchне доходит.BusyStateManager.Enter()— этоInterlocked.Incrementплюс тривиальная аллокация, бросать нечему.ApplyInitialPermissionsуже полностью обёрнут в собственный try/catch. Предложенный в issue локальный try/catch вокруг tail-шагов адресует мёртвый путь и не реализован.MarkInitialized(классический NRE из-за рассогласования pin/XML,Docs/known_issues/09) пробрасывалось без очистки, при_runtimeInitialized == false. Две независимые точки входа —put_DesignModeиOnFBLinkChanged— могут сработать в рамках одного времени жизни контрола, а по декомпилированному SDKOnFBLinkChangedподнимается безусловно на каждом присвоении RID. Повторный вход проходит guard и заново прогоняет one-shot шаги, давая: (1) реальную двойную подпискуOnPermissionsChangedна singleton-еStateProvider(единственный-=снимает только одну); (2) осиротевшие pass-1 инстансыTableRenderCoordinator/TablePresenter, чьи подписки на DI-singleton'ы текут до смерти контейнера FB. Транзакционный откат закрывает оба канала.Тесты
+3 теста на
TaskFaultLogger(faulted → залогирован один раз с распакованным исключением; completed/canceled → не логируется; детерминированно через await континуации, без поллинга; recording-fake + FluentAssertions по конвенции сьюта). Итого 293 зелёных. Остальные изменённые строки — lifecycle-glue контролов, требующий живогоFBConnector/хоста и не конструируемый в тест-проекте; для них регрессионный гейт — существующий набор. Ручная проверка в MasterSCADA пройдена: форсированный сбой инициализации → откат → чистое переоткрытие, оба контрола.🤖 Generated with Claude Code