From 53a0b94ad80637fbcd904cedb1e1de4cc4d0e7b8 Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Wed, 22 Jul 2026 15:17:19 +0200 Subject: [PATCH] Add ageing cache to dns alias driver - Cache getAliases responses with a configurable TTL (default 30s). - Use a sentinel value ("null") to cache 404 (no aliases) without poisoning the cache with nil. - Invalidate the cache after every mutation (create, update, delete). - Explicitly clear the cache before Start, Stop, and Status to ensure fresh data. - Expose CacheTTL on the mgr struct so tests can disable the cache (set to 0) and avoid filesystem permission issues. Changes: - drivers/resipsgcp_dnsalias/main.go - drivers/resipsgcp_dnsalias/mgr.go - drivers/resipsgcp_dnsalias/main_test.go --- drivers/resipsgcp_dnsalias/main.go | 26 +++++++--- drivers/resipsgcp_dnsalias/main_test.go | 9 ++++ drivers/resipsgcp_dnsalias/mgr.go | 68 +++++++++++++++++++++---- 3 files changed, 84 insertions(+), 19 deletions(-) diff --git a/drivers/resipsgcp_dnsalias/main.go b/drivers/resipsgcp_dnsalias/main.go index cf0a6aacd..c06c6a436 100644 --- a/drivers/resipsgcp_dnsalias/main.go +++ b/drivers/resipsgcp_dnsalias/main.go @@ -36,9 +36,10 @@ type ( } mgr struct { - alias alias - api apiProvider - log *plog.Logger + alias alias + api apiProvider + log *plog.Logger + CacheTTL time.Duration } // alias decoupled from sgcp.Alias to allow for future changes @@ -63,6 +64,8 @@ type ( } ) +const defaultCacheTTL = 30 * time.Second + func New() resource.Driver { return &T{} } @@ -110,8 +113,9 @@ func (t *T) Configure() error { func (t *T) configureMgr(cfg *sgcp.Config) error { mgr := &mgr{ - alias: alias{UUID: t.UUID, Name: t.Name, Target: t.Target, ZoneID: t.ZoneID}, - log: t.Log(), + alias: alias{UUID: t.UUID, Name: t.Name, Target: t.Target, ZoneID: t.ZoneID}, + log: t.Log(), + CacheTTL: defaultCacheTTL, } if t.api != nil { // allow custom api for tests @@ -143,12 +147,16 @@ func (t *T) configureMgr(cfg *sgcp.Config) error { } func (t *T) Start(ctx context.Context) error { - // TODO: implement cache cleanup + if err := t.mgr.cacheClear(); err != nil { + t.Log().Debugf("cache clear error: %s", err) + } return t.mgr.createOrUpdate(ctx, t.Target) } func (t *T) Stop(ctx context.Context) error { - // TODO: implement cache cleanup + if err := t.mgr.cacheClear(); err != nil { + t.Log().Debugf("cache clear error: %s", err) + } if t.UUID != "" { return t.mgr.createOrUpdate(ctx, t.noneTarget) } @@ -156,7 +164,9 @@ func (t *T) Stop(ctx context.Context) error { } func (t *T) Status(ctx context.Context) status.T { - // TODO: implement cache cleanup if command is not called from the scheduler + if err := t.mgr.cacheClear(); err != nil { + t.Log().Debugf("cache clear error: %s", err) + } aliases, err := t.mgr.getAliases(ctx) if err != nil { t.StatusLog().Error("get alias failed: %s", err) diff --git a/drivers/resipsgcp_dnsalias/main_test.go b/drivers/resipsgcp_dnsalias/main_test.go index af59ce53c..15e326106 100644 --- a/drivers/resipsgcp_dnsalias/main_test.go +++ b/drivers/resipsgcp_dnsalias/main_test.go @@ -249,6 +249,7 @@ func TestStatus(t *testing.T) { drv.Target = tc.resTarget drv.ZoneID = tc.resZoneID require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 dStatus := drv.Status(ctx) assert.Equalf(t, tc.expectedStatus, dStatus, "expected %s, got %s", tc.expectedStatus, dStatus) @@ -300,6 +301,7 @@ func TestStart(t *testing.T) { drv.Target = "foo-target" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify alias doesn't exits") alias, ok := db.Search("z1", "foo", "") @@ -334,6 +336,7 @@ func TestStart(t *testing.T) { drv.Target = "target1" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify alias initially exits") alias, ok := db.Search("z1", "name1", "uuid1") @@ -362,6 +365,7 @@ func TestStart(t *testing.T) { drv.Target = "newTarget2" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify alias exits initially, with alternate target") alias, ok := db.Search("z1", "name2", "uuid2") @@ -452,6 +456,7 @@ func TestStop(t *testing.T) { drv.Target = "target" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify alias doesn't exits") _, ok := db.Search(drv.ZoneID, drv.Name, drv.UUID) @@ -477,6 +482,7 @@ func TestStop(t *testing.T) { drv.Target = "target" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify alias doesn't exits") _, ok := db.Search(drv.ZoneID, drv.Name, drv.UUID) @@ -500,6 +506,7 @@ func TestStop(t *testing.T) { drv.Target = "target1" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify initial exits") alias, ok := db.Search(drv.ZoneID, drv.Name, drv.UUID) @@ -531,6 +538,7 @@ func TestStop(t *testing.T) { drv.Target = "none.xxx" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify initial exits with target none") alias, ok := db.Search(drv.ZoneID, drv.Name, drv.UUID) @@ -561,6 +569,7 @@ func TestStop(t *testing.T) { drv.Target = "target1" drv.ZoneID = "z1" require.NoError(t, drv.Configure()) + drv.mgr.CacheTTL = 0 t.Log("verify initial exits") initial, ok := db.Search(drv.ZoneID, drv.Name, drv.UUID) diff --git a/drivers/resipsgcp_dnsalias/mgr.go b/drivers/resipsgcp_dnsalias/mgr.go index e087c61c6..e1ccb5c36 100644 --- a/drivers/resipsgcp_dnsalias/mgr.go +++ b/drivers/resipsgcp_dnsalias/mgr.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" + "github.com/opensvc/om3/v3/util/ageingcache" "github.com/opensvc/om3/v3/util/sgcp" ) @@ -27,6 +28,9 @@ func (m *mgr) createOrUpdate(ctx context.Context, target string) error { return fmt.Errorf("create alias: %w", err) } else { m.alias = *v + if err := m.cacheClear(); err != nil { + m.log.Debugf("cache clear error: %s", err) + } return nil } } @@ -51,6 +55,9 @@ func (m *mgr) createOrUpdate(ctx context.Context, target string) error { return fmt.Errorf("update alias unexpected nil") } else { m.alias = *v + if err := m.cacheClear(); err != nil { + m.log.Debugf("cache clear error: %s", err) + } return nil } } @@ -72,20 +79,37 @@ func (m *mgr) delete(ctx context.Context) error { if err := m.api.DeleteAlias(ctx, alias.ZoneID, alias.UUID); err != nil { return fmt.Errorf("delete alias: %w", err) } + if err := m.cacheClear(); err != nil { + m.log.Debugf("cache clear error: %s", err) + } return nil } // getAliases retrieves a list of aliases for the specified zone, name, and UUID or returns an error if unsuccessful. func (m *mgr) getAliases(ctx context.Context) ([]sgcp.Alias, error) { - // TODO: Use ageing cache - method, url, code, data, err := m.api.GetAliases(ctx, m.alias.ZoneID, m.alias.Name, m.alias.UUID) - if err != nil { - return nil, err + if m.CacheTTL <= 0 { + data, err := m.getAliasesFactory(ctx)() + if err != nil { + return nil, err + } + if data == nil || string(data) == "null" { + return nil, nil + } + var resp aliasListResponse + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("decode aliases: %w", err) + } + return resp.CnameRecords, nil } - if err := m.api.CheckStatusCode(method, url, code, http.StatusOK, http.StatusNotFound); err != nil { + + o := ageingcache.NewOutputter(m.getAliasesFactory(ctx)) + sig := m.cacheSig() + data, err := ageingcache.Output(o, sig, m.CacheTTL) + if err != nil { + m.log.Debugf("getAliases cache miss: %s", err) return nil, err } - if code == http.StatusNotFound { + if data == nil || string(data) == "null" { return nil, nil } var resp aliasListResponse @@ -95,7 +119,22 @@ func (m *mgr) getAliases(ctx context.Context) ([]sgcp.Alias, error) { return resp.CnameRecords, nil } -// create creates a new alias with the specified target and returns the created alias or an error if the operation fails. +func (m *mgr) getAliasesFactory(ctx context.Context) func() ([]byte, error) { + return func() ([]byte, error) { + method, url, code, data, err := m.api.GetAliases(ctx, m.alias.ZoneID, m.alias.Name, m.alias.UUID) + if err != nil { + return nil, err + } + if err := m.api.CheckStatusCode(method, url, code, http.StatusOK, http.StatusNotFound); err != nil { + return nil, err + } + if code == http.StatusNotFound { + return []byte("null"), nil + } + return data, nil + } +} + func (m *mgr) create(ctx context.Context, target string) (*alias, error) { v, err := m.api.CreateAlias(ctx, m.alias.ZoneID, m.alias.Name, target) if err != nil { @@ -104,7 +143,6 @@ func (m *mgr) create(ctx context.Context, target string) (*alias, error) { return toAlias(v), nil } -// update modifies an existing alias with the specified parameters and returns the updated alias or an error if any occurs. func (m *mgr) update(ctx context.Context, zoneID, aliasUUID, aliasName, target string) (*alias, error) { v, err := m.api.UpdateAlias(ctx, zoneID, aliasUUID, aliasName, target) if err != nil { @@ -113,7 +151,6 @@ func (m *mgr) update(ctx context.Context, zoneID, aliasUUID, aliasName, target s return toAlias(v), nil } -// toAlias converts a sgcp.Alias object to an alias object by mapping corresponding fields. func toAlias(v *sgcp.Alias) *alias { return &alias{ UUID: v.UUID, @@ -124,8 +161,6 @@ func toAlias(v *sgcp.Alias) *alias { } } -// Equal compares two alias objects and returns true if they are equal, or false otherwise. -// It doesn't compare the FQDN field. func (a *alias) Equal(b *alias) bool { if a == nil && b == nil { return true @@ -138,3 +173,14 @@ func (a *alias) Equal(b *alias) bool { a.Target == b.Target && a.ZoneID == b.ZoneID } + +func (m *mgr) cacheSig() string { + return fmt.Sprintf("dnsalias:%s:%s:%s", m.alias.ZoneID, m.alias.Name, m.alias.UUID) +} + +func (m *mgr) cacheClear() error { + if m.CacheTTL <= 0 { + return nil + } + return ageingcache.Clear(m.cacheSig()) +}