fix(core): idempotency pre-check без схемы — единый источник (live)
Код-ревью (раунд 5), п.1/6: pre-check брал схему из GET /instanceOperations/default/{opId},
а payload строился по живой схеме ?fields=cfsParams — два источника. default может
расходиться с живой => риск ложного пропуска modify.
Решение: pre-check вообще не запрашивает схему.
- modifierDesiredEqualsLive(ctx, uid, desired): сравнение по live-кодам
(live[lower(code)]), единственный источник — state.params.
- Значения: похожи на JSON ({/[) — смысловое сравнение; иначе скалярное с
нормализацией (null/"" -> ""; true/false без учёта регистра) — закрывает и
регистр bool.
- Fail-safe сохранён: пусто/нет кода/ошибка live => modify выполняется.
- Удалены: fetchOperationSchemaByID, modifierValuesEqual, lookupLiveParam больше
не участвует в pre-check (остаётся для досылки).
- Тесты переписаны (RawValuesEqual_Scalars/JSON, DesiredEqualsLive: 4 кейса).
Документация: TOOLS/ARCHITECTURE.md -> новый раздел «Modifier Idempotency»
(5 правил контракта); HISTORY — журнал раунда 5.
Проверено: go build ./... OK; go test ./internal/... -short PASS.
This commit is contained in:
@@ -134,6 +134,25 @@ Each operation has a kind:
|
||||
provider code nor the captured HAR contain `DELETE /instanceOperations/{uid}`.
|
||||
- Transient 401 is retried for GET (see `isRetryable`).
|
||||
|
||||
### Modifier Idempotency (decided 2026-09-30)
|
||||
|
||||
Modifiers may opt into an idempotency pre-check (`RunInstanceOperationUniversalByIdempotent`,
|
||||
used by `nubes_vc_nsxt_snat`). Rules:
|
||||
|
||||
- The pre-check runs **before** `POST /instanceOperations`. Otherwise a skipped modify
|
||||
leaves a created-but-never-run operation ("draft", pending) and the next call waits
|
||||
for idle until timeout.
|
||||
- The **only** comparison source is the live instance state (`state.params` via
|
||||
`instanceLiveParams`). The operation schema (`/instanceOperations/default/{opId}`,
|
||||
`?fields=cfsParams`) is **deliberately not used**: before the operation exists only the
|
||||
`default` schema is available, and it may diverge from the live one — comparing through
|
||||
it risks a false skip. (Using the live schema would require creating the operation
|
||||
first — which is exactly the regression above.)
|
||||
- Values are matched by the code itself (`live[lower(code)]`).
|
||||
- Fail-safe: empty live, code absent, or live read error → the modify runs (never skipped).
|
||||
- JSON-looking values (`{`/`[`) are compared semantically (key order ignored); scalars are
|
||||
normalized (`null`/`""` → empty; `true`/`false` case-insensitive).
|
||||
|
||||
### Generated Code Resilience
|
||||
|
||||
- **Partial state при ошибке create** (шаблон `instance.go`): если инстанс успел создаться
|
||||
|
||||
Reference in New Issue
Block a user