From fbf36ef1a4a45dd274d9a84f0e8c47a4cc9356fc 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 --- .../seedwork/session/identity_map.go | 107 ++++++++++++++++++ .../seedwork/session/identity_map_test.go | 106 +++++++++++++++++ .../seedwork/session/interfaces.go | 10 ++ 3 files changed, 223 insertions(+) create mode 100644 grade/internal/infrastructure/seedwork/session/identity_map.go create mode 100644 grade/internal/infrastructure/seedwork/session/identity_map_test.go 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..7b0b39d0 --- /dev/null +++ b/grade/internal/infrastructure/seedwork/session/identity_map.go @@ -0,0 +1,107 @@ +package session + +import "errors" + +type IsolationLevel int + +type NonexistentObject struct{} + +var ErrNonexistentObject = 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 SerializableStrategyImpl[K comparable] struct { + identityMap *IdentityMapImpl[K] +} + +func (s *SerializableStrategyImpl[K]) add(key K, value any) error { + if value == nil { + value = NonexistentObject{} + } + + s.identityMap.doAdd(key, value) + return nil +} + +func (s *SerializableStrategyImpl[K]) get(key K) (any, error) { + + entity := s.identityMap.doGet(key) + if _, ok := entity.(NonexistentObject); ok || entity == nil { + return nil, ErrNonexistentObject + } + + return entity, nil +} + +func (s *SerializableStrategyImpl[K]) has(key K) (bool, error) { + return s.identityMap.doHas(key), 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 = &SerializableStrategyImpl[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) +}