Agent Skills: Restate Cloud Troubleshooting — Полное руководство по диагностике и починке

Troubleshoot Restate Cloud failures via Admin API, including stuck invocations, determinism errors, and scheduler recovery. Use when user says "restate troubleshooting", "fix restate invocation", "почини restate", or asks to diagnose Restate errors.

UncategorizedID: tekliner/improvado-agentic-frameworks-and-skills/restate-cloud-troubleshooting

Install this agent skill to your local

pnpm dlx add-skill https://github.com/tekliner/improvado-agentic-frameworks-and-skills/tree/HEAD/skills/restate-cloud-troubleshooting

Skill Files

Browse the full folder contents for restate-cloud-troubleshooting.

Download Skill

Loading file tree…

skills/restate-cloud-troubleshooting/SKILL.md

Skill Metadata

Name
restate-cloud-troubleshooting
Description
Troubleshoot Restate Cloud failures via Admin API, including stuck invocations, determinism errors, and scheduler recovery. Use when user says "restate troubleshooting", "fix restate invocation", "почини restate", or asks to diagnose Restate errors.

Restate Cloud Troubleshooting — Полное руководство по диагностике и починке

Thesis: Пошаговое руководство по управлению инвокациями Restate Cloud через Admin API — как найти сломанные, убить зависшие, починить ошибки детерминизма, и перезапустить cron scheduler без потери данных.

Конфигурация

Endpoints

| Endpoint | URL | Порт | Назначение | |----------|-----|------|------------| | Ingress (вызов хендлеров) | https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud | 8080 | Вызов workflow/handler/send | | Admin (управление) | https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud | 9070 | Query, cancel, purge инвокаций | | Render (наш сервер) | https://gong-notion-jira-v6-pipeline-paid.onrender.com | 443 | HTTP сервер с Restate SDK |

Auth Header (для ВСЕХ запросов)

Authorization: Bearer key_10vasEHHClYrj3wDRagDcx1.GKNY4tR1QaqSxTfxBw8PPc2oSJ3hM4CMne6qdRXxL86T

Наши сервисы в Restate

| Сервис | Тип | Хендлеры | |--------|-----|----------| | GongPipeline | Workflow | run, approve, decline, request_edit, get_status | | GongCronScheduler | VirtualObject | start (Exclusive), stop (Exclusive), run_check (Exclusive), status (Shared) | | DmSpaceRegistry | VirtualObject | register_space, get_space, list_spaces |


1.0 Диагностика: Найти проблемные инвокации

1.1 Запрос через Admin API SQL

Admin API принимает SQL через POST /query. Ответ приходит в бинарном формате Apache Arrow — парсить через regex или strings.

# Все НЕ-completed инвокации (самый полезный запрос)
curl -s --max-time 15 \
  -H "Authorization: Bearer key_10vasEHHClYrj3wDRagDcx1.GKNY4tR1QaqSxTfxBw8PPc2oSJ3hM4CMne6qdRXxL86T" \
  -H "Content-Type: application/json" \
  -X POST "https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:9070/query" \
  -d "{\"query\": \"SELECT id, status, target_handler_name FROM sys_invocation WHERE status != 'completed'\"}" \
  2>&1 | python3 -c "
import sys, re
data = sys.stdin.buffer.read()
text = data.decode('utf-8', errors='replace')
ids = re.findall(r'inv_[A-Za-z0-9]+', text)
statuses = re.findall(r'(backing-off|pending|running|suspended|scheduled)', text)
handlers = re.findall(r'(run_check|start|stop|run|approve|decline)', text)
print(f'IDs: {ids}')
print(f'Statuses: {statuses}')
print(f'Handlers: {handlers}')
"

1.2 Статусы инвокаций

| Статус | Что значит | Действие | |--------|-----------|----------| | running | Сейчас выполняется | Ждать или kill | | suspended | Ждёт promise/sleep/timer | Обычно норм, но может блокировать exclusive | | backing-off | Ошибка, ретраится | Нужно purge! | | pending | В очереди | Cancel если не нужна | | scheduled | Запланирована на будущее | Cancel если не нужна | | completed | Завершена | Всё ок |

1.3 Фильтры по конкретному хендлеру

# Только run_check
-d "{\"query\": \"SELECT id, status FROM sys_invocation WHERE target_handler_name = 'run_check' AND status = 'backing-off'\"}"

# Только GongPipeline workflows
-d "{\"query\": \"SELECT id, status, target_handler_name FROM sys_invocation WHERE target_service_name = 'GongPipeline' AND status != 'completed'\"}"

