fix: synchronize DWH registry snapshots

This commit is contained in:
User
2026-08-21 00:39:35 +02:00
parent 971a0e66b1
commit e90a1a1851
4 changed files with 157 additions and 49 deletions
+44 -26
View File
@@ -25,7 +25,10 @@ import (
"github.com/aritmolab/thothii/tools/dwh-auth/internal/securefile"
)
const maxRecordBytes = 4096
const (
maxRecordBytes = 4096
registryLockName = ".writer.lock"
)
// State describes which registry directory owns a public record.
type State string
@@ -155,12 +158,13 @@ func (s *Store) Find(keyID string) (record.Record, error) {
if !validKeyID(keyID) {
return record.Record{}, ErrNotFound
}
if s == nil {
return record.Record{}, integrity(errors.New("uninitialized store"))
}
s.mu.RLock()
defer s.mu.RUnlock()
return s.findUnlocked(keyID)
var value record.Record
err := s.withReaderLock(func() error {
var findErr error
value, findErr = s.findUnlocked(keyID)
return findErr
})
return value, err
}
func (s *Store) findUnlocked(keyID string) (record.Record, error) {
@@ -182,12 +186,13 @@ func (s *Store) findUnlocked(keyID string) (record.Record, error) {
// FindLegacy returns the sole active legacy_raw record. A revoked legacy record
// wins; any multiple-legacy condition is an integrity fault.
func (s *Store) FindLegacy() (record.Record, error) {
if s == nil {
return record.Record{}, integrity(errors.New("uninitialized store"))
}
s.mu.RLock()
defer s.mu.RUnlock()
return s.findLegacyUnlocked()
var value record.Record
err := s.withReaderLock(func() error {
var findErr error
value, findErr = s.findLegacyUnlocked()
return findErr
})
return value, err
}
func (s *Store) findLegacyUnlocked() (record.Record, error) {
@@ -217,12 +222,13 @@ func (s *Store) findLegacyUnlocked() (record.Record, error) {
// List returns a stable, redacted inventory. A revoked record replaces any
// same-key active record visible during a revoked-first transition.
func (s *Store) List() ([]PublicRecord, error) {
if s == nil {
return nil, integrity(errors.New("uninitialized store"))
}
s.mu.RLock()
defer s.mu.RUnlock()
return s.listUnlocked()
var records []PublicRecord
err := s.withReaderLock(func() error {
var listErr error
records, listErr = s.listUnlocked()
return listErr
})
return records, err
}
func (s *Store) listUnlocked() ([]PublicRecord, error) {
@@ -304,12 +310,7 @@ func (s *Store) revokeUnlocked(keyID, reason string, at time.Time) error {
// Check validates every record and protected directory without exposing any
// digest data.
func (s *Store) Check() error {
if s == nil {
return integrity(errors.New("uninitialized store"))
}
s.mu.RLock()
defer s.mu.RUnlock()
return s.checkUnlocked()
return s.withReaderLock(s.checkUnlocked)
}
func (s *Store) checkUnlocked() error {
@@ -320,6 +321,23 @@ func (s *Store) checkUnlocked() error {
return validateLegacyMultiplicity(active, revoked)
}
func (s *Store) withReaderLock(fn func() error) error {
if s == nil {
return integrity(errors.New("uninitialized store"))
}
s.mu.RLock()
defer s.mu.RUnlock()
if s.root == nil || s.active == nil || s.revoked == nil {
return integrity(errors.New("uninitialized store"))
}
lock, err := s.root.LockShared(registryLockName)
if err != nil {
return integrity(err)
}
defer lock.Close()
return fn()
}
func (s *Store) withWriterLock(fn func() error) error {
if s == nil {
return integrity(errors.New("uninitialized store"))
@@ -329,7 +347,7 @@ func (s *Store) withWriterLock(fn func() error) error {
if s.root == nil || s.active == nil || s.revoked == nil {
return integrity(errors.New("uninitialized store"))
}
lock, err := s.root.Lock(".writer.lock")
lock, err := s.root.Lock(registryLockName)
if err != nil {
return integrity(err)
}
+24 -22
View File
@@ -353,7 +353,7 @@ func TestConcurrentAddDoesNotOverwriteAndFindNeverReadsPartialRecord(t *testing.
}
}()
}
if err := readStore.Revoke(first.KeyID, "synthetic rotation", first.CreatedAt.Add(time.Hour)); err != nil {
if err := left.Revoke(first.KeyID, "synthetic rotation", first.CreatedAt.Add(time.Hour)); err != nil {
t.Fatalf("Revoke() during concurrent reads error = %v", err)
}
readers.Wait()
@@ -597,11 +597,12 @@ func TestFindAndFindLegacyRejectExactExpiration(t *testing.T) {
}
}
func TestReadOperationsWaitAcrossRevokePublicationAndUnlink(t *testing.T) {
func TestCrossStoreReadersWaitAcrossRevokePublicationAndUnlink(t *testing.T) {
root := t.TempDir()
store := openStore(t, root)
writer := openStore(t, root)
reader := openStore(t, root)
legacy := syntheticLegacyRecord()
if err := store.Add(legacy); err != nil {
if err := writer.Add(legacy); err != nil {
t.Fatalf("Add() error = %v", err)
}
@@ -610,17 +611,17 @@ func TestReadOperationsWaitAcrossRevokePublicationAndUnlink(t *testing.T) {
writerDone := make(chan error, 1)
revokedAt := legacy.CreatedAt.Add(time.Hour)
go func() {
writerDone <- store.withWriterLock(func() error {
writerDone <- writer.withWriterLock(func() error {
close(writerReady)
<-releaseWriter
revoked := revokedRecord(legacy, revokedAt, "synthetic rotation")
if err := store.writeRecord(store.revoked, revoked); err != nil {
if err := writer.writeRecord(writer.revoked, revoked); err != nil {
return err
}
if err := store.active.Remove(recordFileName(legacy.KeyID)); err != nil {
if err := writer.active.Remove(recordFileName(legacy.KeyID)); err != nil {
return err
}
return store.active.Sync()
return writer.active.Sync()
})
}()
<-writerReady
@@ -633,10 +634,10 @@ func TestReadOperationsWaitAcrossRevokePublicationAndUnlink(t *testing.T) {
name string
run func() error
}{
{name: "Find", run: func() error { _, err := store.Find(legacy.KeyID); return err }},
{name: "List", run: func() error { _, err := store.List(); return err }},
{name: "Check", run: store.Check},
{name: "FindLegacy", run: func() error { _, err := store.FindLegacy(); return err }},
{name: "Find", run: func() error { _, err := reader.Find(legacy.KeyID); return err }},
{name: "List", run: func() error { _, err := reader.List(); return err }},
{name: "Check", run: reader.Check},
{name: "FindLegacy", run: func() error { _, err := reader.FindLegacy(); return err }},
}
results := make(chan readResult, len(readers))
var started sync.WaitGroup
@@ -689,11 +690,12 @@ waitForReaders:
}
}
func TestScanReadersWaitForWriterTemporaryFile(t *testing.T) {
func TestCrossStoreScanReadersWaitForWriterTemporaryFile(t *testing.T) {
root := t.TempDir()
store := openStore(t, root)
writer := openStore(t, root)
reader := openStore(t, root)
legacy := syntheticLegacyRecord()
if err := store.Add(legacy); err != nil {
if err := writer.Add(legacy); err != nil {
t.Fatalf("Add() error = %v", err)
}
@@ -702,8 +704,8 @@ func TestScanReadersWaitForWriterTemporaryFile(t *testing.T) {
writerDone := make(chan error, 1)
const temporary = ".tmp-regression"
go func() {
writerDone <- store.withWriterLock(func() error {
file, err := store.active.CreateExclusive(temporary, 0o600)
writerDone <- writer.withWriterLock(func() error {
file, err := writer.active.CreateExclusive(temporary, 0o600)
if err != nil {
return err
}
@@ -712,10 +714,10 @@ func TestScanReadersWaitForWriterTemporaryFile(t *testing.T) {
}
close(writerReady)
<-releaseWriter
if err := store.active.Remove(temporary); err != nil {
if err := writer.active.Remove(temporary); err != nil {
return err
}
return store.active.Sync()
return writer.active.Sync()
})
}()
<-writerReady
@@ -728,9 +730,9 @@ func TestScanReadersWaitForWriterTemporaryFile(t *testing.T) {
name string
run func() error
}{
{name: "List", run: func() error { _, err := store.List(); return err }},
{name: "Check", run: store.Check},
{name: "FindLegacy", run: func() error { _, err := store.FindLegacy(); return err }},
{name: "List", run: func() error { _, err := reader.List(); return err }},
{name: "Check", run: reader.Check},
{name: "FindLegacy", run: func() error { _, err := reader.FindLegacy(); return err }},
}
results := make(chan readResult, len(readers))
var started sync.WaitGroup