internal/cloudsql/lazy.go (104 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 cloudsql
import (
"context"
"crypto/rsa"
"sync"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/cloudsqlconn/debug"
"cloud.google.com/go/cloudsqlconn/instance"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)
// LazyRefreshCache caches connection info and refreshes the cache only when
// a caller requests connection info and the current certificate is expired.
type LazyRefreshCache struct {
connName instance.ConnName
logger debug.ContextLogger
r adminAPIClient
mu sync.Mutex
useIAMAuthNDial bool
needsRefresh bool
cached ConnectionInfo
}
// NewLazyRefreshCache initializes a new LazyRefreshCache.
func NewLazyRefreshCache(
cn instance.ConnName,
l debug.ContextLogger,
client *sqladmin.Service,
key *rsa.PrivateKey,
_ time.Duration,
tp auth.TokenProvider,
dialerID string,
useIAMAuthNDial bool,
) *LazyRefreshCache {
return &LazyRefreshCache{
connName: cn,
logger: l,
r: newAdminAPIClient(
l,
client,
key,
tp,
dialerID,
),
useIAMAuthNDial: useIAMAuthNDial,
}
}
// 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.connName.String(),
)
return c.cached, nil
}
c.logger.Debugf(
ctx,
"[%v] Connection info refresh operation started",
c.connName.String(),
)
ci, err := c.r.ConnectionInfo(ctx, c.connName, c.useIAMAuthNDial)
if err != nil {
c.logger.Debugf(
ctx,
"[%v] Connection info refresh operation failed, err = %v",
c.connName.String(),
err,
)
return ConnectionInfo{}, err
}
c.logger.Debugf(
ctx,
"[%v] Connection info refresh operation complete",
c.connName.String(),
)
c.logger.Debugf(
ctx,
"[%v] Current certificate expiration = %v",
c.connName.String(),
ci.Expiration.UTC().Format(time.RFC3339),
)
c.cached = ci
c.needsRefresh = false
return ci, nil
}
// UpdateRefresh updates the refresh operation to either enable or disable IAM
// authentication for the cached connection info.
func (c *LazyRefreshCache) UpdateRefresh(useIAMAuthNDial *bool) {
c.mu.Lock()
defer c.mu.Unlock()
if useIAMAuthNDial != nil && *useIAMAuthNDial != c.useIAMAuthNDial {
c.useIAMAuthNDial = *useIAMAuthNDial
c.needsRefresh = true
}
}
// 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
}