Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
.vendor
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/Go.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion 0-limit-crawler/check_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand Down
2 changes: 1 addition & 1 deletion 0-limit-crawler/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// Your task is to change the code to limit the crawler to at most one
// page per second, while maintaining concurrency (in other words,
// page per second, while maintaining 6-cancellation (in other words,
// Crawl() must be called concurrently)
//
// @hint: you can achieve this by adding 3 lines
Expand Down
2 changes: 1 addition & 1 deletion 0-limit-crawler/mockfetcher.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand Down
2 changes: 1 addition & 1 deletion 1-producer-consumer/mockstream.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand Down
4 changes: 2 additions & 2 deletions 2-race-in-cache/check_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand Down Expand Up @@ -39,7 +39,7 @@ func TestLRU(t *testing.T) {
wg.Add(1)
go func(i int) {
value := cache.Get("Test" + strconv.Itoa(i))
if value != "Test" + strconv.Itoa(i) {
if value != "Test"+strconv.Itoa(i) {
t.Errorf("Incorrect db response %v", value)
}
wg.Done()
Expand Down
160 changes: 129 additions & 31 deletions 2-race-in-cache/main.go
Original file line number Diff line number Diff line change
@@ -1,65 +1,163 @@
//////////////////////////////////////////////////////////////////////
//
// Given is some code to cache key-value pairs from a database into
// the main memory (to reduce access time). Note that golang's map are
// not entirely thread safe. Multiple readers are fine, but multiple
// writers are not. Change the code to make this thread safe.
//

package main

import (
"container/list"
"fmt"
"sync"
"testing"
)

// CacheSize determines how big the cache can grow
const CacheSize = 100

// KeyStoreCacheLoader is an interface for the KeyStoreCache
type KeyStoreCacheLoader interface {
// Load implements a function where the cache should gets it's content from
Load(string) string
}

// page represents an item in our cache.
type page struct {
Key string
Value string
}

// KeyStoreCache is a LRU cache for string key-value pairs
// Future represents a pending or completed result for a key.
// It allows multiple goroutines to wait for the result of a single load operation.
type Future struct {
wg sync.WaitGroup // Used to wait for the load to complete
result *list.Element // Pointer to the list element when done
err error // Any error during loading
once sync.Once // Ensures load is called only once
loader func() (string, error) // The function to perform the actual load
}

func newFuture(loader func() (string, error)) *Future {
f := &Future{
loader: loader,
}
f.wg.Add(1) // Initialize wait group for 1 completion
return f
}

// Do performs the actual loading operation exactly once.
func (f *Future) Do() {
f.once.Do(func() {
// Simulate a time-consuming load operation
val, err := f.loader()
if err != nil {
f.err = err
} else {
f.result = &list.Element{Value: &page{"", val}}
}
f.wg.Done() // Signal that loading is complete
})
}

// Wait blocks until the future's operation is complete and returns the result.
func (f *Future) Wait() (*list.Element, error) {
f.wg.Wait()
return f.result, f.err
}

// SetResult sets the list.Element once the loading is done and added to the list.
func (f *Future) SetResult(e *list.Element) {
f.result = e
}

// KeyStoreCache implements a concurrent LRU cache.
type KeyStoreCache struct {
cache map[string]*list.Element
pages list.List
load func(string) string
mu sync.RWMutex // Guards access to cache and pages
cache map[string]*Future // Maps key to its Future (pending or completed)
pages *list.List // Doubly linked list for LRU eviction
load func(key string) string // The actual resource loading function
}

// New creates a new KeyStoreCache
// NewKeyStoreCache creates a new concurrent LRU cache.
func New(load KeyStoreCacheLoader) *KeyStoreCache {
return &KeyStoreCache{
cache: make(map[string]*Future),
pages: list.New(),
load: load.Load,
cache: make(map[string]*list.Element),
}
}

// Get gets the key from cache, loads it from the source if needed
// Get retrieves a value from the cache, loading it if necessary.
func (k *KeyStoreCache) Get(key string) string {
if e, ok := k.cache[key]; ok {
k.pages.MoveToFront(e)
return e.Value.(page).Value
// --- Phase 1: Check for existing entry (read-locked) ---
k.mu.RLock() // Acquire a read lock
f, ok := k.cache[key]
k.mu.RUnlock() // Release read lock quickly

if ok {
elem, err := f.Wait() // This blocks if the future is not yet done
if err != nil {
// Handle load error here if you want to propagate it
fmt.Printf("Error loading key '%s': %v\n", key, err)
return "" // Or re-attempt load, or return a specific error
}

k.mu.Lock()
k.pages.MoveToFront(elem)
k.mu.Unlock()

return elem.Value.(*page).Value
}
// Miss - load from database and save it in cache
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)

k.mu.Lock()
f, ok = k.cache[key]
if ok {
// Another goroutine beat us to it. Release lock and wait for its result.
k.mu.Unlock()
elem, err := f.Wait()
if err != nil {
fmt.Printf("Error loading key '%s': %v\n", key, err)
return ""
}
k.mu.Lock() // Re-acquire lock to move to front
k.pages.MoveToFront(elem)
k.mu.Unlock()
return elem.Value.(*page).Value
}
k.pages.PushFront(p)
k.cache[key] = k.pages.Front()

// It's genuinely not in the cache. Create a new future.
newF := newFuture(func() (string, error) {
// The actual load operation that will be called by Do()
val := k.load(key)
return val, nil // Assuming k.load doesn't return an error, adjust if it does
})
k.cache[key] = newF
k.mu.Unlock() // Release the write lock *before* calling Do()

newF.Do() // This will call the loader function for this key exactly once.

// Now that loading is complete, acquire write lock again to update LRU and set result.
k.mu.Lock()
defer k.mu.Unlock() // Ensure lock is released

// Check for eviction before adding the new item
if k.pages.Len() >= CacheSize {
oldest := k.pages.Back()
if oldest != nil {
pToDelete := oldest.Value.(*page)
delete(k.cache, pToDelete.Key) // Remove from map
k.pages.Remove(oldest) // Remove from list
fmt.Printf("Evicting key: %s\n", pToDelete.Key)
}
}

// Get the loaded result from the future
loadedElem, err := newF.Wait() // This should return immediately now as Do() just completed.
if err != nil {
// Handle the error (e.g., remove from cache if load failed permanently)
delete(k.cache, key)
fmt.Printf("Final error after load for key '%s': %v\n", key, err)
return ""
}

// Add the new page to the front of the list and set its result in the future.
p := &page{key, loadedElem.Value.(*page).Value} // Re-create page to get its value
elem := k.pages.PushFront(p)
newF.SetResult(elem) // Set the actual list.Element in the future for future lookups

return p.Value
}

Expand Down
4 changes: 2 additions & 2 deletions 2-race-in-cache/mockdb.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand All @@ -12,7 +12,7 @@ import (
)

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

Expand Down
4 changes: 2 additions & 2 deletions 2-race-in-cache/mockserver.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand Down Expand Up @@ -31,7 +31,7 @@ func RunMockServer(cache *KeyStoreCache, t *testing.T) {
go func(i int) {
value := cache.Get("Test" + strconv.Itoa(i))
if t != nil {
if value != "Test" + strconv.Itoa(i) {
if value != "Test"+strconv.Itoa(i) {
t.Errorf("Incorrect db response %v", value)
}
}
Expand Down
2 changes: 1 addition & 1 deletion 3-limit-service-time/mockserver.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand Down
4 changes: 2 additions & 2 deletions 4-graceful-sigint/mockprocess.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand All @@ -15,7 +15,7 @@ import (

// MockProcess for example
type MockProcess struct {
mu sync.Mutex
mu sync.Mutex
isRunning bool
}

Expand Down
2 changes: 1 addition & 1 deletion 5-session-cleaner/helper.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////
//
// DO NOT EDIT THIS PART
// Your task is to edit `main.go`
// Your task is to edit `base.go`
//

package main
Expand Down
7 changes: 7 additions & 0 deletions 6-cancellation/cancellation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Exercise requirements

You are implementing a DB instance which is used to query data at external DB through a driver called `EmulatedDriver`.
Your task is to implement `QueryContext`, which must ensure:
1. When the context is timed out or get cancelled, you must return as soon as possible.
2. Before return, ensuring all the resource of the operation is clean up.
3. The operation must return errors if a failure happens.
3 changes: 3 additions & 0 deletions 6-cancellation/cancellation/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module go_concurrency/cancellation

go 1.24.2
Loading