github.com/Jeffail/benthos/v3@v3.65.0/lib/cache/memory.go (about)

     1  package cache
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/Jeffail/benthos/v3/internal/component/cache"
    10  	"github.com/Jeffail/benthos/v3/internal/docs"
    11  	"github.com/Jeffail/benthos/v3/lib/log"
    12  	"github.com/Jeffail/benthos/v3/lib/metrics"
    13  	"github.com/Jeffail/benthos/v3/lib/types"
    14  	"github.com/OneOfOne/xxhash"
    15  )
    16  
    17  //------------------------------------------------------------------------------
    18  
    19  func init() {
    20  	Constructors[TypeMemory] = TypeSpec{
    21  		constructor: NewMemory,
    22  		Summary: `
    23  Stores key/value pairs in a map held in memory. This cache is therefore reset
    24  every time the service restarts. Each item in the cache has a TTL set from the
    25  moment it was last edited, after which it will be removed during the next
    26  compaction.`,
    27  		Description: `
    28  The compaction interval determines how often the cache is cleared of expired
    29  items, and this process is only triggered on writes to the cache. Access to the
    30  cache is blocked during this process.
    31  
    32  Item expiry can be disabled entirely by either setting the
    33  ` + "`compaction_interval`" + ` to an empty string.
    34  
    35  The field ` + "`init_values`" + ` can be used to prepopulate the memory cache
    36  with any number of key/value pairs which are exempt from TTLs:
    37  
    38  ` + "```yaml" + `
    39  memory:
    40    ttl: 60
    41    init_values:
    42      foo: bar
    43  ` + "```" + `
    44  
    45  These values can be overridden during execution, at which point the configured
    46  TTL is respected as usual.`,
    47  		FieldSpecs: docs.FieldSpecs{
    48  			docs.FieldCommon("ttl", "The TTL of each item in seconds. After this period an item will be eligible for removal during the next compaction."),
    49  			docs.FieldCommon("compaction_interval", "The period of time to wait before each compaction, at which point expired items are removed."),
    50  			docs.FieldAdvanced("shards", "A number of logical shards to spread keys across, increasing the shards can have a performance benefit when processing a large number of keys."),
    51  			docs.FieldString(
    52  				"init_values", "A table of key/value pairs that should be present in the cache on initialization. This can be used to create static lookup tables.",
    53  				map[string]string{
    54  					"Nickelback":       "1995",
    55  					"Spice Girls":      "1994",
    56  					"The Human League": "1977",
    57  				},
    58  			).Map(),
    59  		},
    60  	}
    61  }
    62  
    63  //------------------------------------------------------------------------------
    64  
    65  // MemoryConfig contains config fields for the Memory cache type.
    66  type MemoryConfig struct {
    67  	TTL                int               `json:"ttl" yaml:"ttl"`
    68  	CompactionInterval string            `json:"compaction_interval" yaml:"compaction_interval"`
    69  	InitValues         map[string]string `json:"init_values" yaml:"init_values"`
    70  	Shards             int               `json:"shards" yaml:"shards"`
    71  }
    72  
    73  // NewMemoryConfig creates a MemoryConfig populated with default values.
    74  func NewMemoryConfig() MemoryConfig {
    75  	return MemoryConfig{
    76  		TTL:                300, // 5 Mins
    77  		CompactionInterval: "60s",
    78  		InitValues:         map[string]string{},
    79  		Shards:             1,
    80  	}
    81  }
    82  
    83  //------------------------------------------------------------------------------
    84  
    85  type item struct {
    86  	value []byte
    87  	ts    time.Time
    88  }
    89  
    90  type shard struct {
    91  	items map[string]item
    92  	ttl   time.Duration
    93  
    94  	compInterval   time.Duration
    95  	lastCompaction time.Time
    96  
    97  	mKeys        metrics.StatGauge
    98  	mCompactions metrics.StatCounter
    99  
   100  	sync.RWMutex
   101  }
   102  
   103  func (s *shard) isExpired(i item) bool {
   104  	if s.compInterval == 0 {
   105  		return false
   106  	}
   107  	if i.ts.IsZero() {
   108  		return false
   109  	}
   110  	return time.Since(i.ts) >= s.ttl
   111  }
   112  
   113  func (s *shard) compaction() {
   114  	if s.compInterval == 0 {
   115  		return
   116  	}
   117  	if time.Since(s.lastCompaction) < s.compInterval {
   118  		return
   119  	}
   120  	s.mCompactions.Incr(1)
   121  	for k, v := range s.items {
   122  		if s.isExpired(v) {
   123  			delete(s.items, k)
   124  		}
   125  	}
   126  	s.lastCompaction = time.Now()
   127  }
   128  
   129  //------------------------------------------------------------------------------
   130  
   131  // NewMemory creates a new Memory cache type.
   132  func NewMemory(conf Config, mgr types.Manager, log log.Modular, stats metrics.Type) (types.Cache, error) {
   133  	var interval time.Duration
   134  	if tout := conf.Memory.CompactionInterval; len(tout) > 0 {
   135  		var err error
   136  		if interval, err = time.ParseDuration(tout); err != nil {
   137  			return nil, fmt.Errorf("failed to parse compaction interval string: %v", err)
   138  		}
   139  	}
   140  
   141  	m := &memoryV2{}
   142  	if conf.Memory.Shards <= 0 {
   143  		return nil, fmt.Errorf("expected >=1 shards, found: %v", conf.Memory.Shards)
   144  	}
   145  	if conf.Memory.Shards == 1 {
   146  		m.shards = []*shard{
   147  			{
   148  				items: map[string]item{},
   149  				ttl:   time.Second * time.Duration(conf.Memory.TTL),
   150  
   151  				compInterval:   interval,
   152  				lastCompaction: time.Now(),
   153  
   154  				mKeys:        stats.GetGauge("keys"),
   155  				mCompactions: stats.GetCounter("compaction"),
   156  			},
   157  		}
   158  	} else {
   159  		for i := 0; i < conf.Memory.Shards; i++ {
   160  			m.shards = append(m.shards, &shard{
   161  				items: map[string]item{},
   162  				ttl:   time.Second * time.Duration(conf.Memory.TTL),
   163  
   164  				compInterval:   interval,
   165  				lastCompaction: time.Now(),
   166  
   167  				mKeys:        stats.GetGauge(fmt.Sprintf("shard.%v.keys", i)),
   168  				mCompactions: stats.GetCounter(fmt.Sprintf("shard.%v.compaction", i)),
   169  			})
   170  		}
   171  	}
   172  
   173  	for k, v := range conf.Memory.InitValues {
   174  		m.getShard(k).items[k] = item{
   175  			value: []byte(v),
   176  			ts:    time.Time{},
   177  		}
   178  	}
   179  
   180  	return cache.NewV2ToV1Cache(m, stats), nil
   181  }
   182  
   183  type memoryV2 struct {
   184  	shards []*shard
   185  }
   186  
   187  func (m *memoryV2) getShard(key string) *shard {
   188  	if len(m.shards) == 1 {
   189  		return m.shards[0]
   190  	}
   191  	h := xxhash.New64()
   192  	h.WriteString(key)
   193  	return m.shards[h.Sum64()%uint64(len(m.shards))]
   194  }
   195  
   196  func (m *memoryV2) Get(_ context.Context, key string) ([]byte, error) {
   197  	shard := m.getShard(key)
   198  	shard.RLock()
   199  	k, exists := shard.items[key]
   200  	shard.RUnlock()
   201  	if !exists {
   202  		return nil, types.ErrKeyNotFound
   203  	}
   204  	// Simulate compaction by returning ErrKeyNotFound if ttl expired.
   205  	if shard.isExpired(k) {
   206  		return nil, types.ErrKeyNotFound
   207  	}
   208  	return k.value, nil
   209  }
   210  
   211  func (m *memoryV2) Set(_ context.Context, key string, value []byte, _ *time.Duration) error {
   212  	shard := m.getShard(key)
   213  	shard.Lock()
   214  	shard.compaction()
   215  	shard.items[key] = item{value: value, ts: time.Now()}
   216  	shard.mKeys.Set(int64(len(shard.items)))
   217  	shard.Unlock()
   218  	return nil
   219  }
   220  
   221  func (m *memoryV2) SetMulti(ctx context.Context, keyValues map[string]types.CacheTTLItem) error {
   222  	for k, v := range keyValues {
   223  		if err := m.Set(ctx, k, v.Value, v.TTL); err != nil {
   224  			return err
   225  		}
   226  	}
   227  	return nil
   228  }
   229  
   230  func (m *memoryV2) Add(_ context.Context, key string, value []byte, _ *time.Duration) error {
   231  	shard := m.getShard(key)
   232  	shard.Lock()
   233  	if _, exists := shard.items[key]; exists {
   234  		shard.Unlock()
   235  		return types.ErrKeyAlreadyExists
   236  	}
   237  	shard.compaction()
   238  	shard.items[key] = item{value: value, ts: time.Now()}
   239  	shard.mKeys.Set(int64(len(shard.items)))
   240  	shard.Unlock()
   241  	return nil
   242  }
   243  
   244  func (m *memoryV2) Delete(_ context.Context, key string) error {
   245  	shard := m.getShard(key)
   246  	shard.Lock()
   247  	shard.compaction()
   248  	delete(shard.items, key)
   249  	shard.mKeys.Set(int64(len(shard.items)))
   250  	shard.Unlock()
   251  	return nil
   252  }
   253  
   254  func (m *memoryV2) Close(context.Context) error {
   255  	return nil
   256  }
   257  
   258  //------------------------------------------------------------------------------
   259  
   260  // Memory is a memory based cache implementation.
   261  //
   262  // TODO: V4 remove this
   263  //
   264  // Deprecated: This implementation is no longer used.
   265  type Memory struct {
   266  	shards []*shard
   267  }
   268  
   269  func (m *Memory) getShard(key string) *shard {
   270  	if len(m.shards) == 1 {
   271  		return m.shards[0]
   272  	}
   273  	h := xxhash.New64()
   274  	h.WriteString(key)
   275  	return m.shards[h.Sum64()%uint64(len(m.shards))]
   276  }
   277  
   278  // Get attempts to locate and return a cached value by its key, returns an error
   279  // if the key does not exist.
   280  //
   281  // Deprecated: This implementation is no longer used.
   282  func (m *Memory) Get(key string) ([]byte, error) {
   283  	shard := m.getShard(key)
   284  	shard.RLock()
   285  	k, exists := shard.items[key]
   286  	shard.RUnlock()
   287  	if !exists {
   288  		return nil, types.ErrKeyNotFound
   289  	}
   290  	// Simulate compaction by returning ErrKeyNotFound if ttl expired.
   291  	if shard.isExpired(k) {
   292  		return nil, types.ErrKeyNotFound
   293  	}
   294  	return k.value, nil
   295  }
   296  
   297  // Set attempts to set the value of a key.
   298  //
   299  // Deprecated: This implementation is no longer used.
   300  func (m *Memory) Set(key string, value []byte) error {
   301  	shard := m.getShard(key)
   302  	shard.Lock()
   303  	shard.compaction()
   304  	shard.items[key] = item{value: value, ts: time.Now()}
   305  	shard.mKeys.Set(int64(len(shard.items)))
   306  	shard.Unlock()
   307  	return nil
   308  }
   309  
   310  // SetMulti attempts to set the value of multiple keys, returns an error if any
   311  // keys fail.
   312  //
   313  // Deprecated: This implementation is no longer used.
   314  func (m *Memory) SetMulti(items map[string][]byte) error {
   315  	for k, v := range items {
   316  		if err := m.Set(k, v); err != nil {
   317  			return err
   318  		}
   319  	}
   320  	return nil
   321  }
   322  
   323  // Add attempts to set the value of a key only if the key does not already exist
   324  // and returns an error if the key already exists.
   325  //
   326  // Deprecated: This implementation is no longer used.
   327  func (m *Memory) Add(key string, value []byte) error {
   328  	shard := m.getShard(key)
   329  	shard.Lock()
   330  	if _, exists := shard.items[key]; exists {
   331  		shard.Unlock()
   332  		return types.ErrKeyAlreadyExists
   333  	}
   334  	shard.compaction()
   335  	shard.items[key] = item{value: value, ts: time.Now()}
   336  	shard.mKeys.Set(int64(len(shard.items)))
   337  	shard.Unlock()
   338  	return nil
   339  }
   340  
   341  // Delete attempts to remove a key.
   342  //
   343  // Deprecated: This implementation is no longer used.
   344  func (m *Memory) Delete(key string) error {
   345  	shard := m.getShard(key)
   346  	shard.Lock()
   347  	shard.compaction()
   348  	delete(shard.items, key)
   349  	shard.mKeys.Set(int64(len(shard.items)))
   350  	shard.Unlock()
   351  	return nil
   352  }
   353  
   354  // CloseAsync shuts down the cache.
   355  //
   356  // Deprecated: This implementation is no longer used.
   357  func (m *Memory) CloseAsync() {
   358  }
   359  
   360  // WaitForClose blocks until the cache has closed down.
   361  //
   362  // Deprecated: This implementation is no longer used.
   363  func (m *Memory) WaitForClose(timeout time.Duration) error {
   364  	return nil
   365  }