From 4d3d77d4ee6c1cf29c16104255044e0ffcfc20d5 Mon Sep 17 00:00:00 2001 From: Ivan Sushkov Date: Fri, 31 May 2024 14:59:11 +0700 Subject: [PATCH] First steps in IdentityMap implementation --- .../seedwork/identity/manageable.go | 24 --- .../infrastructure/seedwork/identity/map.go | 23 +-- .../seedwork/identity/strategy.go | 61 +++--- grade/pkg/cache/lru.go | 52 ++++++ grade/pkg/cache/lru_test.go | 17 ++ grade/pkg/collections/map.go | 103 +++++++++++ grade/pkg/collections/map_test.go | 173 ++++++++++++++++++ 7 files changed, 390 insertions(+), 63 deletions(-) delete mode 100644 grade/internal/infrastructure/seedwork/identity/manageable.go create mode 100644 grade/pkg/cache/lru.go create mode 100644 grade/pkg/cache/lru_test.go create mode 100644 grade/pkg/collections/map.go create mode 100644 grade/pkg/collections/map_test.go diff --git a/grade/internal/infrastructure/seedwork/identity/manageable.go b/grade/internal/infrastructure/seedwork/identity/manageable.go deleted file mode 100644 index 8ede6774..00000000 --- a/grade/internal/infrastructure/seedwork/identity/manageable.go +++ /dev/null @@ -1,24 +0,0 @@ -package identity - -type cacheMap[K comparable, V any] struct { - m map[K]V - //cache ?? -} - -func newCacheMap[K comparable, V any]() cacheMap[K, V] { - return cacheMap[K, V]{map[K]V{}} -} - -func (m cacheMap[K, V]) add(key K, object V) { - m.m[key] = object -} - -func (m cacheMap[K, V]) get(key K) (V, bool) { - object, found := m.m[key] - return object, found -} - -func (m cacheMap[K, V]) has(key K) bool { - _, found := m.m[key] - return found -} diff --git a/grade/internal/infrastructure/seedwork/identity/map.go b/grade/internal/infrastructure/seedwork/identity/map.go index a2de2f8c..e601cd98 100644 --- a/grade/internal/infrastructure/seedwork/identity/map.go +++ b/grade/internal/infrastructure/seedwork/identity/map.go @@ -1,6 +1,10 @@ package identity -import "errors" +import ( + "errors" + + "github.com/emacsway/grade/grade/pkg/collections" +) var ( ErrObjectAlreadyWatched = errors.New("") @@ -8,28 +12,25 @@ var ( ) type IdentityMap[K comparable, V any] struct { - manageable cacheMap[K, V] + manageable collections.ReplacingMap[K, V] isolation IsolationStrategy[K, V] } -func NewIdentityMap[K comparable, V any]() *IdentityMap[K, V] { - manageable := newCacheMap[K, V]() +func NewIdentityMap[K comparable, V any](size uint) *IdentityMap[K, V] { + manageable := collections.NewReplacingMap[K, V](size) isolation := serializableStrategy[K, V]{manageable: manageable} return &IdentityMap[K, V]{ manageable: manageable, - isolation: isolation, + isolation: &isolation, } } func (im *IdentityMap[K, V]) Add(key K, object V) (bool, error) { - im.isolation.add(key, object) - - //if _, found := im.objects[key]; found { - // return false, ErrObjectAlreadyWatched - //} + if err := im.isolation.add(key, object); err != nil { + return false, err + } - //im.objects[key] = object return true, nil } diff --git a/grade/internal/infrastructure/seedwork/identity/strategy.go b/grade/internal/infrastructure/seedwork/identity/strategy.go index 00b18e01..ff7f099d 100644 --- a/grade/internal/infrastructure/seedwork/identity/strategy.go +++ b/grade/internal/infrastructure/seedwork/identity/strategy.go @@ -1,6 +1,10 @@ package identity -import "errors" +import ( + "errors" + + "github.com/emacsway/grade/grade/pkg/collections" +) type IsolationLevel uint @@ -11,21 +15,24 @@ const ( Serializable = iota ) -var ErrDeniedOperationForStrategy = errors.New("") +var ( + ErrNonexistentObject = errors.New("") + ErrDeniedOperationForStrategy = errors.New("") +) type IsolationStrategy[K comparable, V any] interface { - add(key K, object V) + add(key K, object V) error get(key K) (V, error) has(key K) bool } -/////// - type readUncommittedStrategy[K comparable, V any] struct { - manageable cacheMap[K, V] + manageable collections.ReplacingMap[K, V] } -func (r *readUncommittedStrategy[K, V]) add(key K, object V) {} +func (r *readUncommittedStrategy[K, V]) add(key K, object V) error { + return nil +} func (r *readUncommittedStrategy[K, V]) get(key K) (object V, err error) { return object, ErrDeniedOperationForStrategy @@ -41,45 +48,43 @@ type readCommittedStrategy[K comparable, V any] struct { readUncommittedStrategy[K, V] } -// // type repeatableReadsStrategy[K comparable, V any] struct { - manageable cacheMap[K, V] + manageable collections.ReplacingMap[K, V] } -func (r *repeatableReadsStrategy[K, V]) add(key K, object V) { +func (r *repeatableReadsStrategy[K, V]) add(key K, object V) error { if object != nil { - r.manageable.add(key, object) + r.manageable.Add(key, object) } + + return nil } func (r *repeatableReadsStrategy[K, V]) get(key K) (V, error) { - object, found := r.manageable.get(key) - + return r.manageable.Get(key) } func (r *repeatableReadsStrategy[K, V]) has(key K) bool { - return r.manageable.has(key) + return r.manageable.Has(key) } -// /// type serializableStrategy[K comparable, V any] struct { - manageable cacheMap[K, V] + manageable collections.ReplacingMap[K, V] } -func (s serializableStrategy[K, V]) add(key K, object V) { - //TODO implement me - panic("implement me") -} +func (s *serializableStrategy[K, V]) add(key K, object V) error { + if object == nil { + return ErrNonexistentObject + } -func (s serializableStrategy[K, V]) get(key K) (V, error) { - //obj = self._identity_map().do_get(key) - //if isinstance(obj, NonexistentObject): - //raise ObjectDoesNotExist() - //return obj + s.manageable.Add(key, object) + return nil +} - s.manageable.get(ke) +func (s *serializableStrategy[K, V]) get(key K) (V, error) { + return s.manageable.Get(key) } -func (s serializableStrategy[K, V]) has(key K) bool { - return s.manageable.has(key) +func (s *serializableStrategy[K, V]) has(key K) bool { + return s.manageable.Has(key) } diff --git a/grade/pkg/cache/lru.go b/grade/pkg/cache/lru.go new file mode 100644 index 00000000..2107fe40 --- /dev/null +++ b/grade/pkg/cache/lru.go @@ -0,0 +1,52 @@ +package cache + +import ( + "container/list" +) + +type Lru[T comparable] struct { + items map[T]*list.Element + order *list.List + size uint +} + +func NewLru[T comparable](size uint) Lru[T] { + return Lru[T]{ + items: make(map[T]*list.Element, size), + order: list.New(), + size: size, + } +} + +func (l *Lru[T]) Add(value T) { + order := l.order.PushBack(&value) + l.items[value] = order + + if (uint)(len(l.items)) > l.size { + order = l.order.Front() + l.order.Remove(order) + + delete(l.items, order.Value.(T)) + } +} + +func (l *Lru[T]) Touch(value T) { + order := l.items[value] + l.order.MoveToBack(order) +} + +func (l *Lru[T]) Remove(value T) { + order := l.items[value] + + delete(l.items, value) + l.order.Remove(order) +} + +func (l *Lru[T]) Clear() { + l.order = list.New() + l.items = make(map[T]*list.Element, l.size) +} + +func (l *Lru[T]) SetSize(size uint) { + l.size = size +} diff --git a/grade/pkg/cache/lru_test.go b/grade/pkg/cache/lru_test.go new file mode 100644 index 00000000..f13ab0e8 --- /dev/null +++ b/grade/pkg/cache/lru_test.go @@ -0,0 +1,17 @@ +package cache + +import "testing" + +func TestLru(t *testing.T) { + + lru := NewLru[string](2) + + //lru.Add("1") + //lru.Add("2") + lru.Add("3") + lru.Add("2") + lru.Add("1") + + //lru.Touch("3") + //lru.Add("3") +} diff --git a/grade/pkg/collections/map.go b/grade/pkg/collections/map.go new file mode 100644 index 00000000..ad832391 --- /dev/null +++ b/grade/pkg/collections/map.go @@ -0,0 +1,103 @@ +package collections + +import ( + "container/list" + "errors" +) + +var ( + ErrKeyDoesNotContains = errors.New("") +) + +type CachedMap[K comparable, V any] struct { + m map[K]V +} + +func NewCachedMap[K comparable, V any]() CachedMap[K, V] { + return CachedMap[K, V]{ + m: map[K]V{}, + } +} + +func (m CachedMap[K, V]) Add(key K, value V) { + m.m[key] = value +} + +func (m CachedMap[K, V]) Get(key K) (value V, err error) { + if value, found := m.m[key]; found { + return value, nil + } + + return value, ErrKeyDoesNotContains +} + +func (m CachedMap[K, V]) Remove(key K) { + delete(m.m, key) +} + +func (m CachedMap[K, V]) Has(key K) bool { + _, found := m.m[key] + return found +} + +type item[K comparable, V any] struct { + key K + value V +} + +type ReplacingMap[K comparable, V any] struct { + items map[K]*list.Element + order *list.List + size uint +} + +func NewReplacingMap[K comparable, V any](size uint) ReplacingMap[K, V] { + return ReplacingMap[K, V]{ + items: make(map[K]*list.Element, size), + order: list.New(), + size: size, + } +} + +func (m *ReplacingMap[K, V]) Add(key K, value V) { + element := m.order.PushBack(item[K, V]{key, value}) + m.items[key] = element + + if (uint)(len(m.items)) > m.size { + element = m.order.Front() + m.order.Remove(element) + delete(m.items, element.Value.(item[K, V]).key) + } +} + +func (m *ReplacingMap[K, V]) Get(key K) (value V, err error) { + if element, found := m.items[key]; found { + return element.Value.(item[K, V]).value, nil + } + + return value, ErrKeyDoesNotContains +} + +func (m *ReplacingMap[K, V]) Touch(key K) { + element := m.items[key] + m.order.MoveToBack(element) +} + +func (m *ReplacingMap[K, V]) Remove(key K) { + element, found := m.items[key] + if !found { + return + } + + delete(m.items, key) + m.order.Remove(element) +} + +func (m *ReplacingMap[K, V]) Has(key K) bool { + _, found := m.items[key] + return found +} + +func (m *ReplacingMap[K, V]) SetSize(size uint) { + m.size = size +} diff --git a/grade/pkg/collections/map_test.go b/grade/pkg/collections/map_test.go new file mode 100644 index 00000000..85f3f590 --- /dev/null +++ b/grade/pkg/collections/map_test.go @@ -0,0 +1,173 @@ +package collections + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCachedMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + assertion func(t *testing.T, cm CachedMap[string, string]) + }{ + { + name: "must contains a set key", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + cm.Add("1", "1") + assert.Equal(t, true, cm.Has("1")) + }, + }, + + { + name: "must not contain a removed key", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + + assert.Equal(t, false, cm.Has("1")) + }, + }, + + { + name: "the key can be reused", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + cm.Add("1", "2") + + val, _ := cm.Get("1") + assert.Equal(t, "2", val) + }, + }, + + { + name: "trying to access an unset key", + assertion: func(t *testing.T, cm CachedMap[string, string]) { + _, err := cm.Get("1") + assert.Equal(t, ErrKeyDoesNotContains, err) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + cm := NewCachedMap[string, string]() + tt.assertion(t, cm) + }) + } +} + +func TestReplacingMapMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + assertion func(t *testing.T, cm ReplacingMap[string, string]) + }{ + { + name: "must contains a set key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Add("1", "1") + assert.Equal(t, true, cm.Has("1")) + }, + }, + + { + name: "must not contain a removed key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + + assert.Equal(t, false, cm.Has("1")) + }, + }, + + { + name: "the key can be reused", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Add("1", "1") + cm.Remove("1") + cm.Add("1", "2") + + val, _ := cm.Get("1") + assert.Equal(t, "2", val) + }, + }, + + { + name: "trying to access an unset key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + _, err := cm.Get("1") + assert.Equal(t, ErrKeyDoesNotContains, err) + }, + }, + + { + name: "trying to remove an unset key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + cm.Remove("1") + cm.Add("1", "1") + + val, _ := cm.Get("1") + assert.Equal(t, "1", val) + assert.Equal(t, true, cm.Has("1")) + }, + }, + + { + name: "touched key must be in map", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + + cm.SetSize(2) + + cm.Add("1", "1") + cm.Add("2", "2") + + cm.Touch("1") + cm.Add("3", "3") + + assert.Equal(t, true, cm.Has("1")) + assert.Equal(t, true, cm.Has("3")) + + assert.Equal(t, false, cm.Has("2")) + }, + }, + + { + name: "trying to access to replaced key", + assertion: func(t *testing.T, cm ReplacingMap[string, string]) { + + cm.SetSize(2) + + cm.Add("1", "1") + cm.Add("2", "2") + cm.Add("3", "3") + + _, err := cm.Get("1") + assert.Equal(t, ErrKeyDoesNotContains, err) + assert.Equal(t, false, cm.Has("1")) + + assert.Equal(t, true, cm.Has("2")) + assert.Equal(t, true, cm.Has("3")) + + val, _ := cm.Get("2") + assert.Equal(t, "2", val) + + val, _ = cm.Get("3") + assert.Equal(t, "3", val) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + cm := NewReplacingMap[string, string](3) + tt.assertion(t, cm) + }) + } +}