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

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

Принудительно снимает зависший лок операции на сервере. Используйте, когда [`/exec`](./exec.md) или [`/deploy`](./deploy.md) оборвались со стороны клиента (потеряно соединение, локальный таймаут), но на сервере всё ещё активен лок и последующие вызовы возвращают `EXEC_BUSY`. Это ПОСЛЕДНИЙ шаг восстановления, а не первый: лок снимается БЕЗУСЛОВНО, поэтому сначала идут повторы по `Retry-After` и гардированный [`/unstick`](/docs/infra/servers/unstick), и только потом этот вызов — после того как простой сервера подтверждён независимо, его состоянием и логами. Эндпоинт всегда возвращает 200: если лок держался на этой реплике — снят, в ответе `released: true` и `localLock: true`, а если нет — `released: false` (тоже успех).

У вызова два разных охвата, и их важно не путать. НА СВОЕЙ реплике снятие безусловно — поэтому независимое подтверждение простоя обязательно именно здесь. РАССЫЛКА на **все реплики** (`broadcast: true`) уже, и намеренно: сосед освобождает замок, только если держит его сам и только если это `exec`; живой чужой `deploy` она не трогает. Поэтому эндпоинт снимает зависший `exec`-лок и тогда, когда он держится на другой реплике — именно поэтому `released` может быть `false`, хотя лок реально был и снят на реплице-держателе. `released`/`localLock` описывают только текущую реплику и **не являются подтверждением снятия по всему флоту** — не опрашивайте эндпоинт в цикле до `released: true`. Вместо этого прочитайте исход предыдущей операции перед решением о повторе (`/exec`/`/deploy`). Устойчивый `EXEC_BUSY` после этого вызова САМ ПО СЕБЕ не доказывает, что держит агент: рассылка намеренно не снимает чужой живой `deploy`-лок, поэтому это может быть и деплой на другой реплике. Различает их `error.hint`: есть `autoExpiresInSeconds` — платформенный лок, ждите его; нет — канал агента может занимать синхронная команда, а на `STANDALONE` — также длительная задача. Сначала прочитайте [операции сервера](./operations.md) с `kind=exec` и их [исходы](./operation-status.md). Отсутствие поля не доказывает утечку мьютекса. Гардированный [`/unstick`](/docs/infra/servers/unstick) подходит только для подтверждённо зависшего синхронного мьютекса независимо подтверждённой простаивающей отдельной машины (`kind: "STANDALONE"`). У galaxy-хоста и galaxy-приложения вызов отвечает `409 GALAXY_UNSTICK_UNSUPPORTED`, потому что exec-канал общий, и там остаётся повтор с интервалом из `Retry-After`, а при устойчивом отказе — обращение в поддержку. Доставка рассылки не подтверждается: при одной активной реплике или недоступности шины она ничего не делает. Гарантированное снятие зависшего `exec`-лока даёт серверный авто-сброс по времени жизни лока (TTL), разобранный ниже.

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

На `STANDALONE` работающая длительная задача занимает канал после освобождения платформенного лока с `202`. Опрашивайте её операцию; при неизвестном исходе нужны поддержка и независимое подтверждение полной остановки. Отсутствие `autoExpiresInSeconds` не доказывает утечку мьютекса. Восстановление туннеля и удаление лока не снимают ограничение приёма длительной задачи. В Galaxy команды выполняются синхронно с пределом 600 секунд и общим каналом для всех приложений и выкладок; задача соседнего приложения может не попасть в ваш список операций, поэтому пустой список не доказывает простой хоста.

## Параметры

| Параметр | В | Тип | Обяз. | Описание |
|----------|---|-----|:-----:|----------|
| `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` | Ключ не опознан: такой строки на платформе нет |
| 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)
