Спецификация канального уровня

Примечание

Канальные уровни теперь являются внутренними только для каналов и не используются как часть ASGI. Эта спецификация определяет, что Каналы и приложения, написанные с их использованием, ожидают от канального уровня.

Аннотация

Этот документ описывает набор стандартизированных определений для каналов и канального уровня, который обеспечивает механизм для отправки и получения сообщений по ним. Они обеспечивают межпроцессное взаимодействие между различными процессами, что помогает создавать приложения, в которых происходит обмен сообщениями и событиями между различными клиентами.

Обзор

Сообщения

Сообщения должны иметь вид dict. Поскольку эти сообщения иногда передаются по сети, они должны быть сериализуемыми, и поэтому им разрешено содержать только следующие типы:

  • Строки байтов

  • Строки Юникода

  • Целые числа (в пределах знакового 64-битного диапазона)

  • Числа с плавающей запятой (в диапазоне двойной точности IEEE 754)

  • Списки (кортежи должны быть закодированы как списки)

  • Дикты (ключи должны быть строками юникода)

  • Булевы

  • Нет

Каналы

Каналы идентифицируются строковым именем unicode, состоящим только из букв ASCII, цифровых цифр ASCII, точек (.), тире (-) и подчеркивания (_), плюс необязательный символ типа (см. ниже).

Каналы представляют собой очередь с семантикой доставки по принципу «первый вошел - первый вышел». Они могут иметь несколько писателей и несколько читателей; только один читатель должен получить каждое записанное сообщение. Реализации никогда не должны доставлять сообщение более одного раза или более чем одному читателю, и должны отбрасывать сообщения, если это необходимо для достижения этого ограничения.

Чтобы облегчить масштабирование и сетевую архитектуру, делается различие между каналами, которые имеют несколько читателей, и специфическими каналами, которые читаются из одного известного процесса.

Имена обычных каналов не содержат символов типа и могут быть направлены по желанию бэкенда; в частности, они не обязаны выглядеть глобально согласованными, и бэкенд может разделить их содержимое на разные серверы так, чтобы запрашивающий клиент видел только часть сообщений. Вызов receive на этих каналах не гарантирует, что вы получите сообщения по порядку или что вы получите что-либо, если канал не пуст.

Имена специфических каналов содержат восклицательный знак (!), который разделяет удаленную и локальную части. Эти каналы принимаются по-разному; только имя до символа ! включительно передается вызову receive(), и он примет любое сообщение на любом канале с таким префиксом. Это позволяет процессу, например, HTTP-терминатору, прослушивать один специфический для процесса канал, а затем распределять входящие запросы по соответствующим клиентским сокетам, используя локальную часть (часть после !). Локальные части должны генерироваться и управляться процессом, который их потребляет. Эти каналы, как и каналы с одним считывателем, гарантированно выдают все сохранившиеся сообщения по порядку, если они получены от одного процесса.

Сообщения должны истекать через заданное время, оставаясь непрочитанными в канале; рекомендация - одна минута, хотя наилучшее значение зависит от уровня канала и способа его развертывания, и рекомендуется разрешить пользователям настраивать время истечения.

Максимальный размер сообщения составляет 1 МБ, если сообщение закодировано в виде JSON; если необходимо передать больше данных, чем нужно, они должны быть разбиты на более мелкие сообщения. Все канальные уровни должны поддерживать сообщения до этого размера, но пользователям канальных уровней рекомендуется не превышать его.

Расширения

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

Здесь определены следующие расширения:

  • groups: Позволяет группировать каналы для обеспечения вещания; подробнее см. ниже.

  • flush: Позволяет упростить тестирование и разработку с использованием слоев каналов.

Существует возможность добавления дополнительных расширений; они могут быть определены в отдельной спецификации или в новой версии данной спецификации.

