From 8cd785e77cd829779f734af2375b47d73ee86803 Mon Sep 17 00:00:00 2001 From: Ivan Sushkov Date: Mon, 4 Mar 2024 18:51:35 +0700 Subject: [PATCH] First steps in IdentityMap implementation --- .../specialist_reconstitutor_test.go | 20 ++ .../infrastructure/seedwork/identity/map.go | 63 ++++++ .../seedwork/identity/strategy.go | 91 +++++++++ .../seedwork/session/identity_map.go | 180 ++++++++++++++++++ .../seedwork/session/identity_map_test.go | 106 +++++++++++ .../seedwork/session/interfaces.go | 10 + .../seedwork/session/pgx_session.go | 5 +- 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 +++++++++++++++++ 11 files changed, 817 insertions(+), 3 deletions(-) create mode 100644 grade/internal/domain/specialist/specialist_reconstitutor_test.go create mode 100644 grade/internal/infrastructure/seedwork/identity/map.go create mode 100644 grade/internal/infrastructure/seedwork/identity/strategy.go create mode 100644 grade/internal/infrastructure/seedwork/session/identity_map.go create mode 100644 grade/internal/infrastructure/seedwork/session/identity_map_test.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/domain/specialist/specialist_reconstitutor_test.go b/grade/internal/domain/specialist/specialist_reconstitutor_test.go new file mode 100644 index 00000000..fc3e78ce --- /dev/null +++ b/grade/internal/domain/specialist/specialist_reconstitutor_test.go @@ -0,0 +1,20 @@ +package specialist + +import "testing" + +func TestSpecialistReconstitutor(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + }{ + {}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + + }) + } +} diff --git a/grade/internal/infrastructure/seedwork/identity/map.go b/grade/internal/infrastructure/seedwork/identity/map.go new file mode 100644 index 00000000..16f84b0a --- /dev/null +++ b/grade/internal/infrastructure/seedwork/identity/map.go @@ -0,0 +1,63 @@ +package identity + +import ( + "errors" + + "github.com/emacsway/grade/grade/pkg/collections" +) + +var ( + ErrObjectAlreadyWatched = errors.New("") + ErrObjectNotFound = errors.New("") +) + +type IdentityMap[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] + isolation IsolationStrategy[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, + } +} + +func (im *IdentityMap[K, V]) Add(key K, object V) (bool, error) { + if err := im.isolation.add(key, object); err != nil { + return false, err + } + + return true, nil +} + +func (im *IdentityMap[K, V]) Get(key K) (object V, err error) { + return im.isolation.get(key) +} + +func (im *IdentityMap[K, V]) Has(key K) bool { + return im.isolation.has(key) +} + +func (im *IdentityMap[K, V]) SetSize(size uint) { + im.manageable.SetSize(size) +} + +func (im *IdentityMap[K, V]) SetIsolationLevel(level IsolationLevel) { + + switch level { + case ReadUncommitted: + im.isolation = &readUncommittedStrategy[K, V]{manageable: im.manageable} + case RepeatableReads: + im.isolation = &repeatableReadsStrategy[K, V]{manageable: im.manageable} + case Serializable: + im.isolation = &serializableStrategy[K, V]{manageable: im.manageable} + case ReadCommitted: + im.isolation = &readCommittedStrategy[K, V]{manageable: im.manageable} + default: + im.isolation = &serializableStrategy[K, V]{manageable: im.manageable} + } +} diff --git a/grade/internal/infrastructure/seedwork/identity/strategy.go b/grade/internal/infrastructure/seedwork/identity/strategy.go new file mode 100644 index 00000000..2ad032ae --- /dev/null +++ b/grade/internal/infrastructure/seedwork/identity/strategy.go @@ -0,0 +1,91 @@ +package identity + +import ( + "errors" + + "github.com/emacsway/grade/grade/pkg/collections" +) + +type IsolationLevel uint + +const ( + ReadUncommitted IsolationLevel = iota + ReadCommitted = iota + RepeatableReads = iota + Serializable = iota +) + +var ( + ErrNonexistentObject = errors.New("") + ErrDeniedOperationForStrategy = errors.New("") +) + +type IsolationStrategy[K comparable, V any] interface { + add(key K, object V) error + get(key K) (V, error) + has(key K) bool +} + +type readUncommittedStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, 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 +} + +func (r *readUncommittedStrategy[K, V]) has(key K) bool { + return false +} + +type readCommittedStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] +} + +func (r *readCommittedStrategy[K, V]) add(key K, object V) error { + return nil +} + +func (r *readCommittedStrategy[K, V]) get(key K) (object V, err error) { + return object, nil +} + +func (r *readCommittedStrategy[K, V]) has(key K) bool { + return false +} + +type repeatableReadsStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] +} + +func (r *repeatableReadsStrategy[K, V]) add(key K, object V) error { + return nil +} + +func (r *repeatableReadsStrategy[K, V]) get(key K) (V, error) { + return r.manageable.Get(key) +} + +func (r *repeatableReadsStrategy[K, V]) has(key K) bool { + return r.manageable.Has(key) +} + +type serializableStrategy[K comparable, V any] struct { + manageable collections.ReplacingMap[K, V] +} + +func (s *serializableStrategy[K, V]) add(key K, object V) error { + return nil +} + +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) +} diff --git a/grade/internal/infrastructure/seedwork/session/identity_map.go b/grade/internal/infrastructure/seedwork/session/identity_map.go new file mode 100644 index 00000000..48c39872 --- /dev/null +++ b/grade/internal/infrastructure/seedwork/session/identity_map.go @@ -0,0 +1,180 @@ +package session + +import "errors" + +type IsolationLevel int + +type NonexistentObject struct{} + +var ( + ErrNonexistentObject = errors.New("") + ErrUnknownKey = errors.New("") +) + +const ( + ReadUncommittedLevel IsolationLevel = iota + ReadCommittedLevel + RepeatableReadsLevel + SerializableLevel +) + +type IsolationStrategy[K any] interface { + add(key K, value any) error + get(key K) (any, error) + has(key K) (bool, error) +} + +type SerializableStrategy[K comparable] struct { + identityMap *IdentityMapImpl[K] +} + +func (s *SerializableStrategy[K]) add(key K, value any) error { + if value == nil { + value = NonexistentObject{} + } + + s.identityMap.doAdd(key, value) + return nil +} + +func (s *SerializableStrategy[K]) get(key K) (any, error) { + + object := s.identityMap.doGet(key) + if _, ok := object.(NonexistentObject); ok || object == nil { + return nil, ErrNonexistentObject + } + + return object, nil +} + +func (s *SerializableStrategy[K]) has(key K) (bool, error) { + return s.identityMap.doHas(key), nil +} + +type ReadUncommittedStrategy[K comparable] struct { + identityMap *IdentityMapImpl[K] +} + +func (s *ReadUncommittedStrategy[K]) add(key K, value any) error { + return nil +} + +func (s *ReadUncommittedStrategy[K]) get(key K) (any, error) { + return nil, ErrUnknownKey +} + +func (s *ReadUncommittedStrategy[K]) has(key K) (bool, error) { + return false, nil +} + +type ReadCommittedStrategy[K comparable] struct { + identityMap *IdentityMapImpl[K] +} + +func (s *ReadCommittedStrategy[K]) add(key K, value any) error { + return nil +} + +func (s *ReadCommittedStrategy[K]) get(key K) (any, error) { + return nil, ErrUnknownKey +} + +func (s *ReadCommittedStrategy[K]) has(key K) (bool, error) { + return false, nil +} + +type RepeatableReadsStrategy[K comparable] struct { + identityMap *IdentityMapImpl[K] +} + +func (s *RepeatableReadsStrategy[K]) add(key K, value any) error { + if value != nil { + s.identityMap.doAdd(key, value) + } + + return nil +} + +func (s *RepeatableReadsStrategy[K]) get(key K) (any, error) { + object := s.identityMap.doGet(key) + if _, ok := object.(NonexistentObject); ok || object == nil { + return nil, ErrNonexistentObject + } + + return object, nil +} + +func (s *RepeatableReadsStrategy[K]) has(key K) (bool, error) { + if !s.identityMap.doHas(key) { + return false, ErrUnknownKey + } + + object := s.identityMap.doGet(key) + _, ok := object.(NonexistentObject) + + return ok, nil +} + +type IdentityMapImpl[K comparable] struct { + alive map[K]any + strategy IsolationStrategy[K] +} + +func NewIdentityMap[K comparable](isolation IsolationLevel) IdentityMap[K] { + identity := &IdentityMapImpl[K]{ + alive: map[K]any{}, + } + + identity.SetIsolationLevel(isolation) + return identity +} + +func (i *IdentityMapImpl[K]) Get(key K) (any, error) { + return i.strategy.get(key) +} + +func (i *IdentityMapImpl[K]) Add(key K, value any) error { + return i.strategy.add(key, value) +} + +func (i *IdentityMapImpl[K]) Has(key K) (bool, error) { + return i.strategy.has(key) +} + +func (i *IdentityMapImpl[K]) Clear() { + i.alive = map[K]any{} +} + +func (i *IdentityMapImpl[K]) Remove(key K) { + if _, found := i.alive[key]; !found { + return + } + + delete(i.alive, key) +} + +func (i *IdentityMapImpl[K]) SetIsolationLevel(isolation IsolationLevel) { + switch isolation { + case SerializableLevel: + i.strategy = &SerializableStrategy[K]{i} + case ReadUncommittedLevel: + i.strategy = &ReadUncommittedStrategy[K]{i} + case ReadCommittedLevel: + i.strategy = &ReadCommittedStrategy[K]{i} + case RepeatableReadsLevel: + i.strategy = &RepeatableReadsStrategy[K]{i} + } +} + +func (i *IdentityMapImpl[K]) doAdd(key K, value any) { + i.alive[key] = value +} + +func (i *IdentityMapImpl[K]) doGet(key K) any { + return i.alive[key] +} + +func (i *IdentityMapImpl[K]) doHas(key K) bool { + _, found := i.alive[key] + return found +} diff --git a/grade/internal/infrastructure/seedwork/session/identity_map_test.go b/grade/internal/infrastructure/seedwork/session/identity_map_test.go new file mode 100644 index 00000000..f3566297 --- /dev/null +++ b/grade/internal/infrastructure/seedwork/session/identity_map_test.go @@ -0,0 +1,106 @@ +package session + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type Model struct { + pk int +} + +func TestIdentityMap(t *testing.T) { + + tests := []struct { + name string + testCase func(t *testing.T) + }{ + { + name: "Test IdentityMap with Serializable Level", + testCase: func(t *testing.T) { + idMap := NewIdentityMap[int](SerializableLevel) + + model := Model{3} + + err := idMap.Add(model.pk, model) + assert.NoError(t, err) + + exists, err := idMap.Has(model.pk) + assert.NoError(t, err) + assert.Equal(t, true, exists) + + result, err := idMap.Get(model.pk) + assert.NoError(t, err) + + assert.Equal(t, model, result) + + _, err = idMap.Get(10) + assert.Equal(t, ErrNonexistentObject, err) + + err = idMap.Add(10, nil) + assert.NoError(t, err) + + _, err = idMap.Get(10) + assert.Equal(t, ErrNonexistentObject, err) + }, + }, + + { + name: "Test IdentityMap object removing", + testCase: func(t *testing.T) { + idMap := NewIdentityMap[int](SerializableLevel) + + _ = idMap.Add(3, Model{3}) + _ = idMap.Add(5, Model{5}) + + idMap.Remove(3) + idMap.Remove(1) // remove non-exists + + _, err := idMap.Get(3) + assert.Equal(t, ErrNonexistentObject, err) + + _, err = idMap.Get(5) + assert.NoError(t, err) + }, + }, + + { + name: "Test IdentityMap clearing", + testCase: func(t *testing.T) { + idMap := NewIdentityMap[int](SerializableLevel) + + models := []Model{ + Model{3}, + Model{5}, + Model{15}, + } + + for _, model := range models { + err := idMap.Add(model.pk, model) + assert.NoError(t, err) + } + + for _, model := range models { + _, err := idMap.Get(model.pk) + assert.NoError(t, err) + } + + idMap.Clear() + + for _, model := range models { + _, err := idMap.Get(model.pk) + assert.Equal(t, ErrNonexistentObject, err) + } + }, + }, + } + + t.Parallel() + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tt.testCase(t) + }) + } +} diff --git a/grade/internal/infrastructure/seedwork/session/interfaces.go b/grade/internal/infrastructure/seedwork/session/interfaces.go index 805a19dc..2333d981 100644 --- a/grade/internal/infrastructure/seedwork/session/interfaces.go +++ b/grade/internal/infrastructure/seedwork/session/interfaces.go @@ -83,3 +83,13 @@ type DeferredDbSession interface { DeferredDbSessionQuerier DeferredDbSessionSingleQuerier } + +type IdentityMap[K comparable] interface { + Get(key K) (any, error) + Add(key K, value any) error + Has(key K) (bool, error) + Remove(key K) + Clear() + + SetIsolationLevel(isolation IsolationLevel) +} diff --git a/grade/internal/infrastructure/seedwork/session/pgx_session.go b/grade/internal/infrastructure/seedwork/session/pgx_session.go index 60001a60..09b48d7a 100644 --- a/grade/internal/infrastructure/seedwork/session/pgx_session.go +++ b/grade/internal/infrastructure/seedwork/session/pgx_session.go @@ -1,9 +1,8 @@ package session import ( - "strings" - "database/sql" + "strings" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -24,7 +23,7 @@ type PgxSession struct { } func (s *PgxSession) Atomic(callback session.SessionCallback) error { - // TODO: Add support for SavePoint: + // TODO: add support for SavePoint: // https://github.com/golang/go/issues/7898#issuecomment-580080390 if s.db == nil { return errors.New("savePoint is not currently supported") 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) + }) + } +}