Предположим, что есть компонент A, который должен делать асинхронные запросы к компоненту B посредством пары сообщений request/response. Т.е. A отсылает request, B его обрабатывает, а затем отсылает response. Каждая пара request/response идентифицируется уникальным номером. Поскольку взаимодействие асинхронное, то A может отослать B сразу несколько request-ов (например, с номерами 1, 2, 3, 4 и 5). Компонент B будет отвечать по мере готовности и его ответы могут идти в совсем другом порядке. Например, сначала response с номером 4, затем 5, затем 2 и 1, и только затем 3. Компонент A на каждый запрос ждет ответа в течении определенного тайм-аута. Если ответ не пришел, то A повторяет запрос, но обязательно с тем же самым номером, что и в первый раз.
Это я сейчас описал первоначальную схему, очень простую, достаточно удобную и надежную. Но во время ее эксплуатации со временем стало очевидно, что увеличивать пропускную способность для такого рода протоколов оказывается не просто.
Проблема в возможности перепосылок сообщений request. Из-за этого компонент B получив очередной request должен проверить его уникальность. Если же запрос получен повторно, то он не должен обрабатываться. Тут все зависит от того, в какой стадии находится выполнение запроса – если запрос обрабатывается в данный момент, то повторный request игнорируется. А если запрос уже был обработан, то на повторный request нужно повторить ранее отосланный response.
Но и на стороне A нужно проверять уникальность response. Вполне может произойти ситуация, когда A может получить два повторных response с одинаковым идентификатором.
Т.е. и A, и B при получении очередной порции сообщений от удаленной стороны, должны сначала проверить уникальность полученных в сообщениях идентификаторов. Пока речь идет о сотнях сообщений в секунду, это не проблема. Но когда счет начинает идти о тысяче в секунду и выше, тут уже приходится выкручиваться.
И вот в качестве одного из решений было предложено объединять request и response в пакеты. Т.е. компоненты A и B переходят на взаимодействие посредством сообщений request_pkg и response_pkg, внутри которых уже собраны request-ы и respons-ы.
Например, компонент A отсылает request_pkg с идентификатором 1, в котором находятся request-ы с номерами 1, 2, 3, 4, 5. Компонент B сначала присылает request_pkg_ack с идентификатором 1 – это есть подтверждение того, что запросы дошли до компонента B. Если request_pkg_ack до A не дойдет за положенное время, то A вновь отсылает request_pkg с идентификатором 1 с точно таким же содержимым внутри.
По мере обработки запросов компонент B будет формировать response_pkg, в которые будет собирать готовые ответы. При этом размер и идентификатор response_pkg никак не связан с request_pkg. Так, компонент B может сначала отослать response_pkg с идентификатором 2, в котором будут response с номерами 3 и 5. А затем – response_pkg с идентификатором 3, в котором будут response с номерами 1, 2 и 4.
В ответ на response_pkg компонент A должен выслать response_pkg_ack. Если он этого не сделает, то после некоторого тайм-аута компонент B перепошлет response_pkg.
Краеугольными принципами этого пакетного протокола являются:
- если сообщение с идентификатором X включено в пакет с идентификатором Y, то оно может присутствовать только в пакете с идентификатором Y и ни в каком другом;
- состав конкретного пакета должен быть постоянным. Т.е. если в пакет с идентификатором Y входили сообщения с идентификаторами X, Z и W, то только эти сообщения и только в этом количестве должны присутствовать в пакете Y при всех его перепосылках.
Благодаря таким принципам, объем работы по проверке уникальности получаемых от удаленного компонента сообщений уменьшается в разы (пропорционально размеру пакета). Ведь сейчас нужно проверять уникальность не индивидуальных request/response, а уникальность пакетов.
Вот такая в итоге сложилась схема. В соответствии с ней для компонента A создана табличка responses в БД, первичным ключем в которой является уникальный номер request/response. Вставки строк в эту таблицу происходят при обработке response_pkg_ack.
Все вроде бы нормально. Но, допустим, что компонент B вдруг по какой-то причине нарушает принципы пакетного протокола. Скажем, из-за не выявленной вовремя ошибки в коде включает response с некоторым номером в два разных пакета.
Соответственно, при получении второго пакета с тем же самым response внутри, компонент A попытается добавить в табличку responses строку с неуникальным значением первичного ключа. И получит ошибку при работе с БД. Причем, поскольку компонент B будет перепосылать проблемный пакет (не получая из-за сбоев компонента A подтверждения), то компонент A будет “падать” снова и снова (не обязательно падать в прямом смысле, достаточно будет того, что он начнет тратить время на устранение этой ситуации).
Вот, собственно, хорошая ситуация для размышлений. Должны ли компоненты A и B защищаться от возможных ошибок в реализации друг друга? Особенно принимая во внимание, что компоненты разработаны одной командой, предназначены для работы в рамках одной задачи. Просто отвечают за разные куски функциональности.
PS. Это вопрос риторический. Я его озвучил не для того, чтобы посоветоваться с общественностью. Просто выступаю в роли акына – что вижу вокруг себя, то и пою :)