ms-ai-architect/skills/ms-ai-infrastructure/references/bcdr/state-management-failover.md
Kjell Tore Guttormsen ddce43d8b2 feat(ms-ai-architect): Spor 1 — Port-1-substrat migrert på 4 ikke-advisor-skills (243 Source + 327 Type + 325 TOC + stale-verified poison fjernet) [skip-docs]
Steg 9 (R4): unified migrate-corpus.mjs --write over engineering/governance/
infrastructure/security. 327 filer mutert, verified=null, prosa byte-identisk
(fra første ## seksjon), advisor urørt (0 endringer).

To applier-fixes oppdaget under kjøring (TDD, RED→GREEN):
- insertHeaderFields: anker faller nå tilbake når en meta-linje selv passerer
  500B (2 filer pakket et avsnitt i **Status:** → Type/Source landet utenfor
  scan-vinduet, applierens post-write-assertion fanget + restaurerte).
- normalizeStaleVerified: fjerner nå ALLE stale non-date **Verified:** i
  500B-vinduet, inkl. stray body-dup rett under --- (9 mlops-genaiops-filer var
  ellers falskt "verified"/fresh, droppet fra worklist). Operatør-godkjent
  utvidelse av carve-out; kun stray metadata-linjer, aldri prosa.

test-transform-criterion: precondition oppdatert til post-migrasjons-sannhet
(fila bærer nå Source). Suite 728/728 grønn.
2026-07-04 10:19:11 +02:00

418 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# State Management and Consistency During Failover
**Last updated:** 2026-06-24
**Status:** GA
**Category:** Business Continuity & Disaster Recovery
**Type:** reference
**Source:** https://learn.microsoft.com/azure/architecture/patterns/retry
---
## Innhold
- [Introduksjon](#introduksjon)
- [Distribuerte state management-mønstre](#distribuerte-state-management-mønstre)
- [Sesjonsstilstandsreplikering og synkronisering](#sesjonsstilstandsreplikering-og-synkronisering)
- [Håndtering av in-flight requests under failover](#håndtering-av-in-flight-requests-under-failover)
- [Idempotens og request retry-strategier](#idempotens-og-request-retry-strategier)
- [State validering og verifikasjonsprosedyrer](#state-validering-og-verifikasjonsprosedyrer)
- [Referanser](#referanser)
- [For Cosmo](#for-cosmo)
## Introduksjon
Håndtering av applikasjonstilstand (state) under failover-scenarioer er en av de mest utfordrende aspektene ved BCDR for AI-systemer. AI-applikasjoner har typisk flere typer state som må ivaretas: brukersesjoner, konversasjonshistorikk, mellomresultater fra langvarige operasjoner (fine-tuning, batch-indeksering), og applikasjonskonfigurasjon.
Under en failover kan in-flight requests gå tapt, sesjonsstilstand kan bli inkonsistent mellom regioner, og operasjoner som var halvveis fullført kan etterlate systemet i en ukjent tilstand. For å håndtere dette kreves distribuerte state management-mønstre, idempotente operasjoner, og robust request-retry logikk.
For norsk offentlig sektor er tap av state spesielt problematisk når AI-systemet støtter saksbehandling eller vedtaksfatting. Forvaltningsloven krever sporbarhet og etterrettelighet, noe som betyr at konversasjonshistorikk og AI-anbefalinger må bevares konsistent gjennom failover.
## Distribuerte state management-mønstre
### State-kategorier for AI-systemer
| State-type | Eksempel | Varighet | Kritikalitet | Lagring |
|-----------|---------|----------|-------------|---------|
| Session state | Autentiseringstoken, brukerpreferanser | Timer | Middels | Redis Cache / Cosmos DB |
| Conversation state | Chat-historikk, kontekstvindu | Dager | Høy | Cosmos DB |
| Operation state | Fine-tuning progress, batch-status | TimerDager | Middels | Queue + Cosmos DB |
| Configuration state | Model deployments, system prompts | Permanent | Kritisk | App Configuration / Git |
| Cache state | Søkeresultater, embeddings | MinutterTimer | Lav | Redis Cache |
### Distribuert state med Azure Cosmos DB
```python
# Distribuert state management for AI chatbot
from azure.cosmos.aio import CosmosClient
from azure.identity.aio import DefaultAzureCredential
import json
from datetime import datetime, timedelta
class DistributedStateManager:
"""Manage AI application state across regions with Cosmos DB."""
def __init__(self, connection_string, database_name="ai-state"):
self.client = CosmosClient.from_connection_string(connection_string)
self.database = self.client.get_database_client(database_name)
self.sessions = self.database.get_container_client("sessions")
self.conversations = self.database.get_container_client("conversations")
async def save_session(self, session_id: str, user_id: str, data: dict):
"""Save session state with TTL and version tracking."""
document = {
"id": session_id,
"userId": user_id,
"data": data,
"version": data.get("version", 0) + 1,
"lastUpdated": datetime.utcnow().isoformat(),
"ttl": 3600 * 24, # 24 timer TTL
"region": self._get_current_region()
}
await self.sessions.upsert_item(document)
return document["version"]
async def get_session(self, session_id: str, user_id: str):
"""Get session with partition key optimization."""
try:
response = await self.sessions.read_item(
item=session_id,
partition_key=user_id
)
return response
except Exception:
return None # Session not found
async def save_conversation_turn(
self, conversation_id: str, user_id: str, turn: dict
):
"""Append a conversation turn atomically."""
# Bruk conditional update for å unngå konflikter
conversation = await self._get_or_create_conversation(
conversation_id, user_id
)
# Legg til turn med unik ID for idempotens
turn["turnId"] = f"{conversation_id}-{len(conversation['turns'])}"
turn["timestamp"] = datetime.utcnow().isoformat()
conversation["turns"].append(turn)
conversation["lastUpdated"] = datetime.utcnow().isoformat()
# Conditional update med ETag for optimistisk locking
await self.conversations.replace_item(
item=conversation_id,
body=conversation,
match_condition=conversation.get("_etag")
)
def _get_current_region(self):
import os
return os.environ.get("AZURE_REGION", "unknown")
```
### Redis Cache for Session State
> **Merk (2026-06):** Azure Cache for Redis er under retirement (alle SKU-er; annonsert okt. 2025). For Basic/Standard/Premium blokkeres ny-provisjonering 1. apr. 2026 (nye kunder) og 1. okt. 2026 (eksisterende kunder); gjenværende instanser slås av fra 1. okt. 2028 (Enterprise/Enterprise Flash retires 31. mar. 2027). For nye løsninger, bruk **Azure Managed Redis** (active geo-replication, zone-redundant by default, Entra ID-autentisering). `az redis server-link`-geo-replikering under er fortsatt gyldig i levetiden, men planlegg migrering.
```bash
# Azure Cache for Redis med geo-replikering
# Primær region
az redis create \
--name "redis-ai-norwayeast" \
--resource-group "rg-ai-prod" \
--location "norwayeast" \
--sku "Premium" \
--vm-size "P1" \
--enable-non-ssl-port false \
--minimum-tls-version "1.2"
# Sekundær region (geo-replica)
az redis create \
--name "redis-ai-swedencentral" \
--resource-group "rg-ai-dr" \
--location "swedencentral" \
--sku "Premium" \
--vm-size "P1" \
--enable-non-ssl-port false
# Opprett geo-replikering
az redis server-link create \
--name "redis-ai-norwayeast" \
--resource-group "rg-ai-prod" \
--server-to-link "/subscriptions/{sub}/resourceGroups/rg-ai-dr/providers/Microsoft.Cache/Redis/redis-ai-swedencentral" \
--replication-role Secondary
```
## Sesjonsstilstandsreplikering og synkronisering
### Session Affinity vs. Shared State
| Tilnærming | Fordel | Ulempe | Anbefalt for |
|-----------|--------|--------|-------------|
| Session affinity (sticky) | Enkel, ingen replikering | Session tapt ved node-feil | Dev/test |
| Shared state (Redis) | Rask failover | Replikeringsforsinkelse | Produksjon |
| Shared state (Cosmos DB) | Global replikering | Høyere latens enn Redis | Multi-region |
| Stateless (JWT) | Ingen server-state | Begrenset datamengde | API-first design |
### Session migration under failover
```csharp
// C# Session migration strategy
public class ResilientSessionStore : ISessionStore
{
private readonly IDistributedCache _primaryCache;
private readonly IDistributedCache _secondaryCache;
private readonly CosmosClient _cosmosClient;
private bool _usingPrimary = true;
public async Task<SessionData?> GetSessionAsync(string sessionId)
{
var cache = _usingPrimary ? _primaryCache : _secondaryCache;
try
{
var data = await cache.GetStringAsync(sessionId);
if (data != null)
return JsonSerializer.Deserialize<SessionData>(data);
}
catch (RedisConnectionException)
{
// Redis failover
_usingPrimary = !_usingPrimary;
cache = _usingPrimary ? _primaryCache : _secondaryCache;
try
{
var data = await cache.GetStringAsync(sessionId);
if (data != null)
return JsonSerializer.Deserialize<SessionData>(data);
}
catch
{
// Begge Redis nede — fall tilbake til Cosmos DB
}
}
// Fallback: hent fra Cosmos DB (persistent store)
return await GetFromCosmosAsync(sessionId);
}
public async Task SaveSessionAsync(string sessionId, SessionData data)
{
// Skriv til Redis OG Cosmos DB (write-through)
var json = JsonSerializer.Serialize(data);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24)
};
// Redis (rask, men kan feile)
try
{
var cache = _usingPrimary ? _primaryCache : _secondaryCache;
await cache.SetStringAsync(sessionId, json, options);
}
catch { /* Redis-feil er ikke kritisk */ }
// Cosmos DB (persistent, geo-replikert)
await SaveToCosmosAsync(sessionId, data);
}
}
```
## Håndtering av in-flight requests under failover
### Request draining
```python
# Graceful request draining under failover
import asyncio
from contextlib import asynccontextmanager
class GracefulFailoverManager:
"""Manage in-flight requests during failover."""
def __init__(self, drain_timeout_seconds=30):
self.drain_timeout = drain_timeout_seconds
self.active_requests = 0
self.accepting_requests = True
self._lock = asyncio.Lock()
@asynccontextmanager
async def track_request(self):
"""Context manager to track active requests."""
async with self._lock:
if not self.accepting_requests:
raise ServiceUnavailableError(
"Service is draining for failover. "
"Please retry against the new endpoint."
)
self.active_requests += 1
try:
yield
finally:
async with self._lock:
self.active_requests -= 1
async def initiate_drain(self):
"""Stop accepting new requests and wait for in-flight to complete."""
async with self._lock:
self.accepting_requests = False
# Vent på at aktive requests fullføres
start = asyncio.get_event_loop().time()
while self.active_requests > 0:
elapsed = asyncio.get_event_loop().time() - start
if elapsed > self.drain_timeout:
print(f"Drain timeout! {self.active_requests} requests still active")
break
await asyncio.sleep(0.5)
return self.active_requests == 0
# Bruk i applikasjon
failover_mgr = GracefulFailoverManager(drain_timeout_seconds=30)
async def handle_chat_request(request):
async with failover_mgr.track_request():
response = await process_ai_request(request)
return response
```
## Idempotens og request retry-strategier
### Idempotent design for AI-operasjoner
```python
# Idempotent AI operations with deduplication
import hashlib
import json
class IdempotentAIService:
"""Ensure AI operations are idempotent using request IDs."""
def __init__(self, state_store, cache_ttl_seconds=3600):
self.state_store = state_store
self.cache_ttl = cache_ttl_seconds
def generate_idempotency_key(self, operation: str, params: dict) -> str:
"""Generate deterministic key for deduplication."""
canonical = json.dumps(params, sort_keys=True)
return hashlib.sha256(f"{operation}:{canonical}".encode()).hexdigest()
async def execute_idempotent(
self, operation: str, params: dict, execute_fn
):
"""Execute operation with idempotency guarantee."""
key = self.generate_idempotency_key(operation, params)
# Sjekk om operasjonen allerede er utført
existing = await self.state_store.get(f"idempotent:{key}")
if existing:
return json.loads(existing) # Returner cached resultat
# Utfør operasjonen
result = await execute_fn(params)
# Lagre resultat for deduplisering
await self.state_store.set(
f"idempotent:{key}",
json.dumps(result),
ttl=self.cache_ttl
)
return result
# Eksempel: Idempotent embedding-generering
service = IdempotentAIService(redis_store)
async def generate_embedding(text):
return await service.execute_idempotent(
operation="embed",
params={"text": text, "model": "text-embedding-3-large"},
execute_fn=lambda p: openai_client.embeddings.create(
input=p["text"], model=p["model"]
)
)
```
### Retry-strategi med idempotens
| Operasjonstype | Idempotent? | Retry-strategi | Max retries |
|---------------|-------------|---------------|-------------|
| Chat completion | Ja (med seed) | Exponential backoff | 3 |
| Embedding generation | Ja (deterministisk) | Fast retry | 3 |
| Search query | Ja (read-only) | Fast retry | 5 |
| Index update | Ja (upsert) | Exponential backoff | 3 |
| Fine-tuning start | Nei | Ingen retry | 0 |
| Conversation save | Conditional (ETag) | Exponential backoff | 3 |
## State validering og verifikasjonsprosedyrer
### Post-failover validering
```python
# Post-failover state validation checklist
async def validate_state_after_failover(primary_region, dr_region):
"""Validate state consistency after failover."""
results = {}
# 1. Verifiser session state
sample_sessions = await get_recent_sessions(limit=100)
session_ok = 0
for session in sample_sessions:
dr_session = await dr_state_store.get_session(session["id"])
if dr_session and dr_session["version"] >= session["version"] - 1:
session_ok += 1
results["sessions"] = {
"total": len(sample_sessions),
"consistent": session_ok,
"pct": round(session_ok / max(len(sample_sessions), 1) * 100, 1)
}
# 2. Verifiser conversation state
sample_convs = await get_recent_conversations(limit=50)
conv_ok = 0
for conv in sample_convs:
dr_conv = await dr_state_store.get_conversation(conv["id"])
if dr_conv and len(dr_conv["turns"]) >= len(conv["turns"]) - 1:
conv_ok += 1
results["conversations"] = {
"total": len(sample_convs),
"consistent": conv_ok,
"pct": round(conv_ok / max(len(sample_convs), 1) * 100, 1)
}
# 3. Verifiser configuration state
primary_config = await get_app_configuration(primary_region)
dr_config = await get_app_configuration(dr_region)
config_match = primary_config == dr_config
results["configuration"] = {"consistent": config_match}
# 4. Samlet vurdering
all_ok = (
results["sessions"]["pct"] > 95 and
results["conversations"]["pct"] > 95 and
results["configuration"]["consistent"]
)
results["overall"] = "PASS" if all_ok else "FAIL"
return results
```
## Referanser
- [Recommendations for handling transient faults](https://learn.microsoft.com/en-us/azure/well-architected/design-guides/handle-transient-faults) — Retry og idempotens
- [Retry pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/retry) — Retry-mønster
- [Designing Azure Functions for identical input](https://learn.microsoft.com/en-us/azure/azure-functions/functions-idempotent) — Idempotent design
- [Compensating Transaction pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction) — Kompenserende transaksjoner
- [Azure Cosmos DB consistency levels](https://learn.microsoft.com/en-us/azure/cosmos-db/consistency-levels) — Konsistensmodeller
- [Azure Cache for Redis geo-replication](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-how-to-geo-replication) — Redis geo-replikering
## For Cosmo
- **Bruk denne referansen** når kunden trenger hjelp med state management under failover for AI-applikasjoner.
- Anbefal alltid write-through til Cosmos DB selv om Redis brukes som primær session store — Redis-data kan gå tapt ved failover.
- Idempotens er OBLIGATORISK for alle AI-operasjoner som kan retries — bruk request IDs og conditional updates.
- For konversasjonshistorikk: Bruk append-only mønster med unik turnId for å unngå duplikater ved retry.
- Graceful request draining bør implementeres i alle produksjonsapplikasjoner — brå terminering av in-flight requests gir dårlig brukeropplevelse.