Skip to content

Commit

Permalink
Merge pull request loong#12 from benprew/master
Browse files Browse the repository at this point in the history
Couple of changes to exercise 2
  • Loading branch information
loong committed Sep 7, 2020
2 parents 8872b65 + cd4ebd6 commit a36886f
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 21 deletions.
23 changes: 20 additions & 3 deletions 2-race-in-cache/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Race condition in caching szenario
# Race condition in caching scenario

Given is some code to cache key-value pairs from a mock database into
the main memory (to reduce access time). The code is buggy and
contains a race conditition. Change the code to make this thread safe.

*Note*: that golang's map are not entirely thread safe. Multiple readers
are fine, but multiple writers are not.
Also, try to get your solution down to less than 30 seconds to run tests. *Hint*: fetching from the database takes the longest.

*Note*: Map access is unsafe only when updates are occurring. As long as all goroutines are only reading and not changing the map, it is safe to access the map concurrently without synchronization. (See [https://golang.org/doc/faq#atomic_maps](https://golang.org/doc/faq#atomic_maps))

## Background Reading

* [https://tour.golang.org/concurrency/9](https://tour.golang.org/concurrency/9)
* [https://golang.org/ref/mem](https://golang.org/ref/mem)

# Test your solution

Expand All @@ -30,3 +36,14 @@ Write by goroutine 7:
==================
Found 3 data race(s)
```

## Additional Reading

* [https://golang.org/pkg/sync/](https://golang.org/pkg/sync/)
* [https://gobyexample.com/mutexes](https://gobyexample.com/mutexes)
* [https://golangdocs.com/mutex-in-golang](https://golangdocs.com/mutex-in-golang)

### High Performance Caches in Production

* [https://www.mailgun.com/blog/golangs-superior-cache-solution-memcached-redis/](https://www.mailgun.com/blog/golangs-superior-cache-solution-memcached-redis/)
* [https://allegro.tech/2016/03/writing-fast-cache-service-in-go.html](https://allegro.tech/2016/03/writing-fast-cache-service-in-go.html)
26 changes: 25 additions & 1 deletion 2-race-in-cache/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

package main

import "testing"
import (
"strconv"
"testing"
)

func TestMain(t *testing.T) {
cache := run()
Expand All @@ -20,3 +23,24 @@ func TestMain(t *testing.T) {
t.Errorf("Incorrect pages size %v", pagesLen)
}
}

func TestLRU(t *testing.T) {
loader := Loader{
DB: GetMockDB(),
}
cache := New(&loader)

for i := 0; i < 100; i++ {
cache.Get("Test" + strconv.Itoa(i))
}

if len(cache.cache) != 100 {
t.Errorf("cache not 100: %d", len(cache.cache))
}
cache.Get("Test0")
cache.Get("Test101")
if _, ok := cache.cache["Test0"]; !ok {
t.Errorf("0 evicted incorrectly: %v", cache.cache)
}

}
38 changes: 22 additions & 16 deletions 2-race-in-cache/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ type KeyStoreCacheLoader interface {
Load(string) string
}

type page struct {
Key string
Value string
}

// KeyStoreCache is a LRU cache for string key-value pairs
type KeyStoreCache struct {
cache map[string]string
cache map[string]*list.Element
pages list.List
load func(string) string
}
Expand All @@ -30,28 +35,29 @@ type KeyStoreCache struct {
func New(load KeyStoreCacheLoader) *KeyStoreCache {
return &KeyStoreCache{
load: load.Load,
cache: make(map[string]string),
cache: make(map[string]*list.Element),
}
}

// Get gets the key from cache, loads it from the source if needed
func (k *KeyStoreCache) Get(key string) string {
val, ok := k.cache[key]

if e, ok := k.cache[key]; ok {
k.pages.MoveToFront(e)
return e.Value.(page).Value
}
// Miss - load from database and save it in cache
if !ok {
val = k.load(key)
k.cache[key] = val
k.pages.PushFront(key)

// if cache is full remove the least used item
if len(k.cache) > CacheSize {
delete(k.cache, k.pages.Back().Value.(string))
k.pages.Remove(k.pages.Back())
}
p := page{key, k.load(key)}
// if cache is full remove the least used item
if len(k.cache) >= CacheSize {
end := k.pages.Back()
// remove from map
delete(k.cache, end.Value.(page).Key)
// remove from list
k.pages.Remove(end)
}

return val
k.pages.PushFront(p)
k.cache[key] = k.pages.Front()
return p.Value
}

// Loader implements KeyStoreLoader
Expand Down
4 changes: 4 additions & 0 deletions 2-race-in-cache/mockdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

package main

import "time"

// MockDB used to simulate a database model
type MockDB struct{}

// Get only returns an empty string, as this is only for demonstration purposes
func (*MockDB) Get(key string) (string, error) {
d, _ := time.ParseDuration("20ms")
time.Sleep(d)
return "", nil
}

Expand Down
2 changes: 1 addition & 1 deletion 2-race-in-cache/mockserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

const (
cycles = 3
cycles = 15
callsPerCycle = 100
)

Expand Down

0 comments on commit a36886f

Please sign in to comment.