Files
story/scripts/load-privacy-delete.ps1
T

120 lines
7.4 KiB
PowerShell
Raw Normal View History

2026-08-26 06:41:07 +08:00
param(
[ValidateRange(1, 100)][int]$Count = 100,
[ValidateRange(10, 600)][int]$CompletionTimeoutSeconds = 180
)
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$projectRoot = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $projectRoot
$apiBase = 'http://localhost:58080'
$tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
$tenantId = '10000000-0000-0000-0000-000000000010'
$run = [Guid]::NewGuid().ToString('N')
$prefix = "privacy-capacity-$run"
$fixtureFileName = ".privacy-delete-$run.json"
$fixtureFile = Join-Path $projectRoot "test/load/$fixtureFileName"
$householdPublicId = $null
$householdInternalId = $null
function Invoke-Sql([string]$Sql) {
$result = $Sql | docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q -t -A
if ($LASTEXITCODE -ne 0) { throw 'Privacy capacity SQL failed' }
return ($result | Out-String).Trim()
}
function Get-OwnerToken {
return (Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'password'; client_id = 'voice-platform-web'; username = 'owner'; password = 'LocalOnly123!'; scope = 'openid'
}).access_token
}
try {
$ownerToken = Get-OwnerToken
$householdResponse = Invoke-RestMethod -Method Post -Uri "$apiBase/api/v1/households" -Headers @{
Authorization = "Bearer $ownerToken"
'X-Tenant-Id' = $tenantId
} -ContentType 'application/json' -Body (@{ name = "$prefix-household" } | ConvertTo-Json -Compress)
$householdPublicId = [string]$householdResponse.data.id
$householdEvidence = Invoke-Sql "SELECT h.id::text||'|'||h.owner_user_id::text||'|'||h.tenant_id::text FROM household h WHERE h.public_id='$householdPublicId'::uuid"
$householdParts = $householdEvidence -split '\|'
if ($householdParts.Count -ne 3) { throw "Cannot resolve capacity household: $householdEvidence" }
$householdInternalId = $householdParts[0]
$ownerInternalId = $householdParts[1]
$tenantId = $householdParts[2]
$setup = @"
INSERT INTO subject_profile(tenant_id,household_id,preferred_name,language,timezone,profile_data,conversation_preferences,created_by,updated_by)
SELECT '$tenantId'::uuid,'$householdInternalId'::uuid,'并发删除主体-'||lpad(g::text,3,'0'),'zh-CN','Asia/Shanghai',jsonb_build_object('capacity_run','$run'),'{}'::jsonb,'$ownerInternalId'::uuid,'$ownerInternalId'::uuid
FROM generate_series(1,$Count) g;
SELECT json_agg(json_build_object('subject_id',public_id::text,'preferred_name',preferred_name) ORDER BY preferred_name)::text
FROM subject_profile WHERE household_id='$householdInternalId'::uuid;
"@
$fixtureJson = ((Invoke-Sql $setup) -split "`n" | Select-Object -Last 1).Trim()
$fixtures = $fixtureJson | ConvertFrom-Json
if ($fixtures.Count -ne $Count) { throw "Expected $Count subjects but created $($fixtures.Count)" }
[IO.File]::WriteAllText($fixtureFile, $fixtureJson, [Text.UTF8Encoding]::new($false))
$startedAt = [DateTime]::UtcNow
docker run --rm -v "${projectRoot}/test/load:/scripts:ro" grafana/k6 run `
-e "API_BASE=http://host.docker.internal:58080" `
-e "ACCESS_TOKEN=$ownerToken" `
-e "FIXTURE_FILE=/scripts/$fixtureFileName" `
-e "VUS=$Count" `
/scripts/privacy-deletes.js
if ($LASTEXITCODE -ne 0) { throw 'Concurrent privacy delete submission thresholds failed' }
$submissionSeconds = ([DateTime]::UtcNow - $startedAt).TotalSeconds
$deadline = [DateTime]::UtcNow.AddSeconds($CompletionTimeoutSeconds)
do {
$evidence = Invoke-Sql @"
SELECT count(*)||'|'||
count(*) FILTER(WHERE pr.status='COMPLETED')||'|'||
count(*) FILTER(WHERE pr.status='PARTIAL_FAILED')||'|'||
count(*) FILTER(WHERE pr.status NOT IN('COMPLETED','PARTIAL_FAILED'))||'|'||
(SELECT count(*) FROM privacy_subtask ps JOIN privacy_request px ON px.id=ps.privacy_request_id JOIN subject_profile spx ON spx.id=px.subject_id WHERE spx.household_id='$householdInternalId'::uuid)||'|'||
(SELECT count(*) FROM tombstone t JOIN privacy_request px ON px.id=t.deletion_request_id JOIN subject_profile spx ON spx.id=px.subject_id WHERE spx.household_id='$householdInternalId'::uuid)
FROM privacy_request pr JOIN subject_profile sp ON sp.id=pr.subject_id
WHERE sp.household_id='$householdInternalId'::uuid;
"@
$parts = $evidence -split '\|'
if ($parts.Count -eq 6 -and [int]$parts[1] -eq $Count) { break }
Start-Sleep -Milliseconds 500
} while ([DateTime]::UtcNow -lt $deadline)
$completionSeconds = ([DateTime]::UtcNow - $startedAt).TotalSeconds
$expectedSubtasks = $Count * 6
if ($parts.Count -ne 6 -or [int]$parts[0] -ne $Count -or [int]$parts[1] -ne $Count -or [int]$parts[2] -ne 0 -or [int]$parts[3] -ne 0 -or [int]$parts[4] -ne $expectedSubtasks -or [int]$parts[5] -ne $Count) {
$failed = Invoke-Sql "SELECT pr.status||':'||count(*) FROM privacy_request pr JOIN subject_profile sp ON sp.id=pr.subject_id WHERE sp.household_id='$householdInternalId'::uuid GROUP BY pr.status ORDER BY pr.status"
throw "Privacy delete reconciliation mismatch: evidence=$evidence statuses=$failed"
}
$profileEvidence = Invoke-Sql "SELECT count(*)||'|'||count(*) FILTER(WHERE status='deleted' AND preferred_name='[已删除]' AND profile_data='{}'::jsonb AND conversation_preferences='{}'::jsonb) FROM subject_profile WHERE household_id='$householdInternalId'::uuid"
if ($profileEvidence -ne "$Count|$Count") { throw "Subject redaction mismatch: $profileEvidence" }
$subtaskEvidence = Invoke-Sql "SELECT count(*)||'|'||count(*) FILTER(WHERE ps.status IN('COMPLETED','SKIPPED')) FROM privacy_subtask ps JOIN privacy_request pr ON pr.id=ps.privacy_request_id JOIN subject_profile sp ON sp.id=pr.subject_id WHERE sp.household_id='$householdInternalId'::uuid"
if ($subtaskEvidence -ne "$expectedSubtasks|$expectedSubtasks") { throw "Privacy subtask convergence mismatch: $subtaskEvidence" }
Write-Host ("Concurrent privacy deletion passed: requests={0}, submitted_seconds={1:N2}, completed_seconds={2:N2}, profiles_redacted={3}, subtasks={4}, tombstones={5}" -f $Count, $submissionSeconds, $completionSeconds, $Count, $expectedSubtasks, $Count)
}
finally {
if ($householdInternalId) {
$cleanup = @"
BEGIN;
CREATE TEMP TABLE capacity_subject ON COMMIT DROP AS SELECT id,public_id FROM subject_profile WHERE household_id='$householdInternalId'::uuid;
CREATE TEMP TABLE capacity_privacy ON COMMIT DROP AS SELECT id,public_id FROM privacy_request WHERE subject_id IN(SELECT id FROM capacity_subject);
DELETE FROM alert_incident WHERE resource_public_id IN(SELECT public_id FROM capacity_privacy);
DELETE FROM tombstone WHERE deletion_request_id IN(SELECT id FROM capacity_privacy);
DELETE FROM outbox_event WHERE aggregate_id IN(SELECT id FROM capacity_privacy);
DELETE FROM audit_event WHERE resource_id IN(SELECT public_id FROM capacity_privacy UNION SELECT public_id FROM capacity_subject UNION SELECT '$householdPublicId'::uuid);
DELETE FROM privacy_request WHERE id IN(SELECT id FROM capacity_privacy);
DELETE FROM subject_profile WHERE id IN(SELECT id FROM capacity_subject);
DELETE FROM household_member WHERE household_id='$householdInternalId'::uuid;
DELETE FROM household WHERE id='$householdInternalId'::uuid;
COMMIT;
"@
try { Invoke-Sql $cleanup | Out-Null } catch { Write-Warning "Capacity cleanup failed: $_" }
}
if (Test-Path -LiteralPath $fixtureFile) { Remove-Item -LiteralPath $fixtureFile -Force }
}