1.4 ВАЖНО: Экранирование кавычек

  • Используй двойные кавычки для JSON, одинарные для SQL значений
  • НЕ используй bash single-quote экранирование ('\'') внутри JSON — ломает парсер
  • Правильно: -d "{\"query\": \"... WHERE status = 'backing-off'\"}"
  • Неправильно: -d '{"query": "... WHERE status = '\''backing-off'\''"}'

2.0 Убийство инвокаций

2.1 Три режима DELETE

BASE="https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:9070"
AUTH="Authorization: Bearer key_10vasEHHClYrj3wDRagDcx1.GKNY4tR1QaqSxTfxBw8PPc2oSJ3hM4CMne6qdRXxL86T"

# 1) Cancel (мягкий) — помечает для отмены, но может не сработать для backing-off
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/{inv_id}"

# 2) Kill — принудительное завершение
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/{inv_id}?mode=kill"

# 3) Purge — ПОЛНОЕ УДАЛЕНИЕ журнала (единственный способ для backing-off!)
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/{inv_id}?mode=purge"

2.2 Когда какой режим

| Ситуация | Режим | Почему | |----------|-------|--------| | Pending инвокация не нужна | cancel (без mode) | Мягкая отмена | | Running инвокация зависла | ?mode=kill | Принудительное завершение | | Backing-off из-за ошибки детерминизма | ?mode=purge | Только purge удаляет сломанный журнал! | | Suspended блокирует exclusive handler | ?mode=kill или ?mode=purge | Освобождает exclusive lock | | Suspended Workflow (ждёт promise) | ?mode=kill?mode=purge | Purge без kill НЕ работает для suspended! Сначала kill, потом purge | | PreviouslyAccepted при повторном запуске workflow | kill → purge старую | Workflow key занят, нужно очистить |

2.3 КРИТИЧНО: suspended workflow — kill ПЕРЕД purge

Проблема: Workflow в статусе suspended (ждёт ctx.promise()) не удаляется через purge напрямую. Purge молча игнорируется, инвокация остаётся.

Решение — двухшаговый kill+purge:

# Шаг 1: Kill (переводит suspended → completed/killed)
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/inv_XXXXX?mode=kill"
sleep 3

# Шаг 2: Purge (теперь работает)
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/inv_XXXXX?mode=purge"

Типичный сценарий: Хочешь перезапустить GongPipeline для call_id, но получаешь PreviouslyAccepted:

# 1. Найти инвокацию по ключу
curl -s -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "$ADMIN/query" \
  -d "{\"query\": \"SELECT id, status FROM sys_invocation WHERE target_service_key = 'CALL_ID_HERE'\"}"

# 2. Kill + Purge
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/inv_XXXXX?mode=kill"
sleep 3
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/inv_XXXXX?mode=purge"

# 3. Re-run
curl -s -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "$INGRESS/GongPipeline/CALL_ID_HERE/run/send" -d '"CALL_ID_HERE"'

2.4 КРИТИЧНО: backing-off лечится ТОЛЬКО через purge

Проблема: Когда код хендлера изменился (новые шаги, другой порядок), Restate при реплее журнала видит несовпадение и выбрасывает VMException:

restate_sdk_python_core.VMException: (570) Found a mismatch between the code paths
taken during the previous execution and the paths taken during this execution.
 - The previous execution ran and recorded: 'run' (index '3')
 - The current execution attempts to perform: 'get state'

Решение:

  1. Обычный DELETE и kill НЕ помогают — инвокация остаётся backing-off
  2. Только ?mode=purge удаляет сломанный журнал полностью
  3. После purge — перезапустить хендлер через /send
# Шаг 1: Purge
curl -s -H "$AUTH" -X DELETE "$BASE/invocations/inv_XXXXX?mode=purge"

# Шаг 2: Проверить что ушла
curl -s -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "$BASE/query" \
  -d "{\"query\": \"SELECT id FROM sys_invocation WHERE status = 'backing-off'\"}" \
  | python3 -c "import sys,re; ids=re.findall(r'inv_\w+',sys.stdin.buffer.read().decode('utf-8','replace')); print(ids or 'CLEAN')"

# Шаг 3: Перезапустить
curl -s -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:8080/GongCronScheduler/main/start/send" \
  -d '{"interval_minutes": 60}'

3.0 Управление Cron Scheduler

3.1 Проверить статус (Shared — всегда работает)

curl -s --max-time 15 \
  -H "Authorization: Bearer key_10vasEHHClYrj3wDRagDcx1.GKNY4tR1QaqSxTfxBw8PPc2oSJ3hM4CMne6qdRXxL86T" \
  "https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:8080/GongCronScheduler/main/status"

