Skip to content

Commit 65fb751

Browse files
authored
Cache managment (#49)
* code cleanup * add cache invalidation * extract cache into own module * extract mutex * add mutex test * fix tests * fix rename * cache is internally managed * test mutexes and bugfix * add cache test * add headers * add sanity check * cleanup test * fix it could be 0
1 parent ef30981 commit 65fb751

File tree

11 files changed

+422
-116
lines changed

11 files changed

+422
-116
lines changed

blockchain/cache.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Flow Playground
3+
*
4+
* Copyright 2019 Dapper Labs, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package blockchain
20+
21+
import (
22+
"fmt"
23+
"github.com/dapperlabs/flow-playground-api/model"
24+
"github.com/getsentry/sentry-go"
25+
"github.com/golang/groupcache/lru"
26+
"github.com/google/uuid"
27+
"github.com/pkg/errors"
28+
)
29+
30+
// newCache returns a new instance of cache with provided capacity.
31+
func newCache(capacity int) *cache {
32+
return &cache{
33+
cache: lru.New(capacity),
34+
}
35+
}
36+
37+
type cache struct {
38+
cache *lru.Cache
39+
}
40+
41+
// reset the cache for the ID.
42+
func (c *cache) reset(ID uuid.UUID) {
43+
c.cache.Remove(ID)
44+
}
45+
46+
// get returns a cached emulator if exists, but also checks if it's stale.
47+
//
48+
// based on the executions the function receives it compares that to the emulator block height, since
49+
// one execution is always one block it can compare the heights to the length. If it finds some executions
50+
// that are not part of emulator it returns that subset, so they can be applied on top.
51+
func (c *cache) get(
52+
ID uuid.UUID,
53+
executions []*model.TransactionExecution,
54+
) (blockchain, []*model.TransactionExecution, error) {
55+
val, ok := c.cache.Get(ID)
56+
if !ok {
57+
return nil, executions, nil
58+
}
59+
60+
emulator := val.(blockchain)
61+
latest, err := emulator.getLatestBlock()
62+
if err != nil {
63+
return nil, nil, errors.Wrap(err, "cache failure")
64+
}
65+
66+
// this should never happen, sanity check
67+
if int(latest.Header.Height) > len(executions) {
68+
err := fmt.Errorf("cache failure, block height is higher than executions count")
69+
sentry.CaptureException(err)
70+
return nil, nil, err
71+
}
72+
73+
// this will return only executions that are missing from the emulator
74+
return emulator, executions[latest.Header.Height:], nil
75+
}
76+
77+
// add new entry in the cache.
78+
func (c *cache) add(ID uuid.UUID, emulator blockchain) {
79+
c.cache.Add(ID, emulator)
80+
}

blockchain/cache_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Flow Playground
3+
*
4+
* Copyright 2019 Dapper Labs, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package blockchain
20+
21+
import (
22+
"fmt"
23+
"github.com/dapperlabs/flow-playground-api/model"
24+
"github.com/google/uuid"
25+
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
27+
"testing"
28+
)
29+
30+
func createExecutions(count int) []*model.TransactionExecution {
31+
executions := make([]*model.TransactionExecution, count)
32+
for i := 0; i < count; i++ {
33+
executions[i] = &model.TransactionExecution{
34+
ProjectChildID: model.NewProjectChildID(uuid.New(), uuid.New()),
35+
Index: i,
36+
Script: fmt.Sprintf(`transaction { execute { log(%d) } }`, i),
37+
}
38+
}
39+
return executions
40+
}
41+
42+
func Test_Cache(t *testing.T) {
43+
44+
t.Run("returns cached emulator", func(t *testing.T) {
45+
testID := uuid.New()
46+
c := newCache(2)
47+
48+
em, err := newEmulator()
49+
require.NoError(t, err)
50+
51+
c.add(testID, em)
52+
53+
cacheEm, exe, err := c.get(testID, nil)
54+
require.NoError(t, err)
55+
assert.Len(t, exe, 0)
56+
57+
cacheBlock, err := cacheEm.getLatestBlock()
58+
require.NoError(t, err)
59+
60+
block, err := em.getLatestBlock()
61+
require.NoError(t, err)
62+
63+
assert.Equal(t, block.ID(), cacheBlock.ID())
64+
})
65+
66+
t.Run("returns cached emulator with executions", func(t *testing.T) {
67+
testID := uuid.New()
68+
c := newCache(2)
69+
70+
em, err := newEmulator()
71+
require.NoError(t, err)
72+
73+
c.add(testID, em)
74+
75+
executions := createExecutions(5)
76+
for _, exe := range executions {
77+
_, _, err := em.executeTransaction(exe.Script, exe.Arguments, nil)
78+
require.NoError(t, err)
79+
}
80+
81+
cachedEm, cacheExe, err := c.get(testID, executions)
82+
require.NoError(t, err)
83+
// cached emulator contains all the executions
84+
assert.Len(t, cacheExe, 0)
85+
// make sure emulators are same
86+
cacheBlock, _ := cachedEm.getLatestBlock()
87+
block, _ := em.getLatestBlock()
88+
assert.Equal(t, cacheBlock.ID(), block.ID())
89+
})
90+
91+
t.Run("returns cached emulator with missing executions", func(t *testing.T) {
92+
testID := uuid.New()
93+
c := newCache(2)
94+
95+
em, err := newEmulator()
96+
require.NoError(t, err)
97+
98+
c.add(testID, em)
99+
100+
executions := createExecutions(5)
101+
102+
for i, exe := range executions {
103+
if i == 3 {
104+
break // miss last two executions
105+
}
106+
_, _, err := em.executeTransaction(exe.Script, exe.Arguments, nil)
107+
require.NoError(t, err)
108+
}
109+
110+
_, cacheExe, err := c.get(testID, executions)
111+
require.NoError(t, err)
112+
113+
// cached emulator missed two executions
114+
assert.Len(t, cacheExe, 2)
115+
assert.Equal(t, 3, cacheExe[0].Index)
116+
assert.Equal(t, 4, cacheExe[1].Index)
117+
})
118+
}

blockchain/emulator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func newEmulator() (*emulator, error) {
8181
emu.WithTransactionFeesEnabled(false),
8282
)
8383
if err != nil {
84-
return nil, err
84+
return nil, errors.Wrap(err, "failed to create a new emulator instance")
8585
}
8686

8787
return &emulator{

blockchain/mutex.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package blockchain
2+
3+
import (
4+
"github.com/getsentry/sentry-go"
5+
"github.com/google/uuid"
6+
"sync"
7+
)
8+
9+
func newMutex() *mutex {
10+
return &mutex{}
11+
}
12+
13+
// mutex contains locking logic for projects.
14+
//
15+
// this custom implementation of mutex creates per project ID mutex lock, this is needed because
16+
// we need to restrict access to common resource (emulator) based on project ID, and we can not put a mutex lock
17+
// on the emulator instance since it takes time to load the emulator in the first place, during which racing conditions may occur.
18+
// Mutex keeps a map of mutex locks per project ID, and it also keeps a track of obtained locks per that ID so it can, after all
19+
// the locks have been released remove that lock from the mutex map to not pollute memory.
20+
type mutex struct {
21+
mu sync.Map
22+
muCounter sync.Map
23+
}
24+
25+
// load retrieves the mutex lock by the project ID and increase the usage counter.
26+
func (m *mutex) load(uuid uuid.UUID) *sync.RWMutex {
27+
counter, _ := m.muCounter.LoadOrStore(uuid, 0)
28+
m.muCounter.Store(uuid, counter.(int)+1)
29+
30+
mu, _ := m.mu.LoadOrStore(uuid, &sync.RWMutex{})
31+
return mu.(*sync.RWMutex)
32+
}
33+
34+
// remove returns the mutex lock by the project ID and decreases usage counter, deleting the map entry if at 0.
35+
func (m *mutex) remove(uuid uuid.UUID) *sync.RWMutex {
36+
mu, ok := m.mu.Load(uuid)
37+
if !ok {
38+
sentry.CaptureMessage("trying to access non-existing mutex")
39+
}
40+
41+
counter, ok := m.muCounter.Load(uuid)
42+
if !ok {
43+
sentry.CaptureMessage("trying to access non-existing mutex counter")
44+
}
45+
46+
if counter == 1 { // if last one remove it after
47+
m.mu.Delete(uuid)
48+
m.muCounter.Delete(uuid)
49+
} else {
50+
m.muCounter.Store(uuid, counter.(int)-1)
51+
}
52+
53+
return mu.(*sync.RWMutex)
54+
}

blockchain/mutex_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Flow Playground
3+
*
4+
* Copyright 2019 Dapper Labs, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package blockchain
20+
21+
import (
22+
"github.com/google/uuid"
23+
"github.com/stretchr/testify/assert"
24+
"testing"
25+
)
26+
27+
func Test_Mutex(t *testing.T) {
28+
mutex := newMutex()
29+
30+
testUuid := uuid.New()
31+
32+
m := mutex.load(testUuid)
33+
m.Lock()
34+
35+
v, _ := mutex.muCounter.Load(testUuid)
36+
assert.Equal(t, 1, v.(int))
37+
38+
_, exists := mutex.mu.Load(testUuid)
39+
assert.True(t, exists)
40+
41+
m1 := mutex.load(testUuid)
42+
locked := m1.TryLock()
43+
// should fail since we already have one lock
44+
assert.False(t, locked)
45+
46+
v, _ = mutex.muCounter.Load(testUuid)
47+
assert.Equal(t, 2, v.(int))
48+
49+
mutex.remove(testUuid).Unlock()
50+
51+
v, _ = mutex.muCounter.Load(testUuid)
52+
assert.Equal(t, 1, v.(int))
53+
54+
locked = m1.TryLock()
55+
assert.True(t, locked) // should succeed now
56+
57+
mutex.remove(testUuid).Unlock()
58+
59+
// after all locks are released there shouldn't be any counter left
60+
_, found := mutex.muCounter.Load(testUuid)
61+
assert.False(t, found)
62+
63+
_, found = mutex.mu.Load(testUuid)
64+
assert.False(t, found)
65+
}

0 commit comments

Comments
 (0)