- BrainTools - https://www.braintools.ru -

redb 3.6.0: багрепорт, который оказался в шести провайдерах сразу — плюс AS2-EDI и общий порт

redb ecosystem

redb ecosystem

Отчёт пользователя вскрыл утечку между диалогами в query-провайдере ядра — 6 из 6. Что ещё в 3.6.0: AS2/EDI, общий Kestrel, Camel-паритет.

Год стек рос на наших собственных задачах: мы писали то, что нужно было нам, и проверяли на своей проде. С весны им начали пользоваться посторонние люди — и характер входящих сообщений изменился. Вместо «а поддерживаете ли вы X» приходят разборы: воспроизведение, номера строк в наших исходниках, обходные пути, которые человек уже написал у себя, пока ждал ответа.

Это самое ценное, что может случиться с проектом. Спасибо всем, кто не поленился написать вместо того, чтобы молча уйти на другую библиотеку. Один такой отчёт лежит в основе половины этого релиза — и вскрытый им дефект оказался вовсе не там, где его нашли.

Итак, redb 3.6.0: ядро, интеграционный движок redb.Route, рантайм redb.Tsak и OIDC-сервер redb.Identity — все на одном номере, 63 пакета.

Дефект, который был на два слоя ниже, чем казался

Разработчик чат-продукта на redb.Route.Llm прислал: «первый ход нового человека приезжает в модель с историей другого человека, а его первое сообщение прицепляется в чужое дерево».

Звучит как баг LLM-коннектора — истории диалогов хранятся деревьями, что-то напутали с загрузкой ветки. Мы пошли смотреть и обнаружили, что коннектор ни при чём.

В query-провайдере ядра WhereLeaves() замещал корневой CTE вместо того, чтобы работать предикатом поверх него. То есть:

// Намерение: листья ЭТОГО дерева
var leaves = await redb.TreeQuery<MessageProps>(root)
    .WhereLeaves()
    .ToListAsync();

на деле означало «самые свежие листья схемы во всей базе». Фильтра по корню в запросе просто не оставалось.

Дальше арифметика простая. В многопользовательском проде «самый свежий лист схемы» — это транскрипт того, кто писал последним глобально. Не только на первом ходу: на любом. Все диалоги схлопываются в одну ленту. И хуже: следующее сообщение прицепляется родителем в чужое дерево, то есть данные не только читаются криво — они криво пишутся.

Дефект оказался кросс-провайдерным: шесть из шести — Postgres, MSSql, SQLite, каждый в Free и Pro. Чинили одинаково на всех: tree_leaves/tree_roots теперь засеиваются от корня, а не подменяют его.

Отдельная часть этой истории — SQLite. Free-тир исполняет запросы в нативном расширении, и правка живёт в redb_pvt.c, а не только в C#. Пакет со старыми бинарями отдал бы исправление на managed-стороне и оставил утечку в Free — молча, без единой ошибки [1] в логе. Поэтому расширение пересобрано под все три RID (win-x64, linux-x64, linux-arm64), и это проверено сверкой хешей против предыдущего релиза, а не по датам файлов: размеры linux-сборок совпали до байта, и метки времени тут ничего не доказывали.

Мораль, которую стоит забрать даже если вы не пользуетесь redb: фильтр, который «не сработал», и фильтр, который «заменил собой область поиска», выглядят в коде одинаково, а на данных различаются катастрофически. Второй не даёт ошибки — он даёт правдоподобный ответ не про то.

Второй дефект из того же отчёта: инструмент без контекста

Формулировка автора: «тулза получает обмен без контекста запроса — и единственный простой способ дать ей user id или tenant — просить модель передать его аргументом, что открывает prompt-injection-эскалацию».

Это точная постановка проблемы безопасности. Если инструмент узнаёт, от чьего имени его вызвали, из аргумента, который сформировала модель, — значит достаточно уговорить модель подставить туда чужой идентификатор.

Что было: движок копировал в обмен инструмента жёстко зашитый список из трёх заголовков и ничего больше. Что стало:

  • маршруты инструментов исполняются дочерним обменом агентского, а не «голым» — контекст, скоуп и трассировка наследуются;

  • принципал и аудит-теги доезжают до инструментов — причём как значения, вычисленные для прогона, а не как сырые заголовки. Разница существенная: ?user=${header.X-User-Id} и ?audit= разрешаются движком, и простое копирование заголовков их бы не поймало;

  • добавлен .PropagateToolHeaders(...) — явный список дополнительных заголовков, которые нужно пробросить; * на конце работает как префикс. Список по умолчанию пуст: проброс — осознанное решение, а не поведение [2] «на всякий случай».