Если коду приложения требуется расширение, он должен как можно скорее проверить его наличие и выдать ошибку, если оно не предоставлено. Фреймворки должны поощрять необязательное использование расширений, пытаясь при этом перенести ошибки, связанные с ненахождением расширения, на запуск процесса, а не на обработку сообщений.

Асинхронная поддержка

Все канальные уровни должны предоставлять асинхронные (coroutine) методы для своих первичных конечных точек. Конечные пользователи смогут получить синхронные версии, используя обертку asgiref.sync.async_to_sync.

Группы

Хотя базовой модели канала достаточно для удовлетворения основных потребностей приложений, многие более продвинутые варианты использования асинхронных сообщений требуют уведомления многих пользователей одновременно о наступлении события - представьте, например, живой блог, где каждый зритель должен получить длинный ответ на опрос или пакет WebSocket при появлении новой записи.

Таким образом, существует опциональное расширение groups, которое позволяет проще передавать сообщения группам каналов. Конечные пользователи, конечно, могут использовать только имена каналов и прямую отправку, а вместо этого построить свою собственную систему постоянства/трансляции.

Вместимость

Для обеспечения противодавления каждый канал в канальном слое может иметь емкость, определяемую по желанию слоя (рекомендуется, чтобы она настраивалась пользователем с помощью аргументов ключевых слов в конструкторе канального слоя, а также настраивалась по имени канала или префиксу имени).

Когда канал находится на уровне или превышает пропускную способность, попытка send() на этот канал может вызвать ошибку ChannelFull, которая указывает отправителю на то, что канал превысил пропускную способность. Как отправитель хочет поступить в этом случае, зависит от контекста; например, веб-приложение, пытающееся отправить тело ответа, скорее всего, будет ждать, пока канал снова не опустеет, в то время как сервер интерфейса HTTP, пытающийся отправить запрос, отменит запрос и вернет ошибку 503.

Локальные каналы процесса должны использовать свою емкость на нелокальной части (то есть до символа ! включительно), поэтому емкость делится между всеми «виртуальными» каналами внутри него.

Отправка в группу никогда не поднимает ChannelFull; вместо этого она должна молча отбросить сообщение, если ее пропускная способность превышена, в соответствии с политикой доставки ASGI «не более одного раза».

Детали спецификации

Слой канала должен предоставить объект с этими атрибутами (все аргументы функции являются позиционными):

  • coroutine send(channel, message), который принимает два аргумента: канал для отправки, как строка юникода, и сообщение для отправки, как сериализуемое dict.

  • coroutine receive(channel), который принимает одно имя канала и возвращает следующее полученное сообщение на этом канале.

  • coroutine new_channel(), который возвращает новый специфичный для процесса канал, который можно использовать для передачи локальной coroutine или приемнику.

  • MessageTooLarge, исключение, возникающее при неудачной операции отправки из-за того, что закодированное сообщение превышает лимит размера уровня.

  • ChannelFull, исключение, возникающее при неудачной операции отправки из-за превышения пропускной способности канала назначения.

  • extensions, список имен строк unicode, указывающих, какие расширения предоставляет данный слой, или пустой список, если он не поддерживает ни одного. Возможные расширения можно увидеть в Расширения.

Канальный уровень, реализующий расширение groups, также должен обеспечивать:

  • coroutine group_add(group, channel), который принимает channel и добавляет его к группе, заданной group. Оба значения являются строками юникода. Если канал уже находится в группе, функция должна вернуться нормально.

  • coroutine group_discard(group, channel), который удаляет channel из group, если он находится в нем, и ничего не делает в противном случае.

  • coroutine group_send(group, message), который принимает два позиционных аргумента: группу для отправки в виде строки unicode и сообщение для отправки в виде сериализуемого dict. Он может вызвать сообщение MessageTooLarge, но не может вызвать ChannelFull.

  • group_expiry, целое число секунд, определяющее, как долго будет действовать членство в группе после последнего вызова group_add (см. раздел Persistence ниже).