Ответ: {"running": true, "interval_minutes": 60, "lookback_hours": 168}

3.2 Start/Stop (Exclusive — может зависнуть!)

Проблема Exclusive хендлеров: start, stop, run_check — все Exclusive. Если один висит suspended/backing-off, остальные встают в очередь и таймаутят.

Решение: использовать /send (fire-and-forget)

# Stop (fire-and-forget)
curl -s --max-time 15 -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:8080/GongCronScheduler/main/stop/send"

# Start (fire-and-forget)
curl -s --max-time 15 -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:8080/GongCronScheduler/main/start/send" \
  -d '{"interval_minutes": 60}'

Ответ /send: {"invocationId": "inv_XXX", "status": "Accepted"} — значит принято в очередь.

3.3 Полный цикл перезапуска cron

AUTH="Authorization: Bearer key_10vasEHHClYrj3wDRagDcx1.GKNY4tR1QaqSxTfxBw8PPc2oSJ3hM4CMne6qdRXxL86T"
INGRESS="https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:8080"
ADMIN="https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:9070"

# 1. Найти проблемные инвокации
curl -s -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "$ADMIN/query" \
  -d "{\"query\": \"SELECT id, status FROM sys_invocation WHERE target_handler_name = 'run_check' AND status != 'completed'\"}" \
  | python3 -c "import sys,re; d=sys.stdin.buffer.read().decode('utf-8','replace'); print('IDs:', re.findall(r'inv_\w+',d)); print('Statuses:', re.findall(r'(backing-off|pending|running|suspended)',d))"

# 2. Purge каждую (подставить реальные ID)
curl -s -H "$AUTH" -X DELETE "$ADMIN/invocations/inv_XXXXX?mode=purge"

# 3. Stop → Start
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "$INGRESS/GongCronScheduler/main/stop/send"
sleep 3
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "$INGRESS/GongCronScheduler/main/start/send" -d '{"interval_minutes": 60}'

# 4. Проверить
sleep 5
curl -s -H "$AUTH" "$INGRESS/GongCronScheduler/main/status"

4.0 Перерегистрация деплоймента

После пуша в git и деплоя на Render — перерегистрировать в Restate Cloud:

curl -s --max-time 30 \
  -H "Authorization: Bearer key_10vasEHHClYrj3wDRagDcx1.GKNY4tR1QaqSxTfxBw8PPc2oSJ3hM4CMne6qdRXxL86T" \
  -H "Content-Type: application/json" \
  -X POST "https://201kgssd6gzxm8kdwyk3b58ygh7.env.us.restate.cloud:9070/deployments" \
  -d '{"uri": "https://gong-notion-jira-v6-pipeline-paid.onrender.com", "force": true}'

force: true — перезаписывает предыдущую регистрацию (иначе 409 conflict).

Важно: После перерегистрации старые running/suspended инвокации НЕ перезапускаются автоматически. Если они используют изменённый код — получишь VMException (570). Нужно purge + restart.

4.1 Когда НЕ нужна перерегистрация

Перерегистрация нужна ТОЛЬКО когда меняется интерфейс сервисов:

  • Новые хендлеры
  • Изменение сигнатур (аргументы, типы)
  • Новые/удалённые сервисы

НЕ нужна когда:

  • Изменилась только внутренняя логика хендлера (send_message → send_text, новый текст сообщений)
  • Поменялись вспомогательные функции
  • Обновились env переменные

Render задеплоит новый код, Restate вызовет те же хендлеры — просто внутри они работают по-другому.

4.2 Полный цикл деплоя (если нужна перерегистрация)

git push origin main
↓ (ждём 90-120 секунд пока Render задеплоит)
POST /deployments {force: true}
↓
Purge backing-off инвокаций (если есть)
↓
Stop/Start cron через /send
↓
Проверить /status

5.0 Запуск отдельных workflow (GongPipeline)

5.1 Прямой запуск для конкретного звонка

# Fire-and-forget (не ждём ответа)
curl -s --max-time 15 -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "$INGRESS/GongPipeline/{CALL_ID}/run/send" \
  -d '"CALL_ID_STRING"'

Важно: body — это JSON string (в кавычках), не объект!

5.2 Проверить статус workflow

curl -s --max-time 15 -H "$AUTH" \
  "$INGRESS/GongPipeline/{CALL_ID}/get_status"

5.3 Approve/Decline через API

curl -s --max-time 30 -H "$AUTH" -H "Content-Type: application/json" \
  -X POST "$INGRESS/GongPipeline/{CALL_ID}/approve" \
  -d '{"approved_by": "manual@api"}'