route.From("http://0.0.0.0:8080/chat")
     .Llm("llm://openai/gpt-4o?user=${header.X-User-Id}&audit=tenant:${header.X-Tenant}")
     .PropagateToolHeaders("X-Tenant", "X-Request-*")
     .To("direct://reply");

Именно из-за этого метода релиз минорный, а не патч: новая публичная поверхность номером патча не выпускается.

AS2 и EDI: обмен документами с партнёрами прямо в маршруте

Крупная розница, 3PL-операторы, банки с host-to-host каналом, автопром — двадцать лет обмениваются заказами, счетами и уведомлениями об отгрузке по AS2: подписанный и зашифрованный S/MIME поверх HTTP с подписанной квиткой в ответ. В .NET до сих пор это означало коммерческий шлюз или Java-сервер сбоку от вашей интеграции.

Теперь AS2 — обычный коннектор redb.Route, схемы as2: и as2s:. Обе стороны, синхронный и асинхронный MDN, подписанная квитанция, полная матрица алгоритмов (sha-1/256/384/512 × aes-128/192/256-cbc3des, опциональное сжатие RFC 3274). Криптография — MimeKit поверх Bouncy Castle, то есть тот же фундамент, на котором AS2-индустрия и совместима.

Интероперабельность проверена против живого OpenAS2 v4.9.0 в обе стороны — подписанное и зашифрованное сообщение, положительный MDN, сверка MIC.

Подробный разбор с маршрутами, партнёрскими профилями и разницей со шлюзом — в отдельной статье про AS2 [3].

Один порт на несколько коннекторов

AS2-приёмник — это HTTP-сервер. У вас уже есть HTTP-маршруты. Партнёр хочет один эндпоинт, TLS терминируется один раз — значит и то, и другое должно жить на одном порту.

Раньше мультиплексор Kestrel (один сервер на пару host:port) лежал внутри redb.Route.Http. Чтобы AS2 мог им пользоваться, ему пришлось бы зависеть от коннектора общего назначения — а это тянет в приложение весь HTTP-транспорт ради одного класса и выворачивает направление зависимостей наизнанку.

Поэтому SharedHttpServerManager вынесен в отдельный пакет redb.Route.Http.Hosting [4]. Оба коннектора зависят от него, друг от друга — нет; регистрация идемпотентна, каждый резолвит один и тот же синглтон. Типы остались в namespace redb.Route.Http, так что существующий код править не нужно.

Здесь стоит рассказать честную часть, потому что она поучительна. Вынос приехал уже после того, как redb.Route.Http был опубликован на nuget.org [5]. Заменить опубликованную версию нельзя. А redb.Route.As [6]2 ссылается на Hosting, но не на Http — значит резолвер NuGet никогда бы сам не поднял Http до исправленной версии, и у пользователя связки «старый Http + As2» тихо оказались бы два независимых Kestrel: ровно та проблема, ради которой вынос и делался.

Лечится это не хитростью, а честным перевыпуском: вся линия redb.Route ушла одним номером, следом Tsak и Identity, чтобы их пины тоже переехали. Никакая комбинация 3.6.0 не соберёт битую пару.

Camel-паритет: ещё четыре вещи, которых не хватало

Message History — след, который обмен оставил в маршруте: каждый узел с временем, идентификатором и меткой; при исчерпании ретраев след выгружается в лог, и видно, на каком шаге всё встало.

XSLT — .Xslt(...).XsltContent(...) и компонент xslt:. Нужен ровно там, где нужен: чужие форматы, которые проще преобразовать таблицей стилей, чем кодом.

Routing Slip — .RoutingSlip(...): список эндпоинтов вычисляется во время выполнения, обмен идёт по нему последовательно.

Плейсхолдеры в URI эндпоинтов — {{key}} и {{key:default}}, как в Apache Camel. Один и тот же маршрут работает в трёх окружениях без ветвлений в коде.

IBM MQ: от полусекунды до единиц миллисекунд

Не в этом релизе, а в предыдущем, но заслуживает упоминания, потому что цифра говорит сама.

Штатный консьюмер опрашивал очередь блокирующим MQGET-WAIT на managed-клиенте IBM.WMQ, у которого внутренний тик ~500 мс и он не зависит от waitInterval. На простаивающей очереди сообщение доезжало через 250–500 мс после появления. Публичного async-API у managed-клиента нет ни в одной версии 9.4.x.

Приём переведён на событийную модель через MessageListener XMS — задержка упала до единиц миллисекунд. Режим включается явно; поллинг остался режимом по умолчанию.

Identity: уникальность e-mail держится индексом, а не проверкой

