
## Снять зависший лок

`DELETE /v1/infra/servers/:id/lock`

Принудительно снимает зависший лок операции на сервере. Используйте, когда [`/exec`](./exec.md) или [`/deploy`](./deploy.md) оборвались со стороны клиента (потеряно соединение, локальный таймаут), но на сервере всё ещё активен лок и последующие вызовы возвращают `EXEC_BUSY`. Эндпоинт всегда возвращает 200: если лок держался на этой реплике — снят, в ответе `released: true` и `localLock: true`, а если нет — `released: false` (тоже успех).

Снятие лока рассылается на **все реплики** платформы (`broadcast: true`), поэтому эндпоинт снимает зависший лок и тогда, когда он держится на другой реплице — именно поэтому `released` может быть `false`, хотя лок реально был и снят на реплице-держателе. `released`/`localLock` описывают только текущую реплику и **не являются подтверждением снятия по всему флоту** — не опрашивайте эндпоинт в цикле до `released: true`. Вместо этого повторите операцию (`/exec`/`/deploy`) или, если `EXEC_BUSY` не уходит, вызовите [`/unstick`](/docs/infra/servers/unstick) — он доступен только для отдельной виртуальной машины (`kind: "STANDALONE"`). У galaxy-хоста и galaxy-приложения вызов отвечает `409 GALAXY_UNSTICK_UNSUPPORTED`, потому что exec-канал общий, и там остаётся повтор с интервалом из `Retry-After`, а при устойчивом отказе — обращение в поддержку. Доставка рассылки не подтверждается: при одной активной реплике или недоступности шины она ничего не делает. Гарантированное снятие зависшего `exec`-лока даёт серверный авто-сброс по времени жизни лока (TTL), разобранный ниже.

> **Используйте осторожно.** Снятие лока активной операции может привести к непредсказуемому состоянию сервера — одновременно запущенные `/exec` и `/deploy` могут конкурировать за файлы и systemd-юниты.

## Параметры

| Параметр | В | Тип | Обяз. | Описание |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | да | ID сервера |

Тело запроса пустое.

## Примеры

### curl — личный ключ

```bash
curl -X DELETE -H "X-Api-Key: YOUR_API_KEY" \
  https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/lock
```

### curl — OAuth-приложение

```bash
curl -X DELETE -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/lock
```

### JavaScript — личный ключ

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/lock`,
  {
    method: 'DELETE',
    headers: { 'X-Api-Key': 'YOUR_API_KEY' },
  }
)
const { data } = await res.json()

if (data.released) {
  console.log(`Снят лок ${data.operation}, держался ${data.ageMs}ms, до истечения было ${data.expiresInMs}ms`)
} else {
  // Локального лока не было, но снятие разослано на другие реплики (data.broadcast === true).
  console.log(data.message)
}
```

### JavaScript — OAuth-приложение

```javascript
await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/lock`,
  {
    method: 'DELETE',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Всегда `true` при вызове на существующем сервере |
| `data.released` | boolean | `true` — на этой реплице был активный лок, и он снят. `false` — локального лока не было. **Описывает только текущую реплику**, не весь флот |
| `data.localLock` | boolean | Держался ли лок именно на этой реплице (`true` при `released: true`) |
| `data.broadcast` | boolean | `true` — снятие лока разослано на остальные реплики. Доставка запрошена, но не подтверждена, а при одной активной реплике или недоступной шине рассылка ничего не делает |
| `data.operation` | string | Тип снятой операции: `"exec"` или `"deploy"`. Присутствует только при `released: true` |
| `data.ageMs` | number | Сколько миллисекунд лок был активен. Присутствует только при `released: true` |
| `data.expiresInMs` | number | Через сколько миллисекунд лок истёк бы сам. Присутствует только при `released: true` |
| `data.message` | string | Пояснение, что локального лока не было и снятие разослано на реплики. Присутствует только при `released: false` |

## Пример ответа

Лок был на этой реплице — снят:

```json
{
  "success": true,
  "data": {
    "released": true,
    "localLock": true,
    "broadcast": true,
    "operation": "deploy",
    "ageMs": 320000,
    "expiresInMs": 580000
  }
}
```

Локального лока не было (снятие разослано на другие реплики):

```json
{
  "success": true,
  "data": {
    "released": false,
    "localLock": false,
    "broadcast": true,
    "message": "No lock on this replica; a fleet-wide EXEC-lock release was broadcast to peers"
  }
}
```

## Пример ответа при ошибке

404 — сервер не найден:

```json
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Server not found"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — [Проектный ключ для деплоя](/docs/cowork/deploy-key) |
| 404 | `NOT_FOUND` | Сервер не существует или принадлежит другому API-ключу. Членство в команде разработки сервера эту операцию не открывает — она требует управляющего ключа при любой роли. |
| 429 | `RATE_LIMITED` | Превышен общий лимит запросов платформы |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

- **Один лок на сервер.** Защита от гонок между `/exec` и `/deploy` — они делят один общий лок. Невозможно заблокировать отдельную команду.
- **Автосброс при нормальном завершении.** Успех или ошибка `/exec`/`/deploy` снимают лок сами. `DELETE /lock` нужен только когда клиент оборвал соединение до завершения или таймаут агента прошёл без отправки финального события.
- **TTL лока — автоистечение без вызова.** `/exec` ставит лок на `timeout + 30 + 60` секунд. `/deploy` — на 15 минут. Даже без `DELETE /lock` зависший лок истечёт сам.
- **Снятие рассылается на все реплики.** Платформа масштабируется горизонтально, и зависший лок держится на той реплике, которая его взяла, а она не обязательно та, что обслужила ваш `DELETE /lock`. Поэтому снятие всегда рассылается по флоту (`broadcast: true`) — так эндпоинт снимает лок и на реплике-держателе. Именно поэтому `released` может быть `false` при реально снятом локе: локальная реплика лока не держала.
- **Гарантированный авто-сброс зависшего `exec`-лока.** Даже без `DELETE /lock` зависший `exec`-лок гарантированно снимается серверным фоновым сбросом вскоре после истечения его TTL (порядка нескольких минут) — на случай, когда обработчик не смог снять лок сам. `DELETE /lock` — быстрый путь, авто-сброс — страховка. (Для `/deploy` авто-сброса нет: долгий деплой может легитимно превысить TTL. Зависший deploy-лок снимается вызовом `DELETE /lock` или сам по завершении операции.)
- **`released: false` — это успех**, не ошибка. Эндпоинт не возвращает 404 при отсутствии лока, потому что идея в том, чтобы «гарантировать, что лока нет». Клиенту не важно, был он или не был.
- **Работает в любом режиме сервера.** В отличие от большинства Deploy API эндпоинтов, `DELETE /lock` не требует BLACKHOLE — лок держится в памяти платформы, не зависит от агента.
- **Лок переживает удаление сервера.** Зависший лок переживает сам сервер: если предыдущий сервер удалён, а лок в памяти платформы остался, следующий деплой падает с `EXEC_BUSY`. `DELETE /lock` снимет такой лок и на удалённом сервере — при условии, что он всё ещё принадлежит вашему API-ключу (владение — единственная проверка, сам лок не хранит данных и не держит облачных ресурсов).

## Смотрите также

- [Выполнить команду](./exec.md)
- [Полный деплой](./deploy.md)
- [Метрики туннеля](./metrics.md)