Канальный уровень, реализующий расширение flush, также должен обеспечивать:

  • coroutine flush(), который сбрасывает канальный уровень в пустое состояние, не содержащее ни сообщений, ни групп (если реализовано расширение groups). Этот вызов должен блокироваться до тех пор, пока система не будет очищена, и будет постоянно выглядеть пустым для любого клиента, если канальный слой распределен.

Семантика каналов

Каналы должны:

  • Отлично сохраняет порядок сообщений при наличии только одного читателя и писателя, если канал является каналом одного читателя или специфическим каналом.

  • Никогда не передавайте сообщение более одного раза.

  • Никогда не блокируют отправку сообщения (хотя могут поднять ChannelFull или MessageTooLarge).

  • Уметь обрабатывать сообщения размером не менее 1 МБ в кодировке JSON (реализация может использовать лучшее кодирование или сжатие, но при условии, что она соответствует эквивалентному размеру).

  • Иметь максимальную длину имени не менее 100 байт.

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

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

Настойчивость

Канальные уровни не нуждаются в длительном хранении данных; членство в группах должно существовать только до тех пор, пока существует соединение, а сообщения - только до истечения срока действия сообщения, который обычно составляет пару минут.

Если канальный уровень реализует расширение groups, он должен сохранять членство в группе, по крайней мере, до того момента, когда на канале-члене истекает срок действия сообщения из-за непотребления, после чего он может отказаться от членства в любое время. Если канал впоследствии имеет успешную доставку, канальный уровень не должен терять членство в группе до истечения срока действия другого сообщения на этом канале.

Канальные уровни также должны отменять членство в группе после конфигурируемого длительного тайм-аута после последнего вызова group_add для этого членства, по умолчанию 86 400 секунд (один день). Значение этого тайм-аута отображается как свойство group_expiry на канальном уровне.

Примерный глобальный заказ

Хотя поддержание истинного глобального (по всем каналам) порядка сообщений совершенно неразумно ожидать от многих реализаций, они должны стремиться к тому, чтобы предотвратить перегрузку тихих каналов занятыми каналами.

Например, представьте себе два канала, busy, на который приходит до 1000 сообщений в секунду, и quiet, который получает одно сообщение в секунду. Имеется один потребитель receive(['busy', 'quiet']), который может обрабатывать около 200 сообщений в секунду.

В упрощенной реализации цикла for-loop канальный уровень может всегда проверять первым busy; он всегда имеет доступные сообщения, и поэтому потребитель никогда не увидит сообщение от quiet, даже если оно было отправлено с первой партией сообщений busy.

Простым способом решения этой проблемы является рандомизация порядка списка каналов при поиске сообщений внутри канального уровня; существуют и другие, более совершенные методы, но какой бы ни был выбран, он должен стараться избежать сценария, когда сообщение не получено только потому, что другой канал занят.

Строки и Юникод

В этом документе и всех подспецификациях байтовая строка означает str на Python 2 и bytes на Python 3. Если этот тип все еще поддерживает кодовые точки Unicode из-за базовой реализации, то любые значения должны находиться в диапазоне 0 - 255.

Unicode string означает unicode на Python 2 и str на Python 3. В этом документе никогда не будет указано просто string - все строки являются одним из этих двух точных типов.

Некоторые сериализаторы, такие как json, не могут различать байтовые строки и строки юникода; они должны содержать логику, позволяющую отнести один тип к другому (например, кодировать байтовые строки как строки юникода base64 с предшествующим специальным символом, например, U+FFFF).

Имена каналов и групп всегда являются строками Юникода, с дополнительным ограничением, что в них используются только следующие символы:

  • ASCII-буквы

  • Цифры от 0 до << 1 >>>.

  • Гифен -

  • Подчеркивание _

  • Период .

  • Знак вопроса ? (только для разграничения имен каналов с одним считывателем, и только по одному на имя)

  • Восклицательный знак ! (только для разграничения имен каналов, специфичных для процесса, и только один на имя)

Вернуться на верх