Регистрация работала так: посмотреть, есть ли пользователь с таким адресом → если есть, отдать 409 → если нет, создать. Между проверкой и вставкой две одновременные регистрации обе видят «свободно» и обе пишут один адрес. То есть RequireUniqueEmail под конкурентностью не держался. У логина такой проблемы не было никогда — там реляционный UNIQUE.

Теперь гарантию даёт база: частичный уникальный индекс UX_users_email на users(email) с условием WHERE _email IS NOT NULL. Частичный не для красоты — пустых адресов много, а у SQL Server обычный UNIQUE допускает лишь один NULL. Адрес нормализуется (trim + lower-invariant) перед проверкой и вставкой, иначе A@x.com [7] и a@x.com [8] были бы для индекса разными людьми. Нарушение индекса отдаётся как 409 duplicate, а не как 500.

Проверка перед вставкой осталась — но теперь она то, чем и должна быть: быстрый детерминированный ранний отказ, а не сама гарантия.

Заодно из горячего пути регистрации убраны отладочные леса: секундомер с двумя десятками вызовов-пустышек и VERIFY-блок, который после каждого создания слал два лишних запроса в базу, чтобы накормить эти пустышки. Минус два обращения к БД на регистрацию.

Tsak: dead-letter, который на PostgreSQL не работал

Очередь недоставленных сообщений писалась и читалась через Sql-компонент, и на PostgreSQL ломалась дважды.

Захват не сохранялся вообще: флаг replayable биндился как int 1/0 в колонку BOOLEAN, а в Postgres нет неявного приведения integer → boolean — весь INSERT падал с 42804. И собственный catch захвата это исключение проглатывал, так что недоставленные сообщения просто исчезали.

Второе: временные метки биндились ISO-строками против нативных timestamptz. Неявного text → timestamptz в операторе сравнения тоже нет, поэтому DELETE ... WHERE occurred_at < @cutoff падал с 42883 — то есть суточная джоба ретенции и любой запрос дашборда с фильтром по дате.

Оба лечения одинаковые по духу: биндить типами, а не текстом. bool → boolean/bit/0-1DateTimeOffset → нативный timestamp.

Мелочи, которые заметны

SumRedbAsync и AverageRedbAsync кидали InvalidOperationException на пустой выборке. SUM/AVG без строк — это NULL в SQL, а результат читался через JsonElement.GetDecimal, которому нужен Number. Путь стал достижимым только после того, как в 3.5.0 агрегаты по базовым полям начали учитывать фильтр: до этого WhereRedb(...) терялся, агрегат всегда шёл по всей схеме и NULL не получался никогда. Один фикс открыл дорогу другому багу — обычная история, если фильтр раньше молча игнорировался.

Fluent DSL и парсер URI разошлись в кодировании: строитель кодировал значения не так, как парсер их потом разбирал, и значения с пробелами не переживали круговой обход. Теперь обе стороны используют Uri.EscapeDataString.

Как поставить

dotnet add package redb.Core
dotnet add package redb.Postgres        # или redb.MSSql / redb.SQLite
dotnet add package redb.Postgres.Pro    # Pro — бесплатно, без ключа

dotnet add package redb.Route
dotnet add package redb.Route.As2

Pro остаётся проприетарным, но бесплатным и без лицензионного ключа на всей линии 3.x. Всё собрано под .NET 9.

Исходники — github.com/redbase-app [9], про хранилище — redb.ru [10].

Что дальше

Продолжаем разбирать входящее. Журнал отзывов ведётся в репозитории, и туда попадает каждый внешний сигнал — валидный, спорный и отклонённый, с оценкой и ссылкой на то, куда он зафиксирован. Если вы нашли что-то похожее на два дефекта выше — пишите: разбор с воспроизведением стоит дороже любого пожелания.

Другие мои статьи — redb.ru/articles [11], ещё — на Хабре [12].

If this was useful — a ⭐ on GitHub helps others find it.

Автор: grelikt

Источник [13]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/34378

URLs in this post:

[1] ошибки: http://www.braintools.ru/article/4192

[2] поведение: http://www.braintools.ru/article/9372

[3] отдельной статье про AS2: https://redb.ru/articles/as2-edi

[4] redb.Route.Http.Hosting: http://redb.Route.Http.Hosting

[5] nuget.org: http://nuget.org

[6] redb.Route.As: http://redb.Route.As

[7] A@x.com: mailto:A@x.com

[8] a@x.com: mailto:a@x.com

[9] github.com/redbase-app: https://github.com/redbase-app/

[10] redb.ru: http://redb.ru

[11] redb.ru/articles: https://redb.ru/articles

[12] Хабре: https://habr.com/ru/users/grelikt/articles/

[13] Источник: https://habr.com/ru/articles/1070192/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1070192

www.BrainTools.ru

Rambler's Top100