├── .travis.yml ├── 0-limit-crawler ├── README.md ├── check_test.go ├── main.go └── mockfetcher.go ├── 1-producer-consumer ├── README.md ├── main.go └── mockstream.go ├── 2-race-in-cache ├── README.md ├── check_test.go ├── main.go ├── mockdb.go └── mockserver.go ├── 3-limit-service-time ├── README.md ├── main.go └── mockserver.go ├── 4-graceful-sigint ├── README.md ├── main.go └── mockprocess.go ├── 5-session-cleaner ├── README.md ├── check_test.go ├── helper.go └── main.go ├── LICENSE ├── README.md ├── go.mod └── go.sum /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | 7 | install: 8 | - go get ./... 9 | 10 | before_script: 11 | - find . -name "check_test.go" | xargs rm # exclude solutions checks -------------------------------------------------------------------------------- /0-limit-crawler/README.md: -------------------------------------------------------------------------------- 1 | # Limit your crawler 2 | 3 | Given is a crawler (modified from the Go tour) that requests pages 4 | excessively. However, we don't want to burden the webserver too 5 | much. Your task is to change the code to limit the crawler to at most 6 | one page per second, while maintaining concurrency (in other words, 7 | Crawl() must be called concurrently) 8 | 9 | ## Hint 10 | 11 | This exercise can be solved in 3 lines only. If you can't do 12 | it, have a look at this: 13 | https://go.dev/wiki/RateLimiting 14 | 15 | ## Test your solution 16 | 17 | Use `go test` to verify if your solution is correct. 18 | 19 | Correct solution: 20 | ``` 21 | PASS 22 | ok github.com/loong/go-concurrency-exercises/0-limit-crawler 13.009s 23 | ``` 24 | 25 | Incorrect solution: 26 | ``` 27 | --- FAIL: TestMain (7.80s) 28 | main_test.go:18: There exists a two crawls who were executed less than 1 sec apart. 29 | main_test.go:19: Solution is incorrect. 30 | FAIL 31 | exit status 1 32 | FAIL github.com/loong/go-concurrency-exercises/0-limit-crawler 7.808s 33 | ``` 34 | -------------------------------------------------------------------------------- /0-limit-crawler/check_test.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestMain(t *testing.T) { 15 | fetchSig := fetchSignalInstance() 16 | 17 | start := time.Unix(0, 0) 18 | go func(start time.Time) { 19 | for { 20 | switch { 21 | case <-fetchSig: 22 | // Check if signal arrived earlier than a second (with error margin) 23 | if time.Now().Sub(start).Nanoseconds() < 950000000 { 24 | t.Log("There exists a two crawls that were executed less than 1 second apart.") 25 | t.Log("Solution is incorrect.") 26 | t.FailNow() 27 | } 28 | start = time.Now() 29 | } 30 | } 31 | }(start) 32 | 33 | main() 34 | } 35 | -------------------------------------------------------------------------------- /0-limit-crawler/main.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // Your task is to change the code to limit the crawler to at most one 4 | // page per second, while maintaining concurrency (in other words, 5 | // Crawl() must be called concurrently) 6 | // 7 | // @hint: you can achieve this by adding 3 lines 8 | // 9 | 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "sync" 15 | ) 16 | 17 | // Crawl uses `fetcher` from the `mockfetcher.go` file to imitate a 18 | // real crawler. It crawls until the maximum depth has reached. 19 | func Crawl(url string, depth int, wg *sync.WaitGroup) { 20 | defer wg.Done() 21 | 22 | if depth <= 0 { 23 | return 24 | } 25 | 26 | body, urls, err := fetcher.Fetch(url) 27 | if err != nil { 28 | fmt.Println(err) 29 | return 30 | } 31 | 32 | fmt.Printf("found: %s %q\n", url, body) 33 | 34 | wg.Add(len(urls)) 35 | for _, u := range urls { 36 | // Do not remove the `go` keyword, as Crawl() must be 37 | // called concurrently 38 | go Crawl(u, depth-1, wg) 39 | } 40 | } 41 | 42 | func main() { 43 | var wg sync.WaitGroup 44 | 45 | wg.Add(1) 46 | Crawl("http://golang.org/", 4, &wg) 47 | wg.Wait() 48 | } 49 | -------------------------------------------------------------------------------- /0-limit-crawler/mockfetcher.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import "fmt" 10 | 11 | // MockFetcher is Fetcher that returns canned results. Taken from 12 | // https://tour.golang.org/concurrency/10 13 | type MockFetcher map[string]*mockResult 14 | 15 | type mockResult struct { 16 | body string 17 | urls []string 18 | } 19 | 20 | // Fetch pretends to retrieve the URLs and its subpages 21 | func (f MockFetcher) Fetch(url string) (string, []string, error) { 22 | fetchSignalInstance() <- true 23 | if res, ok := f[url]; ok { 24 | return res.body, res.urls, nil 25 | } 26 | return "", nil, fmt.Errorf("not found: %s", url) 27 | } 28 | 29 | // fetcher is a populated MockFetcher. 30 | var fetcher = MockFetcher{ 31 | "http://golang.org/": &mockResult{ 32 | "The Go Programming Language", 33 | []string{ 34 | "http://golang.org/pkg/", 35 | "http://golang.org/cmd/", 36 | }, 37 | }, 38 | "http://golang.org/pkg/": &mockResult{ 39 | "Packages", 40 | []string{ 41 | "http://golang.org/", 42 | "http://golang.org/cmd/", 43 | "http://golang.org/pkg/fmt/", 44 | "http://golang.org/pkg/os/", 45 | }, 46 | }, 47 | "http://golang.org/pkg/fmt/": &mockResult{ 48 | "Package fmt", 49 | []string{ 50 | "http://golang.org/", 51 | "http://golang.org/pkg/", 52 | }, 53 | }, 54 | "http://golang.org/pkg/os/": &mockResult{ 55 | "Package os", 56 | []string{ 57 | "http://golang.org/", 58 | "http://golang.org/pkg/", 59 | }, 60 | }, 61 | } 62 | 63 | ////////////////////////////////////////////////////////////////////// 64 | // Code below is mainly used to test whether a solution is correct or 65 | // not 66 | 67 | // fetchSignal is used to test whether the solution is correct 68 | var fetchSignal chan bool 69 | 70 | // fetchSignalInstance is a singleton to access fetchSignal 71 | func fetchSignalInstance() chan bool { 72 | if fetchSignal == nil { 73 | // Use buffered channel to avoid blocking 74 | fetchSignal = make(chan bool, 1000) 75 | } 76 | return fetchSignal 77 | } 78 | -------------------------------------------------------------------------------- /1-producer-consumer/README.md: -------------------------------------------------------------------------------- 1 | # Producer-Consumer Scenario 2 | 3 | The producer reads in tweets from a mockstream and a consumer is processing the data to find out whether someone has tweeted about golang or not. The task is to modify the code inside `main.go` so that producer and consumer can run concurrently to increase the throughput of this program. 4 | 5 | ## Expected results: 6 | Before: 7 | ``` 8 | davecheney tweets about golang 9 | beertocode does not tweet about golang 10 | ironzeb tweets about golang 11 | beertocode tweets about golang 12 | vampirewalk666 tweets about golang 13 | Process took 3.580866005s 14 | ``` 15 | 16 | After: 17 | ``` 18 | davecheney tweets about golang 19 | beertocode does not tweet about golang 20 | ironzeb tweets about golang 21 | beertocode tweets about golang 22 | vampirewalk666 tweets about golang 23 | Process took 1.977756255s 24 | ``` 25 | -------------------------------------------------------------------------------- /1-producer-consumer/main.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // Given is a producer-consumer scenario, where a producer reads in 4 | // tweets from a mockstream and a consumer is processing the 5 | // data. Your task is to change the code so that the producer as well 6 | // as the consumer can run concurrently 7 | // 8 | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "time" 14 | ) 15 | 16 | func producer(stream Stream) (tweets []*Tweet) { 17 | for { 18 | tweet, err := stream.Next() 19 | if err == ErrEOF { 20 | return tweets 21 | } 22 | 23 | tweets = append(tweets, tweet) 24 | } 25 | } 26 | 27 | func consumer(tweets []*Tweet) { 28 | for _, t := range tweets { 29 | if t.IsTalkingAboutGo() { 30 | fmt.Println(t.Username, "\ttweets about golang") 31 | } else { 32 | fmt.Println(t.Username, "\tdoes not tweet about golang") 33 | } 34 | } 35 | } 36 | 37 | func main() { 38 | start := time.Now() 39 | stream := GetMockStream() 40 | 41 | // Producer 42 | tweets := producer(stream) 43 | 44 | // Consumer 45 | consumer(tweets) 46 | 47 | fmt.Printf("Process took %s\n", time.Since(start)) 48 | } 49 | -------------------------------------------------------------------------------- /1-producer-consumer/mockstream.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "errors" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // GetMockStream is a blackbox function which returns a mock stream for 16 | // demonstration purposes 17 | func GetMockStream() Stream { 18 | return Stream{0, mockdata} 19 | } 20 | 21 | // Stream is a mock stream for demonstration purposes, not threadsafe 22 | type Stream struct { 23 | pos int 24 | tweets []Tweet 25 | } 26 | 27 | // ErrEOF returns on End of File error 28 | var ErrEOF = errors.New("End of File") 29 | 30 | // Next returns the next Tweet in the stream, returns EOF error if 31 | // there are no more tweets 32 | func (s *Stream) Next() (*Tweet, error) { 33 | 34 | // simulate delay 35 | time.Sleep(320 * time.Millisecond) 36 | if s.pos >= len(s.tweets) { 37 | return &Tweet{}, ErrEOF 38 | } 39 | 40 | tweet := s.tweets[s.pos] 41 | s.pos++ 42 | 43 | return &tweet, nil 44 | } 45 | 46 | // Tweet defines the simlified representation of a tweet 47 | type Tweet struct { 48 | Username string 49 | Text string 50 | } 51 | 52 | // IsTalkingAboutGo is a mock process which pretend to be a sophisticated procedure to analyse whether tweet is talking about go or not 53 | func (t *Tweet) IsTalkingAboutGo() bool { 54 | // simulate delay 55 | time.Sleep(330 * time.Millisecond) 56 | 57 | hasGolang := strings.Contains(strings.ToLower(t.Text), "golang") 58 | hasGopher := strings.Contains(strings.ToLower(t.Text), "gopher") 59 | 60 | return hasGolang || hasGopher 61 | } 62 | 63 | var mockdata = []Tweet{ 64 | { 65 | "davecheney", 66 | "#golang top tip: if your unit tests import any other package you wrote, including themselves, they're not unit tests.", 67 | }, { 68 | "beertocode", 69 | "Backend developer, doing frontend featuring the eternal struggle of centering something. #coding", 70 | }, { 71 | "ironzeb", 72 | "Re: Popularity of Golang in China: My thinking nowadays is that it had a lot to do with this book and author https://github.com/astaxie/build-web-application-with-golang", 73 | }, { 74 | "beertocode", 75 | "Looking forward to the #gopher meetup in Hsinchu tonight with @ironzeb!", 76 | }, { 77 | "vampirewalk666", 78 | "I just wrote a golang slack bot! It reports the state of github repository. #Slack #golang", 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /2-race-in-cache/README.md: -------------------------------------------------------------------------------- 1 | # Race condition in caching scenario 2 | 3 | Given is some code to cache key-value pairs from a mock database into 4 | the main memory (to reduce access time). The code is buggy and 5 | contains a race condition. Change the code to make this thread safe. 6 | 7 | Also, try to get your solution down to less than 30 seconds to run tests. *Hint*: fetching from the database takes the longest. 8 | 9 | *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)) 10 | 11 | If possible, get your solution down to less than 5 seconds for all tests. 12 | 13 | ## Background Reading 14 | 15 | * [https://tour.golang.org/concurrency/9](https://tour.golang.org/concurrency/9) 16 | * [https://golang.org/ref/mem](https://golang.org/ref/mem) 17 | 18 | # Test your solution 19 | 20 | Use the following command to test for race conditions and correct functionality: 21 | ``` 22 | go test -race 23 | ``` 24 | 25 | Correct solution: 26 | No output = solution correct: 27 | ``` 28 | $ go test -race 29 | $ 30 | ``` 31 | 32 | Incorrect solution: 33 | ``` 34 | ================== 35 | WARNING: DATA RACE 36 | Write by goroutine 7: 37 | ... 38 | ================== 39 | Found 3 data race(s) 40 | ``` 41 | 42 | ## Additional Reading 43 | 44 | * [https://golang.org/pkg/sync/](https://golang.org/pkg/sync/) 45 | * [https://gobyexample.com/mutexes](https://gobyexample.com/mutexes) 46 | * [https://golangdocs.com/mutex-in-golang](https://golangdocs.com/mutex-in-golang) 47 | 48 | ### High Performance Caches in Production 49 | 50 | * [https://www.mailgun.com/blog/golangs-superior-cache-solution-memcached-redis/](https://www.mailgun.com/blog/golangs-superior-cache-solution-memcached-redis/) 51 | * [https://allegro.tech/2016/03/writing-fast-cache-service-in-go.html](https://allegro.tech/2016/03/writing-fast-cache-service-in-go.html) 52 | -------------------------------------------------------------------------------- /2-race-in-cache/check_test.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "strconv" 11 | "sync" 12 | "testing" 13 | ) 14 | 15 | func TestMain(t *testing.T) { 16 | cache, db := run(t) 17 | 18 | cacheLen := len(cache.cache) 19 | pagesLen := cache.pages.Len() 20 | if cacheLen != CacheSize { 21 | t.Errorf("Incorrect cache size %v", cacheLen) 22 | } 23 | if pagesLen != CacheSize { 24 | t.Errorf("Incorrect pages size %v", pagesLen) 25 | } 26 | if db.Calls > callsPerCycle { 27 | t.Errorf("Too much db uses %v", db.Calls) 28 | } 29 | } 30 | 31 | func TestLRU(t *testing.T) { 32 | loader := Loader{ 33 | DB: GetMockDB(), 34 | } 35 | cache := New(&loader) 36 | 37 | var wg sync.WaitGroup 38 | for i := 0; i < 100; i++ { 39 | wg.Add(1) 40 | go func(i int) { 41 | value := cache.Get("Test" + strconv.Itoa(i)) 42 | if value != "Test" + strconv.Itoa(i) { 43 | t.Errorf("Incorrect db response %v", value) 44 | } 45 | wg.Done() 46 | }(i) 47 | } 48 | wg.Wait() 49 | 50 | if len(cache.cache) != 100 { 51 | t.Errorf("cache not 100: %d", len(cache.cache)) 52 | } 53 | cache.Get("Test0") 54 | cache.Get("Test101") 55 | if _, ok := cache.cache["Test0"]; !ok { 56 | t.Errorf("0 evicted incorrectly: %v", cache.cache) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /2-race-in-cache/main.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // Given is some code to cache key-value pairs from a database into 4 | // the main memory (to reduce access time). Note that golang's map are 5 | // not entirely thread safe. Multiple readers are fine, but multiple 6 | // writers are not. Change the code to make this thread safe. 7 | // 8 | 9 | package main 10 | 11 | import ( 12 | "container/list" 13 | "testing" 14 | ) 15 | 16 | // CacheSize determines how big the cache can grow 17 | const CacheSize = 100 18 | 19 | // KeyStoreCacheLoader is an interface for the KeyStoreCache 20 | type KeyStoreCacheLoader interface { 21 | // Load implements a function where the cache should gets it's content from 22 | Load(string) string 23 | } 24 | 25 | type page struct { 26 | Key string 27 | Value string 28 | } 29 | 30 | // KeyStoreCache is a LRU cache for string key-value pairs 31 | type KeyStoreCache struct { 32 | cache map[string]*list.Element 33 | pages list.List 34 | load func(string) string 35 | } 36 | 37 | // New creates a new KeyStoreCache 38 | func New(load KeyStoreCacheLoader) *KeyStoreCache { 39 | return &KeyStoreCache{ 40 | load: load.Load, 41 | cache: make(map[string]*list.Element), 42 | } 43 | } 44 | 45 | // Get gets the key from cache, loads it from the source if needed 46 | func (k *KeyStoreCache) Get(key string) string { 47 | if e, ok := k.cache[key]; ok { 48 | k.pages.MoveToFront(e) 49 | return e.Value.(page).Value 50 | } 51 | // Miss - load from database and save it in cache 52 | p := page{key, k.load(key)} 53 | // if cache is full remove the least used item 54 | if len(k.cache) >= CacheSize { 55 | end := k.pages.Back() 56 | // remove from map 57 | delete(k.cache, end.Value.(page).Key) 58 | // remove from list 59 | k.pages.Remove(end) 60 | } 61 | k.pages.PushFront(p) 62 | k.cache[key] = k.pages.Front() 63 | return p.Value 64 | } 65 | 66 | // Loader implements KeyStoreLoader 67 | type Loader struct { 68 | DB *MockDB 69 | } 70 | 71 | // Load gets the data from the database 72 | func (l *Loader) Load(key string) string { 73 | val, err := l.DB.Get(key) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | return val 79 | } 80 | 81 | func run(t *testing.T) (*KeyStoreCache, *MockDB) { 82 | loader := Loader{ 83 | DB: GetMockDB(), 84 | } 85 | cache := New(&loader) 86 | 87 | RunMockServer(cache, t) 88 | 89 | return cache, loader.DB 90 | } 91 | 92 | func main() { 93 | run(nil) 94 | } 95 | -------------------------------------------------------------------------------- /2-race-in-cache/mockdb.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | // MockDB used to simulate a database model 15 | type MockDB struct{ 16 | Calls int32 17 | } 18 | 19 | // Get only returns the key, as this is only for demonstration purposes 20 | func (db *MockDB) Get(key string) (string, error) { 21 | d, _ := time.ParseDuration("20ms") 22 | time.Sleep(d) 23 | atomic.AddInt32(&db.Calls, 1) 24 | return key, nil 25 | } 26 | 27 | // GetMockDB returns an instance of MockDB 28 | func GetMockDB() *MockDB { 29 | return &MockDB{ 30 | Calls: 0, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /2-race-in-cache/mockserver.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "strconv" 11 | "sync" 12 | "testing" 13 | ) 14 | 15 | const ( 16 | cycles = 15 17 | callsPerCycle = 100 18 | ) 19 | 20 | // RunMockServer simulates a running server, which accesses the 21 | // key-value database through our cache 22 | func RunMockServer(cache *KeyStoreCache, t *testing.T) { 23 | var wg sync.WaitGroup 24 | 25 | for c := 0; c < cycles; c++ { 26 | wg.Add(1) 27 | go func() { 28 | for i := 0; i < callsPerCycle; i++ { 29 | 30 | wg.Add(1) 31 | go func(i int) { 32 | value := cache.Get("Test" + strconv.Itoa(i)) 33 | if t != nil { 34 | if value != "Test" + strconv.Itoa(i) { 35 | t.Errorf("Incorrect db response %v", value) 36 | } 37 | } 38 | wg.Done() 39 | }(i) 40 | 41 | } 42 | wg.Done() 43 | }() 44 | } 45 | 46 | wg.Wait() 47 | } 48 | -------------------------------------------------------------------------------- /3-limit-service-time/README.md: -------------------------------------------------------------------------------- 1 | # Limit Service Time for Free-tier Users 2 | 3 | Your video processing service has a freemium model. Everyone has 10 4 | sec of free processing time on your service. After that, the 5 | service will kill your process, unless you are a paid premium user. 6 | 7 | Beginner Level: 10s max per request 8 | Advanced Level: 10s max per user (accumulated) 9 | 10 | -------------------------------------------------------------------------------- /3-limit-service-time/main.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // Your video processing service has a freemium model. Everyone has 10 4 | // sec of free processing time on your service. After that, the 5 | // service will kill your process, unless you are a paid premium user. 6 | // 7 | // Beginner Level: 10s max per request 8 | // Advanced Level: 10s max per user (accumulated) 9 | // 10 | 11 | package main 12 | 13 | // User defines the UserModel. Use this to check whether a User is a 14 | // Premium user or not 15 | type User struct { 16 | ID int 17 | IsPremium bool 18 | TimeUsed int64 // in seconds 19 | } 20 | 21 | // HandleRequest runs the processes requested by users. Returns false 22 | // if process had to be killed 23 | func HandleRequest(process func(), u *User) bool { 24 | process() 25 | return true 26 | } 27 | 28 | func main() { 29 | RunMockServer() 30 | } 31 | -------------------------------------------------------------------------------- /3-limit-service-time/mockserver.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var wg sync.WaitGroup 16 | 17 | // RunMockServer pretends to be a video processing service. It 18 | // simulates user interacting with the Server. 19 | func RunMockServer() { 20 | u1 := User{ID: 0, IsPremium: false} 21 | u2 := User{ID: 1, IsPremium: true} 22 | 23 | wg.Add(5) 24 | 25 | go createMockRequest(1, shortProcess, &u1) 26 | time.Sleep(1 * time.Second) 27 | 28 | go createMockRequest(2, longProcess, &u2) 29 | time.Sleep(2 * time.Second) 30 | 31 | go createMockRequest(3, shortProcess, &u1) 32 | time.Sleep(1 * time.Second) 33 | 34 | go createMockRequest(4, longProcess, &u1) 35 | go createMockRequest(5, shortProcess, &u2) 36 | 37 | wg.Wait() 38 | } 39 | 40 | func createMockRequest(pid int, fn func(), u *User) { 41 | fmt.Println("UserID:", u.ID, "\tProcess", pid, "started.") 42 | res := HandleRequest(fn, u) 43 | 44 | if res { 45 | fmt.Println("UserID:", u.ID, "\tProcess", pid, "done.") 46 | } else { 47 | fmt.Println("UserID:", u.ID, "\tProcess", pid, "killed. (No quota left)") 48 | } 49 | 50 | wg.Done() 51 | } 52 | 53 | func shortProcess() { 54 | time.Sleep(6 * time.Second) 55 | } 56 | 57 | func longProcess() { 58 | time.Sleep(11 * time.Second) 59 | } 60 | -------------------------------------------------------------------------------- /4-graceful-sigint/README.md: -------------------------------------------------------------------------------- 1 | # Graceful SIGINT killing 2 | 3 | Given is a mock process which runs indefinitely and blocks the program. Right now the only way to stop the program is to send a SIGINT (Ctrl-C). Killing a process like that is not graceful, so we 4 | want to try to gracefully stop the process first. 5 | 6 | Change the program to do the following: 7 | 1. On SIGINT try to gracefully stop the process using 8 | `proc.Stop()` 9 | 2. If SIGINT is called again, just kill the program (last resort) -------------------------------------------------------------------------------- /4-graceful-sigint/main.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // Given is a mock process which runs indefinitely and blocks the 4 | // program. Right now the only way to stop the program is to send a 5 | // SIGINT (Ctrl-C). Killing a process like that is not graceful, so we 6 | // want to try to gracefully stop the process first. 7 | // 8 | // Change the program to do the following: 9 | // 1. On SIGINT try to gracefully stop the process using 10 | // `proc.Stop()` 11 | // 2. If SIGINT is called again, just kill the program (last resort) 12 | // 13 | 14 | package main 15 | 16 | func main() { 17 | // Create a process 18 | proc := MockProcess{} 19 | 20 | // Run the process (blocking) 21 | proc.Run() 22 | } 23 | -------------------------------------------------------------------------------- /4-graceful-sigint/mockprocess.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | // MockProcess for example 17 | type MockProcess struct { 18 | mu sync.Mutex 19 | isRunning bool 20 | } 21 | 22 | // Run will start the process 23 | func (m *MockProcess) Run() { 24 | m.mu.Lock() 25 | m.isRunning = true 26 | m.mu.Unlock() 27 | 28 | fmt.Print("Process running..") 29 | for { 30 | fmt.Print(".") 31 | time.Sleep(1 * time.Second) 32 | } 33 | } 34 | 35 | // Stop tries to gracefully stop the process, in this mock example 36 | // this will not succeed 37 | func (m *MockProcess) Stop() { 38 | m.mu.Lock() 39 | defer m.mu.Unlock() 40 | if !m.isRunning { 41 | log.Fatal("Cannot stop a process which is not running") 42 | } 43 | 44 | fmt.Print("\nStopping process..") 45 | for { 46 | fmt.Print(".") 47 | time.Sleep(1 * time.Second) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /5-session-cleaner/README.md: -------------------------------------------------------------------------------- 1 | # Clean Inactive Sessions to Prevent Memory Overflow 2 | 3 | Given is a SessionManager that stores session information in 4 | memory. The SessionManager itself is working, however, since we 5 | keep on adding new sessions to the manager our program will 6 | eventually run out of memory. 7 | 8 | Your task is to implement a session cleaner routine that runs 9 | concurrently in the background and cleans every session that 10 | hasn't been updated for more than 5 seconds (of course usually 11 | session times are much longer). 12 | 13 | Note that we expect the session to be removed anytime between 5 and 7 14 | seconds after the last update. Also, note that you have to be very 15 | careful in order to prevent race conditions. 16 | 17 | ## Test your solution 18 | 19 | To complete this exercise, you must pass two test. The normal `go 20 | test` test cases as well as the race condition test. 21 | 22 | Use the following commands to test your solution: 23 | ``` 24 | go test 25 | go test --race 26 | ``` -------------------------------------------------------------------------------- /5-session-cleaner/check_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestSessionManagersCreationAndUpdate(t *testing.T) { 9 | // Create manager and new session 10 | m := NewSessionManager() 11 | sID, err := m.CreateSession() 12 | if err != nil { 13 | t.Error("Error CreateSession:", err) 14 | } 15 | 16 | data, err := m.GetSessionData(sID) 17 | if err != nil { 18 | t.Error("Error GetSessionData:", err) 19 | } 20 | 21 | // Modify and update data 22 | data["website"] = "longhoang.de" 23 | err = m.UpdateSessionData(sID, data) 24 | if err != nil { 25 | t.Error("Error UpdateSessionData:", err) 26 | } 27 | 28 | // Retrieve data from manager again 29 | data, err = m.GetSessionData(sID) 30 | if err != nil { 31 | t.Error("Error GetSessionData:", err) 32 | } 33 | 34 | if data["website"] != "longhoang.de" { 35 | t.Error("Expected website to be longhoang.de") 36 | } 37 | } 38 | 39 | func TestSessionManagersCleaner(t *testing.T) { 40 | m := NewSessionManager() 41 | sID, err := m.CreateSession() 42 | if err != nil { 43 | t.Error("Error CreateSession:", err) 44 | } 45 | 46 | // Note that the cleaner is only running every 5s 47 | time.Sleep(7 * time.Second) 48 | _, err = m.GetSessionData(sID) 49 | if err != ErrSessionNotFound { 50 | t.Error("Session still in memory after 7 seconds") 51 | } 52 | } 53 | 54 | func TestSessionManagersCleanerAfterUpdate(t *testing.T) { 55 | m := NewSessionManager() 56 | sID, err := m.CreateSession() 57 | if err != nil { 58 | t.Error("Error CreateSession:", err) 59 | } 60 | 61 | time.Sleep(3 * time.Second) 62 | 63 | err = m.UpdateSessionData(sID, make(map[string]interface{})) 64 | if err != nil { 65 | t.Error("Error UpdateSessionData:", err) 66 | } 67 | 68 | time.Sleep(3 * time.Second) 69 | 70 | _, err = m.GetSessionData(sID) 71 | if err == ErrSessionNotFound { 72 | t.Error("Session not found although has been updated 3 seconds earlier.") 73 | } 74 | 75 | time.Sleep(4 * time.Second) 76 | _, err = m.GetSessionData(sID) 77 | if err != ErrSessionNotFound { 78 | t.Error("Session still in memory 7 seconds after update") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /5-session-cleaner/helper.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // DO NOT EDIT THIS PART 4 | // Your task is to edit `main.go` 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "crypto/rand" 11 | "encoding/base64" 12 | "io" 13 | ) 14 | 15 | // MakeSessionID is used to generate a random dummy sessionID 16 | func MakeSessionID() (string, error) { 17 | buf := make([]byte, 26) 18 | _, err := io.ReadFull(rand.Reader, buf) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | return base64.StdEncoding.EncodeToString(buf), nil 24 | } 25 | -------------------------------------------------------------------------------- /5-session-cleaner/main.go: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////// 2 | // 3 | // Given is a SessionManager that stores session information in 4 | // memory. The SessionManager itself is working, however, since we 5 | // keep on adding new sessions to the manager our program will 6 | // eventually run out of memory. 7 | // 8 | // Your task is to implement a session cleaner routine that runs 9 | // concurrently in the background and cleans every session that 10 | // hasn't been updated for more than 5 seconds (of course usually 11 | // session times are much longer). 12 | // 13 | // Note that we expect the session to be removed anytime between 5 and 14 | // 7 seconds after the last update. Also, note that you have to be 15 | // very careful in order to prevent race conditions. 16 | // 17 | 18 | package main 19 | 20 | import ( 21 | "errors" 22 | "log" 23 | ) 24 | 25 | // SessionManager keeps track of all sessions from creation, updating 26 | // to destroying. 27 | type SessionManager struct { 28 | sessions map[string]Session 29 | } 30 | 31 | // Session stores the session's data 32 | type Session struct { 33 | Data map[string]interface{} 34 | } 35 | 36 | // NewSessionManager creates a new sessionManager 37 | func NewSessionManager() *SessionManager { 38 | m := &SessionManager{ 39 | sessions: make(map[string]Session), 40 | } 41 | 42 | return m 43 | } 44 | 45 | // CreateSession creates a new session and returns the sessionID 46 | func (m *SessionManager) CreateSession() (string, error) { 47 | sessionID, err := MakeSessionID() 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | m.sessions[sessionID] = Session{ 53 | Data: make(map[string]interface{}), 54 | } 55 | 56 | return sessionID, nil 57 | } 58 | 59 | // ErrSessionNotFound returned when sessionID not listed in 60 | // SessionManager 61 | var ErrSessionNotFound = errors.New("SessionID does not exists") 62 | 63 | // GetSessionData returns data related to session if sessionID is 64 | // found, errors otherwise 65 | func (m *SessionManager) GetSessionData(sessionID string) (map[string]interface{}, error) { 66 | session, ok := m.sessions[sessionID] 67 | if !ok { 68 | return nil, ErrSessionNotFound 69 | } 70 | return session.Data, nil 71 | } 72 | 73 | // UpdateSessionData overwrites the old session data with the new one 74 | func (m *SessionManager) UpdateSessionData(sessionID string, data map[string]interface{}) error { 75 | _, ok := m.sessions[sessionID] 76 | if !ok { 77 | return ErrSessionNotFound 78 | } 79 | 80 | // Hint: you should renew expiry of the session here 81 | m.sessions[sessionID] = Session{ 82 | Data: data, 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func main() { 89 | // Create new sessionManager and new session 90 | m := NewSessionManager() 91 | sID, err := m.CreateSession() 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | log.Println("Created new session with ID", sID) 97 | 98 | // Update session data 99 | data := make(map[string]interface{}) 100 | data["website"] = "longhoang.de" 101 | 102 | err = m.UpdateSessionData(sID, data) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | log.Println("Update session data, set website to longhoang.de") 108 | 109 | // Retrieve data from manager again 110 | updatedData, err := m.GetSessionData(sID) 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | 115 | log.Println("Get session data:", updatedData) 116 | } 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyleft from 2017 Long Hoang 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Concurrency Exercises [![Build Status](https://travis-ci.org/loong/go-concurrency-exercises.svg?branch=main)](https://travis-ci.org/loong/go-concurrency-exercises) [![Go Report Card](https://goreportcard.com/badge/github.com/loong/go-concurrency-exercises)](https://goreportcard.com/report/github.com/loong/go-concurrency-exercises) 2 | Exercises for Golang's concurrency patterns. 3 | 4 | ## Why 5 | The Go community has plenty resources to read about go's concurrency model and how to use it effectively. But *who actually wants to read all this*!? This repo tries to teach concurrency patterns by following the 'learning by doing' approach. 6 | 7 | ![Image of excited gopher](https://golang.org/doc/gopher/pkg.png) 8 | 9 | ## How to take this challenge 10 | 1. *Only edit `main.go`* to solve the problem. Do not touch any of the other files. 11 | 2. If you find a `*_test.go` file, you can test the correctness of your solution with `go test` 12 | 3. If you get stuck, join us on [Discord](https://discord.com/invite/golang) or [Slack](https://invite.slack.golangbridge.org/)! Surely there are people who are happy to give you some code reviews (if not, find me via `@loong` ;) ) 13 | 14 | ## Overview 15 | | # | Name of the Challenge + URL | 16 | | - |:-------------| 17 | | 0 | [Limit your Crawler](https://github.com/loong/go-concurrency-exercises/tree/main/0-limit-crawler) | 18 | | 1 | [Producer-Consumer](https://github.com/loong/go-concurrency-exercises/tree/main/1-producer-consumer) | 19 | | 2 | [Race Condition in Caching Cache](https://github.com/loong/go-concurrency-exercises/tree/main/2-race-in-cache#race-condition-in-caching-szenario) | 20 | | 3 | [Limit Service Time for Free-tier Users](https://github.com/loong/go-concurrency-exercises/tree/main/3-limit-service-time) | 21 | | 4 | [Graceful SIGINT Killing](https://github.com/loong/go-concurrency-exercises/tree/main/4-graceful-sigint) | 22 | | 5 | [Clean Inactive Sessions to Prevent Memory Overflow](https://github.com/loong/go-concurrency-exercises/tree/main/5-session-cleaner) | 23 | 24 | ## License 25 | 26 | ``` 27 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 28 | Version 2, December 2004 29 | 30 | Copyleft from 2017 Long Hoang 31 | 32 | Everyone is permitted to copy and distribute verbatim or modified 33 | copies of this license document, and changing it is allowed as long 34 | as the name is changed. 35 | 36 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 37 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 38 | 39 | 0. You just DO WHAT THE FUCK YOU WANT TO. 40 | ``` 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/loong/go-concurrency-exercises 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loong/go-concurrency-exercises/3b1484ee7d295b3644aa24137ddbcaac4abf7522/go.sum --------------------------------------------------------------------------------