v5.0.64: delete idempotency, duplicate adopt, defer unlock, exist marker

This commit is contained in:
“Naeel”
2026-07-07 10:08:50 +04:00
parent 4ac21d9367
commit d70bf5d171
12 changed files with 349 additions and 73 deletions
@@ -0,0 +1,98 @@
# 2026-07-07 — Opus answers: duplicate/exist fix strategy
## Контекст
Промпт из `/home/naeel/tf_provider/prompt_for_opus_duplicate.md`
После mutex (v5.0.63) pg_user_5 упал: «Операция вернула duplicate/exist, но объект не найден в state_out»
## Корень всех проблем
Код путает два разных случая «не-found»:
- `known && !found` — список в state_out ЕСТЬ, объекта нет → реальная несогласованность
- `!known` — списка в state_out НЕТ вообще (PostgreSQL, Kafka) → проверить невозможно
Сейчас `!known` трактуется как несогласованность → ошибка.
---
## Вопрос 1 (ПРИОРИТЕТ) — ДА, доверять API при `!known`
**Где:** templates.go → генерится во все subresource Create
**Фикс:** усыновлять при `found || !known`, ошибку только для `known && !found`:
```go
if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) {
found, known := false, false
if targetValue != "" {
var checkErr error
found, known, checkErr = resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
if checkErr != nil {
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
return
}
}
// found → подтверждено в state_out
// !known → списка в state_out нет (PG/Kafka) → доверяем API
// known&&!found → реальная несогласованность
if found || !known {
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
resp.Diagnostics.AddWarning("Подресурс усыновлён", "API вернул already exists; объект принят в state: "+targetValue)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
return
}
resp.Diagnostics.AddError("Нарушена консистентность", "API вернул duplicate/exist, но объект подтверждённо отсутствует в state_out: "+targetValue)
return
}
```
---
## Вопрос 2 — ДА, нужен явный маппинг из метаданных
**Где:** subresource_guard.go — `SubresourceListKey` (эвристика `name+"s"`) и `SubresourceIdentityKey` (special-map на 2 сервиса)
**Доказательство:** Kafka — реальные ключи `kafkaUsers`/`kafkaTopics`, эвристика даёт `users`/`topics``known=false` даже когда данные есть.
**Фикс:** генератор должен эмитить явные `listKey`/`idKey` как константы из метаданных. Для сервисов без ключа в state_out (PostgreSQL) — эмитить `listKey=""``FindSubresourceInStateOut``known=false` → работает путь «доверять API» из Вопроса 1.
---
## Вопрос 3 — НЕТ, list-эндпоинтов не существует
**Проверено по API-метаданным** (svc 90):
- Операции: create(19), delete(20), resume(114), suspend(115), recovery(47), modify(48), restart(54)
- Subresource: create_user(241), delete_user(244), create_database(245), delete_database(246)
- **Ни одной `list_*`/`get_*`/`show_*`**
Перечислить подресурсы через API невозможно. Единственный источник — state_out, а у PG его нет.
**Вывод:** стратегия «доверять API» (Вопрос 1) — единственно возможная.
---
## Вопрос 4 — Частично; текст ОК, нужен HTTP 409
**Где:** subresource_guard.go — `IsSubresourceAlreadyExistsError`
- `role "pg_user_5" already exists` → ловится ✅
- `already exist` (без s), голый `exist` → НЕ ловится
- **Главная дыра:** HTTP 409 без текста → НЕ ловится
**Фикс:**
```go
markers := []string{"уже существует", "already exists", "already exist", "duplicate", "conflict", " exist"}
```
Плюс проверка HTTP 409 (требует проброса статус-кода из клиента).
---
## Приоритет реализации
1. **Вопрос 1** — чинит `pg_user_5` сразу
2. **Вопрос 2** — чинит Kafka и остальные
3. **Вопрос 4** — устойчивость к формулировкам
4. **Вопрос 3** — документировать как «невозможно, дизайн-решение»
Всё правится в шаблоне templates.go и subresource_guard.go. Сгенерированные файлы НЕ править — перегенерировать.
**Дата:** 2026-07-07
**Статус:** анализ завершён, правки НЕ внесены
@@ -0,0 +1,73 @@
# 2026-07-07 — Opus bug analysis: skip_missing_on_delete и связанные баги
## Prompt
Prompt из `/home/naeel/tf_provider/prompt_for_opus_bugs.md` — анализ 3 файлов на баги, аналогичные `skip_missing_on_delete`.
## Анализированные файлы
- `/home/naeel/tf_provider/TOOLS/resource-generator/internal/templates/templates.go`
- `/home/naeel/tf_provider/provider/internal/resources_core/crud.go`
- `/home/naeel/tf_provider/generated/test/go/90_postgres_database_resource.go`
- `/home/naeel/tf_provider/provider/internal/resources_core/subresource_guard.go`
- `/home/naeel/tf_provider/provider/internal/core/client.go`
---
## Найденные баги
### 1. 🔴 КРИТ — Неидемпотентный Delete подресурса
**Где:** templates.go / 90_postgres_database_resource.go (сгенерированный код)
**Симптом:** если объект удалён вне Terraform, `terraform destroy` падает («Подресурс не найден») вместо успешного удаления из state — ресурс залипает в state навсегда.
**Причина:** pre-check `if known && !found { ... AddError }`. Delete отсутствующего объекта по контракту Terraform = успех, а тут ошибка. Дефолт `skip_missing_on_delete=false` лишь усугубляет — это не корень.
**Исправление:** при `known && !found` в Delete всегда `return` (успех, объект уже удалён), а не AddError. Флаг `skip_missing_on_delete` тогда становится не нужен.
### 2. 🔴 — skip_missing_on_delete default false
**Где:** templates.go (шаблон генератора)
**Симптом:** тот же не-идемпотентный destroy.
**Причина:** `booldefault.StaticBool(false)`.
**Исправление:** дефолт `true` (либо убрать параметр целиком в пользу фикса №1).
### 3. 🟡 — IsSubresourceMissingError — хрупкий матчинг
**Где:** subresource_guard.go
**Симптом:** delete падает как generic-ошибка, если API вернул иную формулировку.
**Причина:** матчинг только по 4 подстрокам: `"не найден"`, `"not found"`, `"does not exist"`, `"отсутствует"`. Пропустит `"не существует"`, `"no such database"`, голый `HTTP 404`, `"unknown object"`. Плюс ложные срабатывания — `"not found"` может встретиться в несвязанном тексте.
**Исправление:** расширить маркеры (`"не существует"`, `"no such"`, `404`) + учитывать HTTP-код, а не только текст.
### 4. 🟡 — LockInstance — только в памяти процесса
**Где:** client.go
**Симптом:** два параллельных `terraform apply` (CI, разные процессы) не сериализуются → orchestrator error / рассинхрон state_out.
**Причина:** mutex хранится в `sync.Map` в памяти процесса. Instance-level mutex из v5.0.63 защищает только внутри одного provider-процесса.
**Исправление:** документировать ограничение; для кросс-процессной защиты нужен серверный lock/ретрай на 409/«operation in progress».
### 5. 🔴 КРИТ — adoptExistingInstanceOnCreate: unlock без defer → deadlock
**Где:** crud.go, кейсы `StateNotCreated` / `StateSuspended`
**Симптом:** при панике в `RunInstanceOperationUniversal` мьютекс инстанса не освобождается → вечный deadlock на этом инстансе.
**Причина:** `unlock := client.LockInstance(...)` вызывается вручную, не через `defer`.
**Исправление:** обернуть в отдельную функцию с `defer unlock()`.
### 6. 🟡 — CreateResourceWithTimeout: TOCTOU
**Где:** crud.go
**Симптом:** между `FindInstanceByDisplayName` и adopt (resume/delete) состояние может измениться.
**Причина:** find выполняется вне лока (instanceUid ещё неизвестен).
**Исправление:** перепроверять состояние уже под локом внутри adopt перед resume/delete.
### 7. 🟡 — Read подресурса не проверяет state_out
**Где:** 90_postgres_database_resource.go
**Симптом:** drift (объект удалён вне Terraform) не обнаруживается; `plan` не покажет пересоздание, а `destroy` затем упадёт (баг №1).
**Причина:** `Read` просто переустанавливает state сам в себя, без проверки `FindSubresourceInStateOut`.
**Исправление:** в Read проверять наличие в state_out и `resp.State.RemoveResource(ctx)` если не найден.
---
## Хардкод-атрибуты instance-шаблона — дефолты ОК
- templates.go: `suspend_on_destroy`/`adopt_existing_on_create` — дефолты из конфига (корректны)
- `resource_name` (Required), `operation_timeout`/`log_level`/`git_revision` (Optional) — корректны
- `skip_missing_on_delete` в instance-шаблоне отсутствует (только у subresource) — правильно
---
## Итог
Главный корень — не-идемпотентный Delete подресурса (№1).
`skip_missing_on_delete=false` (№2) и хрупкий `IsSubresourceMissingError` (№3) — следствия/усилители.
**Дата:** 2026-07-07
**Статус:** анализ завершён, правки НЕ внесены
@@ -0,0 +1,35 @@
# 2026-07-07 — Opus analysis: duplicate/exist subresource adopt
## Контекст
После фикса mutex (v5.0.63) при параллельном создании subresource'ов pg_user_5 упал:
> «Операция вернула duplicate/exist, но объект не найден в state_out»
## Корневая причина (найдена)
PostgreSQL (svc 90), `state_out` инстанса НЕ содержит `databases` и `users` вообще.
Артефакт: `artifacts/output_inventory/running_suspended_output_fields_for_docs.json`
- PG out_paths: `externalConnect, internalConnect, monitoring` — ни баз, ни юзеров.
Для Kafka (svc 116) ключи есть, но называются `kafkaUsers` и `kafkaTopics` (не `users`/`topics`).
### Почему adopt subresource'а не работает
`subresource_guard.go`:
- `SubresourceListKey("database")` → эвристика `name+"s"``"databases"`
- `FindSubresourceInStateOut``details.RawOut["databases"]` → ключа нет → `known=false`
- Create-обработка duplicate требует `known && found`, но `known=false` всегда
- → падает в «Нарушена консистентность» всегда
Для основных ресурсов adopt работает через `client.FindInstanceByDisplayName` — реальный запрос к API инстансов. Для subresource'ов такого API нет.
## Дополнительные вопросы для Опуса
1. Есть ли в API отдельные эндпоинты `list_databases`/`list_users` для PostgreSQL, которые можно дёргать вместо парсинга `state_out`?
2. `SubresourceListKey`/`SubresourceIdentityKey` — эвристики `name+"s"` и special-map на 2 сервиса. Нужен явный маппинг из YAML-метаданных (реальные ключи: `kafkaUsers`, `kafkaTopics`).
3. При `known=false` (ключ не найден в state_out) — должен ли adopt доверять ошибке `already exists` от API и усыновлять без подтверждения? Сейчас падает в «Нарушена консистентность».
4. `IsSubresourceAlreadyExistsError` — достаточно ли маркеров `уже существует/already exists/duplicate/conflict`? Что если API вернёт HTTP 409 без текста или другую формулировку?
**Дата:** 2026-07-07
@@ -0,0 +1,34 @@
# 2026-07-07 — v5.0.64: Фикс идемпотентности subresource delete + duplicate adopt
## План правок
### 1. templates.go — delete идемпотентность
**Баг #1:** pre-check `known && !found → AddError` вместо молчаливого успеха.
**Фикс:** `known && !found` → return (успех, объект уже удалён). Только `known && !found` — ошибка.
### 2. templates.go — create duplicate adopt
**Баг:** adopt требует `known && found`, но `!known` (нет данных в state_out) → падает.
**Фикс:** `found || !known` — adopt при: найдено ИЛИ негде проверять.
### 3. templates.go — skip_missing_on_delete default true
**Баг:** дефолт `false` заставляет пользователя явно включать костыль.
**Фикс:** `StaticBool(true)`. После фикса #1 этот параметр больше не нужен для PG, но сохраняем для обратной совместимости.
### 4. crud.go — defer unlock
**Баг:** `unlock()` без `defer` → паника → deadlock.
**Фикс:** обернуть в анонимную функцию с `defer unlock()`.
### 5. subresource_guard.go — маркер " exist"
**Баг:** пропускает формулировки без `s` на конце.
**Фикс:** добавить `" exist"` в маркеры.
## Принципы
- Все правки в шаблоне/хелперах — универсальны для всех сервисов
- Никаких `if serviceID == 90`
- Сгенерированные файлы не трогать — перегенерировать
## Версия
v5.0.64
**Дата:** 2026-07-07
**Статус:** в реализации