Введение
В предыдущих статьях ошибки gRPC обрабатывались минимально — простой проверкой status.ok(). В статье разберём более полную картину обработки ошибок: коды статуса gRPC, метаданные запросов/ответов, и deadline — механизм ограничения времени ожидания вызова, упоминавшийся как недостаток предыдущих примеров.
Концепция
grpc::Status содержит код ошибки из стандартизированного перечисления grpc::StatusCode (аналогичного по духу кодам HTTP-статуса, но специфичного для gRPC — NOT_FOUND, INVALID_ARGUMENT, DEADLINE_EXCEEDED, UNAVAILABLE, UNAUTHENTICATED и другие) и текстовое сообщение об ошибке. Метаданные — пары «ключ-значение», передаваемые отдельно от основного тела сообщения (аналог HTTP-заголовков), полезные для передачи дополнительной информации, не вписывающейся естественно в структуру .proto-сообщения (токены аутентификации, статья 329; информация для трассировки запросов). Deadline устанавливает абсолютный момент времени, после которого клиент перестаёт ждать ответа и считает вызов неудачным с кодом DEADLINE_EXCEEDED.
Пример кода: Обработка ошибок и статусов gRPC
// Установка deadline для клиентского вызова — ограничение времени ожидания ответа
std::optional<SensorReadingData> getLatestReadingWithDeadline(const QString &deviceId, int timeoutMs)
{
telemetry::ReadingRequest request;
request.set_device_id(deviceId.toStdString());
telemetry::SensorReading response;
grpc::ClientContext context;
// Deadline — абсолютный момент времени, а не просто длительность,
// что важно учитывать при повторных попытках (см. подводные камни)
auto deadline = std::chrono::system_clock::now() + std::chrono::milliseconds(timeoutMs);
context.set_deadline(deadline);
grpc::Status status = m_stub->GetLatestReading(&context, request, &response);
if (!status.ok()) {
switch (status.error_code()) {
case grpc::StatusCode::DEADLINE_EXCEEDED:
qWarning() << "Превышен таймаут ожидания ответа от сервера";
break;
case grpc::StatusCode::UNAVAILABLE:
qWarning() << "Сервер недоступен — возможно, стоит попробовать переподключиться";
break;
case grpc::StatusCode::NOT_FOUND:
qWarning() << "Устройство с указанным ID не найдено на сервере";
break;
default:
qWarning() << "Ошибка gRPC:" << status.error_code()
<< QString::fromStdString(status.error_message());
}
return std::nullopt;
}
return SensorReadingData{
QString::fromStdString(response.device_id()),
response.temperature(), response.humidity(), response.timestamp_unix()
};
}
// Использование метаданных — передача дополнительной информации вне тела сообщения
std::optional<SensorReadingData> getLatestReadingWithMetadata(const QString &deviceId, const QString &requestId)
{
telemetry::ReadingRequest request;
request.set_device_id(deviceId.toStdString());
grpc::ClientContext context;
// Метаданные передаются как обычные строковые пары "ключ-значение",
// независимо от структуры самого .proto-сообщения
context.AddMetadata("x-request-id", requestId.toStdString());
telemetry::SensorReading response;
grpc::Status status = m_stub->GetLatestReading(&context, request, &response);
// ... обработка результата, аналогично предыдущему примеру ...
return std::nullopt; // упрощено для примера
}
// server/telemetry_server.cpp — установка кода ошибки и чтение метаданных на сервере
grpc::Status GetLatestReading(grpc::ServerContext *context,
const telemetry::ReadingRequest *request,
telemetry::SensorReading *response) override
{
// Чтение метаданных, отправленных клиентом
const auto &clientMetadata = context->client_metadata();
auto it = clientMetadata.find("x-request-id");
if (it != clientMetadata.end()) {
std::string requestId(it->second.data(), it->second.size());
std::cout << "Обработка запроса с ID: " << requestId << std::endl;
}
if (request->device_id().empty()) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "device_id не может быть пустым");
}
if (!deviceExists(request->device_id())) {
return grpc::Status(grpc::StatusCode::NOT_FOUND, "Устройство не найдено");
}
// ... заполнение response ...
return grpc::Status::OK;
}
Пояснения к коду
getLatestReadingWithDeadline() показывает явную настройку дедлайна через context.set_deadline(), принимающую конкретную абсолютную временную точку (а не просто «сколько ждать» — это важное отличие от простого timeout), вычисленную как текущий момент плюс желаемая длительность ожидания. switch по status.error_code() показывает практичный паттерн дифференцированной обработки разных причин неудачи — DEADLINE_EXCEEDED и UNAVAILABLE предполагают возможность повторной попытки (временная проблема), тогда как NOT_FOUND указывает на содержательную проблему (запрошенное устройство просто не существует), для которой повторная попытка с теми же параметрами бессмысленна.
Метаданные (context.AddMetadata()/context->client_metadata()) показывают канал передачи информации, естественно не вписывающейся в само сообщение Protobuf — в примере это идентификатор запроса для целей логирования/трассировки, что аналогично заголовку X-Request-ID, часто используемому в REST API для тех же целей в основном цикле статей про сеть.
Подводные камни
- Установка одинакового deadline для каждой отдельной попытки при реализации логики повторных попыток (retry) без учёта общего, накопленного времени всех попыток — если клиент повторяет вызов несколько раз, каждый раз устанавливая deadline «сейчас + 5 секунд», суммарное время ожидания пользователя может многократно превысить ожидаемые 5 секунд; для корректной retry-логики deadline должен либо устанавливаться один раз на всю последовательность попыток, либо явно уменьшаться с каждой следующей попыткой, отражая реально оставшееся «терпение» пользователя.
- Слепое повторение неудачного вызова для любого кода ошибки без разбора, включая коды, для которых повторная попытка точно не поможет (
INVALID_ARGUMENT,NOT_FOUND,UNAUTHENTICATED) — бессмысленные повторные попытки для таких содержательных, а не временных ошибок только увеличивают нагрузку на сервер и задержку для пользователя без какой-либо реальной выгоды; логика повторных попыток должна явно проверять конкретный код ошибки перед решением повторить вызов. - Передача чувствительных данных (токенов аутентификации) через метаданные без TLS-шифрования транспорта — аналогично проблеме передачи паролей в открытом виде из статьи 318 про MQTT, метаданные gRPC передаются в открытом виде при использовании
InsecureChannelCredentials, и для передачи токенов аутентификации (статья 329) обязательно нужно защищённое, зашифрованное соединение, а не только сама механика передачи метаданных как таковая. - Несогласованность кодов ошибок между разными частями большой системы, если разные сервисы или разные разработчики команды используют коды gRPC непоследовательно (один сервис возвращает
INVALID_ARGUMENTтам, где другой в аналогичной ситуации возвращаетFAILED_PRECONDITION) — для проекта с несколькими gRPC-сервисами стоит выработать и явно задокументировать согласованное соглашение о том, какой код статуса используется в каких конкретно ситуациях, аналогично соглашениям об именовании MQTT-топиков из статьи 311.