Skip to content

Commit

Permalink
chore: introduce memorystorev2 (#193)
Browse files Browse the repository at this point in the history
This adds a `memorystorev2` package containing an alternative Memory
Store implementation designed specifically for FDv2 usage.

It is similar to the existing memory store, with obsolete methods
removed and new methods added - but the core `upsert/init` logic remains
the same.
  • Loading branch information
cwaldren-ld authored Sep 24, 2024
1 parent 96b4004 commit ec154cd
Show file tree
Hide file tree
Showing 3 changed files with 962 additions and 0 deletions.
170 changes: 170 additions & 0 deletions internal/memorystorev2/memory_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Package memorystorev2 contains an implementation for a transactional memory store suitable
// for the FDv2 architecture.
package memorystorev2

import (
"sync"

"github.com/launchdarkly/go-sdk-common/v3/ldlog"
"github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes"
)

// Store provides an abstraction that makes flag and segment data available to other components.
// It accepts updates in batches - for instance, flag A was upserted while segment B was deleted -
// such that the contents of the store are consistent with a single payload version at any given time.
//
// The terminology used is "basis" and "deltas". First, the store's basis is set. This is this initial
// data, upon which subsequent deltas will be applied. Whenever the basis is set, any existing data
// is discarded.
//
// Deltas are then applied to the store. A single delta update transforms the contents of the store
// atomically.
type Store struct {
data map[ldstoretypes.DataKind]map[string]ldstoretypes.ItemDescriptor
initialized bool
sync.RWMutex
loggers ldlog.Loggers
}

// New creates a new Store. The Store is uninitialized until SetBasis is called.
func New(loggers ldlog.Loggers) *Store {
return &Store{
data: make(map[ldstoretypes.DataKind]map[string]ldstoretypes.ItemDescriptor),
initialized: false,
loggers: loggers,
}
}

// SetBasis sets the basis of the Store. Any existing data is discarded.
// When the basis is set, the store becomes initialized.
func (s *Store) SetBasis(allData []ldstoretypes.Collection) {
s.Lock()
defer s.Unlock()

s.data = make(map[ldstoretypes.DataKind]map[string]ldstoretypes.ItemDescriptor)

for _, coll := range allData {
items := make(map[string]ldstoretypes.ItemDescriptor)
for _, item := range coll.Items {
items[item.Key] = item.Item
}
s.data[coll.Kind] = items
}

s.initialized = true
}

// ApplyDelta applies a delta update to the store. ApplyDelta should not be called until
// SetBasis has been called at least once. The return value indicates, for each DataKind
// present in the delta, whether the item in the delta was actually updated or not.
//
// An item is updated only if the version of the item in the delta is greater than the version
// in the store, or it wasn't already present.
func (s *Store) ApplyDelta(allData []ldstoretypes.Collection) map[ldstoretypes.DataKind]map[string]bool {
updatedMap := make(map[ldstoretypes.DataKind]map[string]bool)

s.Lock()
defer s.Unlock()

for _, coll := range allData {
for _, item := range coll.Items {
updated := s.upsert(coll.Kind, item.Key, item.Item)
if updatedMap[coll.Kind] == nil {
updatedMap[coll.Kind] = make(map[string]bool)
}
updatedMap[coll.Kind][item.Key] = updated
}
}

return updatedMap
}

// Get retrieves an item of the specified kind from the store. If the item is not found, then
// ItemDescriptor{}.NotFound() is returned with a nil error.
func (s *Store) Get(kind ldstoretypes.DataKind, key string) (ldstoretypes.ItemDescriptor, error) {
s.RLock()

var item ldstoretypes.ItemDescriptor
coll, ok := s.data[kind]
if ok {
item, ok = coll[key]
}

s.RUnlock()

if ok {
return item, nil
}
if s.loggers.IsDebugEnabled() {
s.loggers.Debugf(`Key %s not found in "%s"`, key, kind)
}
return ldstoretypes.ItemDescriptor{}.NotFound(), nil
}

// GetAll retrieves all items of the specified kind from the store.
func (s *Store) GetAll(kind ldstoretypes.DataKind) ([]ldstoretypes.KeyedItemDescriptor, error) {
s.RLock()
defer s.RUnlock()
return s.getAll(kind), nil
}

func (s *Store) getAll(kind ldstoretypes.DataKind) []ldstoretypes.KeyedItemDescriptor {
var itemsOut []ldstoretypes.KeyedItemDescriptor
if itemsMap, ok := s.data[kind]; ok {
if len(itemsMap) > 0 {
itemsOut = make([]ldstoretypes.KeyedItemDescriptor, 0, len(itemsMap))
for key, item := range itemsMap {
itemsOut = append(itemsOut, ldstoretypes.KeyedItemDescriptor{Key: key, Item: item})
}
}
}
return itemsOut
}

// GetAllKinds retrieves all items of all kinds from the store. This is different from calling
// GetAll for each kind because it provides a consistent view of the entire store at a single point in time.
func (s *Store) GetAllKinds() []ldstoretypes.Collection {
s.RLock()
defer s.RUnlock()

allData := make([]ldstoretypes.Collection, 0, len(s.data))
for kind := range s.data {
itemsOut := s.getAll(kind)
allData = append(allData, ldstoretypes.Collection{Kind: kind, Items: itemsOut})
}

return allData
}

func (s *Store) upsert(
kind ldstoretypes.DataKind,
key string,
newItem ldstoretypes.ItemDescriptor) bool {
var coll map[string]ldstoretypes.ItemDescriptor
var ok bool
shouldUpdate := true
updated := false
if coll, ok = s.data[kind]; ok {
if item, ok := coll[key]; ok {
if item.Version >= newItem.Version {
shouldUpdate = false
}
}
} else {
s.data[kind] = map[string]ldstoretypes.ItemDescriptor{key: newItem}
shouldUpdate = false // because we already initialized the map with the new item
updated = true
}
if shouldUpdate {
coll[key] = newItem
updated = true
}
return updated
}

// IsInitialized returns true if the store has ever been initialized with a basis.
func (s *Store) IsInitialized() bool {
s.RLock()
defer s.RUnlock()
return s.initialized
}
Loading

0 comments on commit ec154cd

Please sign in to comment.