github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/server/internal/cache/cache.go (about)

     1  // Copyright 2018 Google Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package cache contains caching structures using internally by the Fleetspeak
    16  // server.
    17  package cache
    18  
    19  import (
    20  	"context"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/google/fleetspeak/fleetspeak/src/common"
    25  	"github.com/google/fleetspeak/fleetspeak/src/server/db"
    26  )
    27  
    28  var (
    29  	// MaxAge defines how long client data should be considered valid
    30  	// for. Variable to support unit testing.
    31  	MaxAge = 45 * time.Second
    32  
    33  	// We occasionally expunge old client data records, to be tidy
    34  	// with RAM and prevent what would effectively be a slow memory leak as
    35  	// clients come and go. Variable to support unit testing.
    36  	expireInterval = 5 * time.Minute
    37  )
    38  
    39  // Clients is a cache of recently connected clients.
    40  type Clients struct {
    41  	m    map[common.ClientID]*clientEntry
    42  	l    sync.RWMutex
    43  	stop chan struct{}
    44  }
    45  
    46  type clientEntry struct {
    47  	u time.Time
    48  	d *db.ClientData
    49  }
    50  
    51  // NewClients returns a new cache of client data.
    52  func NewClients() *Clients {
    53  	ret := &Clients{
    54  		m:    make(map[common.ClientID]*clientEntry),
    55  		stop: make(chan struct{}),
    56  	}
    57  	go ret.expireLoop()
    58  	return ret
    59  }
    60  
    61  // Get returns the cached client data, if there is sufficiently fresh data in
    62  // the cache, otherwise nil.
    63  func (c *Clients) Get(id common.ClientID) *db.ClientData {
    64  	c.l.RLock()
    65  	defer c.l.RUnlock()
    66  
    67  	e := c.m[id]
    68  	if e == nil || db.Now().Sub(e.u) > MaxAge {
    69  		return nil
    70  	}
    71  	return e.d.Clone()
    72  }
    73  
    74  // GetOrRead returns the cached client data, if there is sufficiently fresh data
    75  // in the cache.  Otherwise it attempts to read the data from the provided
    76  // datastore. It returns data if it finds any, hit if it was found in the cache.
    77  func (c *Clients) GetOrRead(ctx context.Context, id common.ClientID, db db.ClientStore) (data *db.ClientData, hit bool, err error) {
    78  	ret := c.Get(id)
    79  	if ret != nil {
    80  		// cloned by Get
    81  		return ret, true, nil
    82  	}
    83  
    84  	if ret, err = db.GetClientData(ctx, id); err != nil {
    85  		return nil, false, err
    86  	}
    87  	c.Update(id, ret)
    88  	return ret, false, nil
    89  }
    90  
    91  // Update updates or sets the cached data for a particular client. If data is
    92  // nil, it clears the data for the client.
    93  func (c *Clients) Update(id common.ClientID, data *db.ClientData) {
    94  	c.l.Lock()
    95  	defer c.l.Unlock()
    96  
    97  	if data == nil {
    98  		delete(c.m, id)
    99  	} else {
   100  		c.m[id] = &clientEntry{
   101  			u: db.Now(),
   102  			d: data.Clone(),
   103  		}
   104  	}
   105  }
   106  
   107  // Clear empties the cache, removing all entries.
   108  func (c *Clients) Clear() {
   109  	c.l.Lock()
   110  	defer c.l.Unlock()
   111  
   112  	c.m = make(map[common.ClientID]*clientEntry)
   113  }
   114  
   115  // Stop releases the resources required for background cache maintenance. The
   116  // cache should not be used once Stop has been called.
   117  func (c *Clients) Stop() {
   118  	close(c.stop)
   119  }
   120  
   121  // Size returns the current size taken up by the cache, this is a count of
   122  // client records, some of which may no longer be up to date.
   123  func (c *Clients) Size() int {
   124  	c.l.RLock()
   125  	defer c.l.RUnlock()
   126  	return len(c.m)
   127  }
   128  
   129  func (c *Clients) expireLoop() {
   130  	t := time.NewTicker(expireInterval)
   131  	defer t.Stop()
   132  
   133  	for {
   134  		select {
   135  		case <-t.C:
   136  			c.expire()
   137  		case <-c.stop:
   138  			return
   139  		}
   140  	}
   141  }
   142  
   143  // expire prunes the cache to clean out clients that are no longer up to date.
   144  func (c *Clients) expire() {
   145  	c.l.Lock()
   146  	defer c.l.Unlock()
   147  
   148  	for k, e := range c.m {
   149  		if db.Now().Sub(e.u) > MaxAge {
   150  			delete(c.m, k)
   151  		}
   152  	}
   153  }