6.0 Тестирование Google Chat отправки

6.1 Эндпоинт /test-send

# По имени клиента
curl -s "https://gong-notion-jira-v6-pipeline-paid.onrender.com/test-send?client=HP"

# По имени + домену
curl -s "https://gong-notion-jira-v6-pipeline-paid.onrender.com/test-send?client=HP&domain=hp.com"

# С кастомным сообщением
curl -s "https://gong-notion-jira-v6-pipeline-paid.onrender.com/test-send?client=HP&message=Hello+test"

6.2 Что возвращает

{
  "ok": true,
  "space_id": "spaces/AAQAl4k8_EI",
  "steps": [
    {"step": "input", "client": "HP", "domain": "hp.com"},
    {"step": "cache_refresh", "status": "ok", "spaces_cached": 23},
    {"step": "find_space", "status": "found", "space_id": "spaces/AAQAl4k8_EI", "display_name": "Customer - HP (Onboarding)", "aliases": ["HP (Onboarding)", "HP", "Onboarding"]},
    {"step": "send_text", "status": "ok"}
  ]
}

6.3 Если spaces_cached = 0

Значит нет OAuth токена. Проверить env var GOOGLE_CHAT_OAUTH_TOKEN на Render:

# Генерация base64 из локального pickle
python3 -c "
import base64
with open('data_sources/google_shared/token_chat_write.pickle', 'rb') as f:
    print(base64.b64encode(f.read()).decode())
"

Установить в Render Dashboard → Environment → GOOGLE_CHAT_OAUTH_TOKEN.


7.0 Частые ошибки и решения

7.1 VMException (570) — Mismatch code paths

Причина: Код хендлера изменился, а старые инвокации реплеят старый журнал.

Решение: Purge всех backing-off + restart (см. §2.3)

7.2 Curl timeout на Exclusive хендлер

Причина: Другая Exclusive инвокация (suspended/backing-off) блокирует.

Решение: Найти блокирующую инвокацию → purge → повторить через /send

7.3 409 Conflict при запуске workflow

Это НОРМАЛЬНО! Restate дедуплицирует — workflow с таким key уже запущен. Если нужно перезапустить — сначала purge старый.

7.4 "No Google Chat OAuth token found"

Причина: На Render не установлена env GOOGLE_CHAT_OAUTH_TOKEN.

Решение: Сгенерировать base64 из pickle и установить (см. §6.3)

7.5 Render cold start timeout

Первый запрос после простоя может таймаутить (Render спинит сервис). Retry через 30 секунд.

7.6 "Message cannot have cards for requests carrying human credentials"

Причина: OAuth user credentials (pickle token из GOOGLE_CHAT_OAUTH_TOKEN) — это human credentials. Google Chat API запрещает отправку cardsV2 от имени пользователя.

Кто может что:

| Тип credentials | Текст (send_text) | Карточки (send_message + cardsV2) | |----------------|---------------------|--------------------------------------| | OAuth user (pickle) | OK | ЗАПРЕЩЕНО — ошибка 400 | | Service account (bot) | OK | OK |

Решение:

  • Спейсы (customer + team) → _space_resolver.send_text() (OAuth, только текст)
  • DM с кнопками → chat_bot.send_approval_dm() (service account, карточки OK)

Ссылки в тексте: Используй формат <url|label> — Google Chat делает их кликабельными:

*HP* — Create 2 ISD + 1 PS | Update ISD-123. <https://notion.so/abc|Notion> | <https://app.gong.io/call?id=123|Gong>

7.7 PreviouslyAccepted при повторном запуске workflow

Причина: Workflow с таким key уже существует (даже если suspended/completed). Restate хранит состояние workflow_completion_retention = 1 день.

Решение: kill → purge → re-run (см. §2.3)


Ground Truth

  • Restate Cloud env: 201kgssd6gzxm8kdwyk3b58ygh7
  • Render service: gong-notion-jira-v6-pipeline-paid
  • Server code: data_sources/gong/pipeline_v6_restate/server.py
  • Workflow code: data_sources/gong/pipeline_v6_restate/workflow.py
  • Space resolver: data_sources/gong/pipeline_v6_restate/gchat_space_resolver.py
  • OAuth token (local): data_sources/google_shared/token_chat_write.pickle
  • Team space ID: spaces/AAQABigi9dw (env: GCHAT_TEAM_SPACE_ID)
  • Date: 2026-02-08
  • Session: mm_a509a216-9abb-4fa8-9153-5db304d81ef3