Merge branch 'feature/lic-05-speicherverbrauch-metrik-je-tenant' into feature/qa-05-abnahme-compliance-pruefung-core
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// StorageBytesMetric ist der feste Metrikname, unter dem der belegte
|
||||
// Speicherplatz je Tenant gefuehrt wird (LIC-05, siehe
|
||||
// core-kanban/tickets/LIC-05.md). LIC-03 fragt genau diese Metrik ueber
|
||||
// Store.Get/Store.Check ab — kein zweiter, paralleler Speicher-Zaehler.
|
||||
const StorageBytesMetric = "storage_bytes"
|
||||
|
||||
// ReportStorageWrite wird von den Objekt-Storage-Treibern der Module (DMS
|
||||
// FDN-03, Mail ARC-01 — existieren als Code noch nicht) bei jedem
|
||||
// Schreibvorgang aufgerufen. Nutzt Store.Increment, das bereits atomar ist
|
||||
// (Akzeptanzkriterium 2, siehe LIC-03) — kein zweiter Inkrement-Mechanismus
|
||||
// nur fuer Speicher.
|
||||
func (s *Store) ReportStorageWrite(ctx context.Context, tenantID string, sizeBytes int64) error {
|
||||
if sizeBytes < 0 {
|
||||
return errors.New("usage: sizeBytes darf bei einem schreibvorgang nicht negativ sein")
|
||||
}
|
||||
if err := s.Increment(ctx, tenantID, StorageBytesMetric, sizeBytes); err != nil {
|
||||
return fmt.Errorf("speicherverbrauch (schreiben) melden: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportStorageDelete wird bei jedem Loeschvorgang aufgerufen — dekrementiert
|
||||
// denselben Zaehler ueber ein negatives Delta desselben atomaren UPSERT.
|
||||
func (s *Store) ReportStorageDelete(ctx context.Context, tenantID string, sizeBytes int64) error {
|
||||
if sizeBytes < 0 {
|
||||
return errors.New("usage: sizeBytes darf bei einem loeschvorgang nicht negativ sein")
|
||||
}
|
||||
if err := s.Increment(ctx, tenantID, StorageBytesMetric, -sizeBytes); err != nil {
|
||||
return fmt.Errorf("speicherverbrauch (loeschen) melden: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentStorageUsage liefert den aktuellen Speicherverbrauch eines Tenants
|
||||
// (Akzeptanzkriterium 3) — ein einfaches Get auf den bereits gefuehrten
|
||||
// Zaehler, kein Scan des Objekt-Storage.
|
||||
func (s *Store) CurrentStorageUsage(ctx context.Context, tenantID string) (int64, error) {
|
||||
return s.Get(ctx, tenantID, StorageBytesMetric)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Akzeptanzkriterium 1 + 2 + Pruefung 1: paralleler Schreib-Test (viele
|
||||
// gleichzeitige Uploads) ergibt korrekten Endstand ohne verlorene Updates.
|
||||
func TestReportStorageWrite_ConcurrentUploadsSumCorrectly(t *testing.T) {
|
||||
store, cleanup := setupTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
tenant := newTenantID()
|
||||
|
||||
sizes := []int64{1024, 2048, 4096, 8192, 512, 256, 1000, 999, 1, 7000}
|
||||
var wg sync.WaitGroup
|
||||
for _, size := range sizes {
|
||||
wg.Add(1)
|
||||
go func(sz int64) {
|
||||
defer wg.Done()
|
||||
if err := store.ReportStorageWrite(ctx, tenant, sz); err != nil {
|
||||
t.Errorf("report write: %v", err)
|
||||
}
|
||||
}(size)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
var expected int64
|
||||
for _, s := range sizes {
|
||||
expected += s
|
||||
}
|
||||
|
||||
got, err := store.CurrentStorageUsage(ctx, tenant)
|
||||
if err != nil {
|
||||
t.Fatalf("current usage: %v", err)
|
||||
}
|
||||
if got != expected {
|
||||
t.Fatalf("erwartet %d bytes (unabhaengige kontrollsumme), habe %d — hinweis auf verlorene updates", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 1 + Pruefung 2: Loeschvorgang dekrementiert korrekt.
|
||||
func TestReportStorageDelete_Decrements(t *testing.T) {
|
||||
store, cleanup := setupTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
tenant := newTenantID()
|
||||
|
||||
if err := store.ReportStorageWrite(ctx, tenant, 10_000); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
if err := store.ReportStorageDelete(ctx, tenant, 3_000); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
|
||||
got, err := store.CurrentStorageUsage(ctx, tenant)
|
||||
if err != nil {
|
||||
t.Fatalf("current usage: %v", err)
|
||||
}
|
||||
if got != 7_000 {
|
||||
t.Fatalf("erwartet 7000 nach schreiben(10000)+loeschen(3000), habe %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 3 + Pruefung 3: Abfrage liefert konsistenten Wert mit
|
||||
// einer unabhaengigen Kontrollzaehlung ueber gemischte Schreib-/Loeschvorgaenge.
|
||||
func TestCurrentStorageUsage_MatchesIndependentTally(t *testing.T) {
|
||||
store, cleanup := setupTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
tenant := newTenantID()
|
||||
|
||||
type op struct {
|
||||
write bool
|
||||
size int64
|
||||
}
|
||||
ops := []op{
|
||||
{true, 5000}, {true, 3000}, {false, 1000}, {true, 2000}, {false, 4000}, {true, 500},
|
||||
}
|
||||
|
||||
var tally int64
|
||||
for _, o := range ops {
|
||||
if o.write {
|
||||
if err := store.ReportStorageWrite(ctx, tenant, o.size); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
tally += o.size
|
||||
} else {
|
||||
if err := store.ReportStorageDelete(ctx, tenant, o.size); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
tally -= o.size
|
||||
}
|
||||
}
|
||||
|
||||
got, err := store.CurrentStorageUsage(ctx, tenant)
|
||||
if err != nil {
|
||||
t.Fatalf("current usage: %v", err)
|
||||
}
|
||||
if got != tally {
|
||||
t.Fatalf("erwartet %d (unabhaengige kontrollzaehlung), habe %d", tally, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 3: LIC-03s generischer Store.Get liefert denselben Wert
|
||||
// wie CurrentStorageUsage — kein zweiter, abweichender Zaehlmechanismus.
|
||||
func TestCurrentStorageUsage_MatchesGenericStoreGet(t *testing.T) {
|
||||
store, cleanup := setupTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
tenant := newTenantID()
|
||||
|
||||
if err := store.ReportStorageWrite(ctx, tenant, 42); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
viaStorage, err := store.CurrentStorageUsage(ctx, tenant)
|
||||
if err != nil {
|
||||
t.Fatalf("current usage: %v", err)
|
||||
}
|
||||
viaGeneric, err := store.Get(ctx, tenant, StorageBytesMetric)
|
||||
if err != nil {
|
||||
t.Fatalf("generic get: %v", err)
|
||||
}
|
||||
if viaStorage != viaGeneric || viaStorage != 42 {
|
||||
t.Fatalf("erwartet beide wege liefern 42, habe storage=%d generic=%d", viaStorage, viaGeneric)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user