# gRPC Streams Cheatsheet
Отдельное внимание стоит уделить работе с gRPC стримами, так как в ней чуть больше нюансов, чем при вызове унарных RPC.
Стримы можно сравнить с транзакциями, когда унарные RPC - это атоматрные операции. Во время транзакции много чего может пойти не так, и эти случаи нужно уметь распознавать и обрабатыавть.
## Основные коды при работе со стримами
Эти коды возвращаются в ошибках/исключениях **только** при непосредственных попытках что-то прочитать из стрима или записать в стрим!
* `EOF` - любая сторона (клиент или сервер) завершила стриминг данных (если стрим был серверным или двунаправленным) по любой причине: сеть оборвалась (уже после начала стриминга) или закончились данные;
* `Canceled` - *клиент* завершил стриминг данных (при чем это мог быть, как непосредственно код разработчика, так и сам gRPC фреймворк в силу наложенных ограничений наподобии `deadline`'ов).
**Важно**, что клиент тоже получит ошибку с таким кодом при попытке что-то сделать со стримом (помним, что со стримом даже со стороны клиента могут работать несколко паралелльных процессов);
* `DeadlineExceeded` - *клиент* завершил стриминг данных, а более точно: истекло максимальное время жизни стрима, установленное клиентом при его инициализации.
**Важно**, что клиент тоже получит ошибку с таким кодом при попытке что-то сделать со стримом (помним, что со стримом даже со стороны клиента могут работать несколко паралелльных процессов);;
* `ResourceExhausted` - из стрима пришло что-то нехорошее (например, сообщение, превышающее установленные лимиты по размеру);
* `Unavailable` - gRPC соединение/канал оборвалось и не может долго восстановиться;
## Советы и хорошие практики для разработчиков:
* Оборачивать стримы в каналы (Golang), генераторы (Python) или любые абстрактные интераторы в языках и работать с ними, закрывая их для дальнейшего чтения, если была получена какая-либо ошибки при чтении непосредственно из gRPC стрима;
* Для передачи метаинформации о стримах по возможности использовать gRPC MetaData, вместо первого сообщения в стрим и oneof;
* Не забывать ставить `deadline`'ы, `defer`'ить `cancel`'ы или вызывать `SendClose` со стороны клиента при стримминге;
* Не забывать вызывать `Close` у gRPC соединений/каналов, когда они пересоздаются или закрываются навсегда;
## Основные вопросы связанные со стримами:
### Когда использовать стримы?
1. Когда на каждый запрос необязательно должен быть возвращен ответ (асинхронное взаимодействие)
2. Когда нужно получить большой объем данных (например, выгрузку чего-либо без усложнения пагинацией)
### Когда начинается сетевое взаимодействие при работе со стримами?
Только при первой операции `Send()`/`Recv()`. Еще раз важно подчеркнуть, что при создании стрима ошибка возвращается только в случае мисконфига.
### Кто должен закрывать стрим?
Важно понимать, что gRPC стрим - это просто абстракция над gRPC каналом/соединением, закрытие которой - это просто отправка сигнала одной стороны для другой, чтобы они обе понимали, что данный экземпляр абстрации (конкретный стрим) более неактуален.
Клиенсткий - клиент, серверный - сервер. Правило следующее: тот, кто пишет данные, тот и должен закрывать.
Двунаправленный стрим по такой логике могут закрыть обе стороны.
### Как закрывается стрим со стороны клиента?
У клиентского стрима всегда есть метод, который отправляет серверу сигнал, после которого любая попытка у сервера что-то прочитать из стрима будет возвращать ошибку с кодом `EOF` или неявно выйдет из цикла итерации по стриму (если он реализован, как генератор).
### Как закрывается стрим со стороны сервера?
Со стороны сервера при работе со стримом таких методов нет, но это просто потому, что для сервера все еще проще - достаточно просто выйти из функции-обработчика стрима, далее gRPC фреймворк сам поймет и отправит нужный сигнал клиенту, после которого любая попытка у клиента что-то прочитать из стрима или записать в него будет возвращать ошибку с кодом `EOF`.
## Частые ошибочные практики и трудности при работе со стримами
* Возвращать ошибки с кодами `Canceled`, `DeadlineExceeded` явно со стороны сервера: считается, что такие ошибки могут быть инициированы только клиентом, так как он инициатор стриминга и может задать (и знать) любые deadline'ы, поэтому в случае легитимного прерывания стрима рекомендуется использовать код `Aborted`.
* Возвращать `Unavailable` со стороны сервера: считается, что если стрим дошел до обработчика сервера, то сетевая связность есть, в противном случае клиент может быть дезинформирован.
* Использовать "кастомные" сигналы для завершения стримов (если нужно создать "подстрим", значит где-то было принято плохое архитектурное решение).
* Промежуточные проксирующие компоненты могут ограничивать стриминг, так например, nginx может обрывать стримы после превышения количества данные отпавленных по стримам: при чем клиенту будет казаться, что это сервер завершил стрим, а серверу - что клиент.
***Как такое обнаруживать***: надо иметь четкую практику логироавния кодов ошибок на обоих сторонах (клиенте и сервере). Тогда такие ситуации легко обнаружить: если клиент и сервер получат разные коды при работе со стримом, например, клиент получит код `EOF` (будто сревер завершил стрим), а сервер со своей стороны получит код Canceled (будто стрим завершил клиент), значит стрим завершило какое-то промежуточное звено.
* Конкурентное чтение или запись со стримами небезопасны: технически такое поведение позволяется и никак не отслеживается, но могут возникать трудно отлавливаемые race condition'ы.
***Как такое обнаруживать***: если появились недетерменированные (невоспроизводимые стабильно) ошибки с потерей/испорченностью данных (коды `ResourceExhausted`, `Unknown`, `Internal`) и при этом gRPC контракты совместимы обратно, значит дело скорее всего в конкурентной записи/чтении из стрима. Только будте внимательны, при ошибках конкуретного чтения могут выдваться дезинформирующие сообщения вроде того, что сообщение слишком большого размера: это просто побочный эффект "кусочного" чтения цельных сообщений и парсинга их длины.
* Если при взаимодействии клиента с сервером могут возникать стримы с относительно продолжительным временем жизни, то лучше не задавать жесткое время жизни gRPC каналов/соединений на стороне сервера (так часто делают при реализации балансировки) - некоторые стримы могут начать рваться;
***Как такое обнаруживать***: обычно такое будет видно в логах на стороне клиентов - синхронное (одновременное) появление множества сообщений о разрыве соединений, а на сервере (при чем именно на одном поде) - синхронно появится множество сообщений о том, будто это клиенты решили разом завершить соединения (как правило, это код ошибки `EOF`).
* Если могут возникать стримы с потенциально неограниченным временем жизни, то использование server.GracefullShutdown равносильно дедлоку или бессмысленно, если не рвать такие стримы по сигналу.
***Как такое обнаруживать***: об этом скорее всего скажут девопсы, так как время завершения подов при подачи им соответствующего сигнала будет равно максимальному времени, которое настроено в K8S.