internal/alloydb/lazy.go (102 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package alloydb
import (
"context"
"crypto/rsa"
"sync"
"time"
alloydbadmin "cloud.google.com/go/alloydb/apiv1alpha"
"cloud.google.com/go/alloydbconn/debug"
telv2 "cloud.google.com/go/alloydbconn/internal/tel/v2"
)
// LazyRefreshCache is caches connection info and refreshes the cache only when
// a caller requests connection info and the current certificate is expired.
type LazyRefreshCache struct {
uri InstanceURI
logger debug.ContextLogger
r adminAPIClient
mu sync.Mutex
needsRefresh bool
cached ConnectionInfo
userAgent string
metricRecorder telv2.MetricRecorder
}
// NewLazyRefreshCache initializes a new LazyRefreshCache.
func NewLazyRefreshCache(
uri InstanceURI,
l debug.ContextLogger,
client *alloydbadmin.AlloyDBAdminClient,
key *rsa.PrivateKey,
_ time.Duration,
dialerID string,
disableMetadataExchange bool,
userAgent string,
mr telv2.MetricRecorder,
) *LazyRefreshCache {
return &LazyRefreshCache{
uri: uri,
logger: l,
r: newAdminAPIClient(client, key, dialerID, disableMetadataExchange),
userAgent: userAgent,
metricRecorder: mr,
}
}
// ConnectionInfo returns connection info for the associated instance. New
// connection info is retrieved under two conditions:
// - the current connection info's certificate has expired, or
// - a caller has separately called ForceRefresh
func (c *LazyRefreshCache) ConnectionInfo(
ctx context.Context,
) (ConnectionInfo, error) {
c.mu.Lock()
defer c.mu.Unlock()
// strip monotonic clock with UTC()
now := time.Now().UTC()
// Pad expiration with a buffer to give the client plenty of time to
// establish a connection to the server with the certificate.
exp := c.cached.Expiration.UTC().Add(-refreshBuffer)
if !c.needsRefresh && now.Before(exp) {
c.logger.Debugf(
ctx,
"[%v] Connection info is still valid, using cached info",
c.uri.String(),
)
return c.cached, nil
}
c.logger.Debugf(
ctx,
"[%v] Connection info refresh operation started",
c.uri.String(),
)
ci, err := c.r.connectionInfo(ctx, c.uri)
if err != nil {
c.logger.Debugf(
ctx,
"[%v] Connection info refresh operation failed, err = %v",
c.uri.String(),
err,
)
go c.metricRecorder.RecordRefreshCount(ctx, telv2.Attributes{
UserAgent: c.userAgent,
RefreshType: telv2.RefreshLazyType,
RefreshStatus: telv2.RefreshFailure,
})
return ConnectionInfo{}, err
}
go c.metricRecorder.RecordRefreshCount(ctx, telv2.Attributes{
UserAgent: c.userAgent,
RefreshType: telv2.RefreshLazyType,
RefreshStatus: telv2.RefreshSuccess,
})
c.logger.Debugf(
ctx,
"[%v] Connection info refresh operation complete",
c.uri.String(),
)
c.logger.Debugf(
ctx,
"[%v] Current certificate expiration = %v",
c.uri.String(),
ci.Expiration.UTC().Format(time.RFC3339),
)
c.cached = ci
c.needsRefresh = false
return ci, nil
}
// ForceRefresh invalidates the caches and configures the next call to
// ConnectionInfo to retrieve a fresh connection info.
func (c *LazyRefreshCache) ForceRefresh() {
c.mu.Lock()
defer c.mu.Unlock()
c.needsRefresh = true
}
// Close is a no-op and provided purely for a consistent interface with other
// caching types.
func (c *LazyRefreshCache) Close() error {
return nil
}