Added exercises source code
parent
f082616475
commit
230de24e7c
@ -0,0 +1,33 @@
|
||||
# Limit your crawler
|
||||
|
||||
Given is a crawler (modified from the Go tour) that requests pages
|
||||
excessively. However, we don't want to burden the webserver too
|
||||
much. Your task is to change the code to limit the crawler to at most
|
||||
one page per second, while maintaining concurrency (in other words,
|
||||
Crawl() must be called concurrently)
|
||||
|
||||
## Hint
|
||||
|
||||
This exercise can be solved in 3 lines only. If you can't do
|
||||
it, have a look at this:
|
||||
https://github.com/golang/go/wiki/RateLimiting
|
||||
|
||||
## Test your solution
|
||||
|
||||
Use `go test` to verify if your solution is correct.
|
||||
|
||||
Correct solution:
|
||||
```
|
||||
PASS
|
||||
ok github.com/loong/go-concurrency-exercises/0-limit-crawler 13.009s
|
||||
```
|
||||
|
||||
Incorrect solution:
|
||||
```
|
||||
--- FAIL: TestMain (7.80s)
|
||||
main_test.go:18: There exists a two crawls who were executed less than 1 sec apart.
|
||||
main_test.go:19: Solution is incorrect.
|
||||
FAIL
|
||||
exit status 1
|
||||
FAIL github.com/loong/go-concurrency-exercises/0-limit-crawler 7.808s
|
||||
```
|
||||
@ -0,0 +1,34 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
fetchSig := fetchSignalInstance()
|
||||
|
||||
start := time.Unix(0, 0)
|
||||
go func(start time.Time) {
|
||||
for {
|
||||
switch {
|
||||
case <-fetchSig:
|
||||
// Check if signal arrived earlier than a second (with error margin)
|
||||
if time.Now().Sub(start).Nanoseconds() < 950000000 {
|
||||
t.Log("There exists a two crawls that were executed less than 1 second apart.")
|
||||
t.Log("Solution is incorrect.")
|
||||
t.FailNow()
|
||||
}
|
||||
start = time.Now()
|
||||
}
|
||||
}
|
||||
}(start)
|
||||
|
||||
main()
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Your task is to change the code to limit the crawler to at most one
|
||||
// page per second, while maintaining concurrency (in other words,
|
||||
// Crawl() must be called concurrently)
|
||||
//
|
||||
// @hint: you can achieve this by adding 3 lines
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
// Crawl uses `fetcher` from the `mockfetcher.go` file to imitate a
|
||||
// real crawler. It crawls until the maximum depth has reached.
|
||||
func Crawl(url string, depth int, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
if depth <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
body, urls, err := fetcher.Fetch(url)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("found: %s %q\n", url, body)
|
||||
|
||||
wg.Add(len(urls))
|
||||
for _, u := range urls {
|
||||
// Do not remove the `go` keyword, as Crawl() must be
|
||||
// called concurrently
|
||||
go Crawl(u, depth-1, wg)
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
Crawl("http://golang.org/", 4, &wg)
|
||||
wg.Wait()
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MockFetcher is Fetcher that returns canned results. Taken from
|
||||
// https://tour.golang.org/concurrency/10
|
||||
type MockFetcher map[string]*mockResult
|
||||
|
||||
type mockResult struct {
|
||||
body string
|
||||
urls []string
|
||||
}
|
||||
|
||||
// Fetch pretends to retrieve the URLs and its subpages
|
||||
func (f MockFetcher) Fetch(url string) (string, []string, error) {
|
||||
fetchSignalInstance() <- true
|
||||
if res, ok := f[url]; ok {
|
||||
return res.body, res.urls, nil
|
||||
}
|
||||
return "", nil, fmt.Errorf("not found: %s", url)
|
||||
}
|
||||
|
||||
// fetcher is a populated MockFetcher.
|
||||
var fetcher = MockFetcher{
|
||||
"http://golang.org/": &mockResult{
|
||||
"The Go Programming Language",
|
||||
[]string{
|
||||
"http://golang.org/pkg/",
|
||||
"http://golang.org/cmd/",
|
||||
},
|
||||
},
|
||||
"http://golang.org/pkg/": &mockResult{
|
||||
"Packages",
|
||||
[]string{
|
||||
"http://golang.org/",
|
||||
"http://golang.org/cmd/",
|
||||
"http://golang.org/pkg/fmt/",
|
||||
"http://golang.org/pkg/os/",
|
||||
},
|
||||
},
|
||||
"http://golang.org/pkg/fmt/": &mockResult{
|
||||
"Package fmt",
|
||||
[]string{
|
||||
"http://golang.org/",
|
||||
"http://golang.org/pkg/",
|
||||
},
|
||||
},
|
||||
"http://golang.org/pkg/os/": &mockResult{
|
||||
"Package os",
|
||||
[]string{
|
||||
"http://golang.org/",
|
||||
"http://golang.org/pkg/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Code below is mainly used to test whether a solution is correct or
|
||||
// not
|
||||
|
||||
// fetchSignal is used to test whether the solution is correct
|
||||
var fetchSignal chan bool
|
||||
|
||||
// fetchSignalInstance is a singleton to access fetchSignal
|
||||
func fetchSignalInstance() chan bool {
|
||||
if fetchSignal == nil {
|
||||
// Use buffered channel to avoid blocking
|
||||
fetchSignal = make(chan bool, 1000)
|
||||
}
|
||||
return fetchSignal
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
# Producer-Consumer Scenario
|
||||
|
||||
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.
|
||||
|
||||
## Expected results:
|
||||
Before:
|
||||
```
|
||||
davecheney tweets about golang
|
||||
beertocode does not tweet about golang
|
||||
ironzeb tweets about golang
|
||||
beertocode tweets about golang
|
||||
vampirewalk666 tweets about golang
|
||||
Process took 3.580866005s
|
||||
```
|
||||
|
||||
After:
|
||||
```
|
||||
davecheney tweets about golang
|
||||
beertocode does not tweet about golang
|
||||
ironzeb tweets about golang
|
||||
beertocode tweets about golang
|
||||
vampirewalk666 tweets about golang
|
||||
Process took 1.977756255s
|
||||
```
|
||||
@ -0,0 +1,48 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Given is a producer-consumer scenario, where a producer reads in
|
||||
// tweets from a mockstream and a consumer is processing the
|
||||
// data. Your task is to change the code so that the producer as well
|
||||
// as the consumer can run concurrently
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func producer(stream Stream) (tweets []*Tweet) {
|
||||
for {
|
||||
tweet, err := stream.Next()
|
||||
if err == ErrEOF {
|
||||
return tweets
|
||||
}
|
||||
|
||||
tweets = append(tweets, tweet)
|
||||
}
|
||||
}
|
||||
|
||||
func consumer(tweets []*Tweet) {
|
||||
for _, t := range tweets {
|
||||
if t.IsTalkingAboutGo() {
|
||||
fmt.Println(t.Username, "\ttweets about golang")
|
||||
} else {
|
||||
fmt.Println(t.Username, "\tdoes not tweet about golang")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
start := time.Now()
|
||||
stream := GetMockStream()
|
||||
|
||||
// Producer
|
||||
tweets := producer(stream)
|
||||
|
||||
// Consumer
|
||||
consumer(tweets)
|
||||
|
||||
fmt.Printf("Process took %s\n", time.Since(start))
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetMockStream is a blackbox function which returns a mock stream for
|
||||
// demonstration purposes
|
||||
func GetMockStream() Stream {
|
||||
return Stream{0, mockdata}
|
||||
}
|
||||
|
||||
// Stream is a mock stream for demonstration purposes, not threadsafe
|
||||
type Stream struct {
|
||||
pos int
|
||||
tweets []Tweet
|
||||
}
|
||||
|
||||
// ErrEOF returns on End of File error
|
||||
var ErrEOF = errors.New("End of File")
|
||||
|
||||
// Next returns the next Tweet in the stream, returns EOF error if
|
||||
// there are no more tweets
|
||||
func (s *Stream) Next() (*Tweet, error) {
|
||||
|
||||
// simulate delay
|
||||
time.Sleep(320 * time.Millisecond)
|
||||
if s.pos >= len(s.tweets) {
|
||||
return &Tweet{}, ErrEOF
|
||||
}
|
||||
|
||||
tweet := s.tweets[s.pos]
|
||||
s.pos++
|
||||
|
||||
return &tweet, nil
|
||||
}
|
||||
|
||||
// Tweet defines the simlified representation of a tweet
|
||||
type Tweet struct {
|
||||
Username string
|
||||
Text string
|
||||
}
|
||||
|
||||
// IsTalkingAboutGo is a mock process which pretend to be a sophisticated procedure to analyse whether tweet is talking about go or not
|
||||
func (t *Tweet) IsTalkingAboutGo() bool {
|
||||
// simulate delay
|
||||
time.Sleep(330 * time.Millisecond)
|
||||
|
||||
hasGolang := strings.Contains(strings.ToLower(t.Text), "golang")
|
||||
hasGopher := strings.Contains(strings.ToLower(t.Text), "gopher")
|
||||
|
||||
return hasGolang || hasGopher
|
||||
}
|
||||
|
||||
var mockdata = []Tweet{
|
||||
{
|
||||
"davecheney",
|
||||
"#golang top tip: if your unit tests import any other package you wrote, including themselves, they're not unit tests.",
|
||||
}, {
|
||||
"beertocode",
|
||||
"Backend developer, doing frontend featuring the eternal struggle of centering something. #coding",
|
||||
}, {
|
||||
"ironzeb",
|
||||
"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",
|
||||
}, {
|
||||
"beertocode",
|
||||
"Looking forward to the #gopher meetup in Hsinchu tonight with @ironzeb!",
|
||||
}, {
|
||||
"vampirewalk666",
|
||||
"I just wrote a golang slack bot! It reports the state of github repository. #Slack #golang",
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
# 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 condition. Change the code to make this thread safe.
|
||||
|
||||
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
|
||||
|
||||
Use the following command to test for race conditions and correct functionality:
|
||||
```
|
||||
go test -race
|
||||
```
|
||||
|
||||
Correct solution:
|
||||
No output = solution correct:
|
||||
```
|
||||
$ go test -race
|
||||
$
|
||||
```
|
||||
|
||||
Incorrect solution:
|
||||
```
|
||||
==================
|
||||
WARNING: DATA RACE
|
||||
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)
|
||||
@ -0,0 +1,46 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
cache := run()
|
||||
|
||||
cacheLen := len(cache.cache)
|
||||
pagesLen := cache.pages.Len()
|
||||
if cacheLen != CacheSize {
|
||||
t.Errorf("Incorrect cache size %v", cacheLen)
|
||||
}
|
||||
if pagesLen != CacheSize {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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"
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type page struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// KeyStoreCache is a LRU cache for string key-value pairs
|
||||
type KeyStoreCache struct {
|
||||
cache map[string]*list.Element
|
||||
pages list.List
|
||||
load func(string) string
|
||||
}
|
||||
|
||||
// New creates a new KeyStoreCache
|
||||
func New(load KeyStoreCacheLoader) *KeyStoreCache {
|
||||
return &KeyStoreCache{
|
||||
load: load.Load,
|
||||
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 {
|
||||
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
|
||||
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.pages.PushFront(p)
|
||||
k.cache[key] = k.pages.Front()
|
||||
return p.Value
|
||||
}
|
||||
|
||||
// Loader implements KeyStoreLoader
|
||||
type Loader struct {
|
||||
DB *MockDB
|
||||
}
|
||||
|
||||
// Load gets the data from the database
|
||||
func (l *Loader) Load(key string) string {
|
||||
val, err := l.DB.Get(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func run() *KeyStoreCache {
|
||||
loader := Loader{
|
||||
DB: GetMockDB(),
|
||||
}
|
||||
cache := New(&loader)
|
||||
|
||||
RunMockServer(cache)
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func main() {
|
||||
run()
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetMockDB returns an instance of MockDB
|
||||
func GetMockDB() *MockDB {
|
||||
return &MockDB{}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
cycles = 15
|
||||
callsPerCycle = 100
|
||||
)
|
||||
|
||||
// RunMockServer simulates a running server, which accesses the
|
||||
// key-value database through our cache
|
||||
func RunMockServer(cache *KeyStoreCache) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for c := 0; c < cycles; c++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
for i := 0; i < callsPerCycle; i++ {
|
||||
|
||||
cache.Get("Test" + strconv.Itoa(i))
|
||||
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
# Limit Service Time for Free-tier Users
|
||||
|
||||
Your video processing service has a freemium model. Everyone has 10
|
||||
sec of free processing time on your service. After that, the
|
||||
service will kill your process, unless you are a paid premium user.
|
||||
|
||||
Beginner Level: 10s max per request
|
||||
Advanced Level: 10s max per user (accumulated)
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Your video processing service has a freemium model. Everyone has 10
|
||||
// sec of free processing time on your service. After that, the
|
||||
// service will kill your process, unless you are a paid premium user.
|
||||
//
|
||||
// Beginner Level: 10s max per request
|
||||
// Advanced Level: 10s max per user (accumulated)
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
// User defines the UserModel. Use this to check whether a User is a
|
||||
// Premium user or not
|
||||
type User struct {
|
||||
ID int
|
||||
IsPremium bool
|
||||
TimeUsed int64 // in seconds
|
||||
}
|
||||
|
||||
// HandleRequest runs the processes requested by users. Returns false
|
||||
// if process had to be killed
|
||||
func HandleRequest(process func(), u *User) bool {
|
||||
process()
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
RunMockServer()
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// RunMockServer pretends to be a video processing service. It
|
||||
// simulates user interacting with the Server.
|
||||
func RunMockServer() {
|
||||
u1 := User{ID: 0, IsPremium: false}
|
||||
u2 := User{ID: 1, IsPremium: true}
|
||||
|
||||
wg.Add(5)
|
||||
|
||||
go createMockRequest(1, shortProcess, &u1)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
go createMockRequest(2, longProcess, &u2)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
go createMockRequest(3, shortProcess, &u1)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
go createMockRequest(4, longProcess, &u1)
|
||||
go createMockRequest(5, shortProcess, &u2)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func createMockRequest(pid int, fn func(), u *User) {
|
||||
fmt.Println("UserID:", u.ID, "\tProcess", pid, "started.")
|
||||
res := HandleRequest(fn, u)
|
||||
|
||||
if res {
|
||||
fmt.Println("UserID:", u.ID, "\tProcess", pid, "done.")
|
||||
} else {
|
||||
fmt.Println("UserID:", u.ID, "\tProcess", pid, "killed. (No quota left)")
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
func shortProcess() {
|
||||
time.Sleep(6 * time.Second)
|
||||
}
|
||||
|
||||
func longProcess() {
|
||||
time.Sleep(11 * time.Second)
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
# Graceful SIGINT killing
|
||||
|
||||
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
|
||||
want to try to gracefully stop the process first.
|
||||
|
||||
Change the program to do the following:
|
||||
1. On SIGINT try to gracefully stop the process using
|
||||
`proc.Stop()`
|
||||
2. If SIGINT is called again, just kill the program (last resort)
|
||||
@ -0,0 +1,22 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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
|
||||
// want to try to gracefully stop the process first.
|
||||
//
|
||||
// Change the program to do the following:
|
||||
// 1. On SIGINT try to gracefully stop the process using
|
||||
// `proc.Stop()`
|
||||
// 2. If SIGINT is called again, just kill the program (last resort)
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// Create a process
|
||||
proc := MockProcess{}
|
||||
|
||||
// Run the process (blocking)
|
||||
proc.Run()
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockProcess for example
|
||||
type MockProcess struct {
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
// Run will start the process
|
||||
func (m *MockProcess) Run() {
|
||||
m.isRunning = true
|
||||
|
||||
fmt.Print("Process running..")
|
||||
for {
|
||||
fmt.Print(".")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop tries to gracefully stop the process, in this mock example
|
||||
// this will not succeed
|
||||
func (m *MockProcess) Stop() {
|
||||
if !m.isRunning {
|
||||
log.Fatal("Cannot stop a process which is not running")
|
||||
}
|
||||
|
||||
fmt.Print("\nStopping process..")
|
||||
for {
|
||||
fmt.Print(".")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,26 @@
|
||||
# Clean Inactive Sessions to Prevent Memory Overflow
|
||||
|
||||
Given is a SessionManager that stores session information in
|
||||
memory. The SessionManager itself is working, however, since we
|
||||
keep on adding new sessions to the manager our program will
|
||||
eventually run out of memory.
|
||||
|
||||
Your task is to implement a session cleaner routine that runs
|
||||
concurrently in the background and cleans every session that
|
||||
hasn't been updated for more than 5 seconds (of course usually
|
||||
session times are much longer).
|
||||
|
||||
Note that we expect the session to be removed anytime between 5 and 7
|
||||
seconds after the last update. Also, note that you have to be very
|
||||
careful in order to prevent race conditions.
|
||||
|
||||
## Test your solution
|
||||
|
||||
To complete this exercise, you must pass two test. The normal `go
|
||||
test` test cases as well as the race condition test.
|
||||
|
||||
Use the following commands to test your solution:
|
||||
```
|
||||
go test
|
||||
go test --race
|
||||
```
|
||||
@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSessionManagersCreationAndUpdate(t *testing.T) {
|
||||
// Create manager and new session
|
||||
m := NewSessionManager()
|
||||
sID, err := m.CreateSession()
|
||||
if err != nil {
|
||||
t.Error("Error CreateSession:", err)
|
||||
}
|
||||
|
||||
data, err := m.GetSessionData(sID)
|
||||
if err != nil {
|
||||
t.Error("Error GetSessionData:", err)
|
||||
}
|
||||
|
||||
// Modify and update data
|
||||
data["website"] = "longhoang.de"
|
||||
err = m.UpdateSessionData(sID, data)
|
||||
if err != nil {
|
||||
t.Error("Error UpdateSessionData:", err)
|
||||
}
|
||||
|
||||
// Retrieve data from manager again
|
||||
data, err = m.GetSessionData(sID)
|
||||
if err != nil {
|
||||
t.Error("Error GetSessionData:", err)
|
||||
}
|
||||
|
||||
if data["website"] != "longhoang.de" {
|
||||
t.Error("Expected website to be longhoang.de")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionManagersCleaner(t *testing.T) {
|
||||
m := NewSessionManager()
|
||||
sID, err := m.CreateSession()
|
||||
if err != nil {
|
||||
t.Error("Error CreateSession:", err)
|
||||
}
|
||||
|
||||
// Note that the cleaner is only running every 5s
|
||||
time.Sleep(7 * time.Second)
|
||||
_, err = m.GetSessionData(sID)
|
||||
if err != ErrSessionNotFound {
|
||||
t.Error("Session still in memory after 7 seconds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionManagersCleanerAfterUpdate(t *testing.T) {
|
||||
m := NewSessionManager()
|
||||
sID, err := m.CreateSession()
|
||||
if err != nil {
|
||||
t.Error("Error CreateSession:", err)
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
err = m.UpdateSessionData(sID, make(map[string]interface{}))
|
||||
if err != nil {
|
||||
t.Error("Error UpdateSessionData:", err)
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
_, err = m.GetSessionData(sID)
|
||||
if err == ErrSessionNotFound {
|
||||
t.Error("Session not found although has been updated 3 seconds earlier.")
|
||||
}
|
||||
|
||||
time.Sleep(4 * time.Second)
|
||||
_, err = m.GetSessionData(sID)
|
||||
if err != ErrSessionNotFound {
|
||||
t.Error("Session still in memory 7 seconds after update")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DO NOT EDIT THIS PART
|
||||
// Your task is to edit `main.go`
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
)
|
||||
|
||||
// MakeSessionID is used to generate a random dummy sessionID
|
||||
func MakeSessionID() (string, error) {
|
||||
buf := make([]byte, 26)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf), nil
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Given is a SessionManager that stores session information in
|
||||
// memory. The SessionManager itself is working, however, since we
|
||||
// keep on adding new sessions to the manager our program will
|
||||
// eventually run out of memory.
|
||||
//
|
||||
// Your task is to implement a session cleaner routine that runs
|
||||
// concurrently in the background and cleans every session that
|
||||
// hasn't been updated for more than 5 seconds (of course usually
|
||||
// session times are much longer).
|
||||
//
|
||||
// Note that we expect the session to be removed anytime between 5 and
|
||||
// 7 seconds after the last update. Also, note that you have to be
|
||||
// very careful in order to prevent race conditions.
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
// SessionManager keeps track of all sessions from creation, updating
|
||||
// to destroying.
|
||||
type SessionManager struct {
|
||||
sessions map[string]Session
|
||||
}
|
||||
|
||||
// Session stores the session's data
|
||||
type Session struct {
|
||||
Data map[string]interface{}
|
||||
}
|
||||
|
||||
// NewSessionManager creates a new sessionManager
|
||||
func NewSessionManager() *SessionManager {
|
||||
m := &SessionManager{
|
||||
sessions: make(map[string]Session),
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// CreateSession creates a new session and returns the sessionID
|
||||
func (m *SessionManager) CreateSession() (string, error) {
|
||||
sessionID, err := MakeSessionID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
m.sessions[sessionID] = Session{
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// ErrSessionNotFound returned when sessionID not listed in
|
||||
// SessionManager
|
||||
var ErrSessionNotFound = errors.New("SessionID does not exists")
|
||||
|
||||
// GetSessionData returns data related to session if sessionID is
|
||||
// found, errors otherwise
|
||||
func (m *SessionManager) GetSessionData(sessionID string) (map[string]interface{}, error) {
|
||||
session, ok := m.sessions[sessionID]
|
||||
if !ok {
|
||||
return nil, ErrSessionNotFound
|
||||
}
|
||||
return session.Data, nil
|
||||
}
|
||||
|
||||
// UpdateSessionData overwrites the old session data with the new one
|
||||
func (m *SessionManager) UpdateSessionData(sessionID string, data map[string]interface{}) error {
|
||||
_, ok := m.sessions[sessionID]
|
||||
if !ok {
|
||||
return ErrSessionNotFound
|
||||
}
|
||||
|
||||
// Hint: you should renew expiry of the session here
|
||||
m.sessions[sessionID] = Session{
|
||||
Data: data,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create new sessionManager and new session
|
||||
m := NewSessionManager()
|
||||
sID, err := m.CreateSession()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Created new session with ID", sID)
|
||||
|
||||
// Update session data
|
||||
data := make(map[string]interface{})
|
||||
data["website"] = "longhoang.de"
|
||||
|
||||
err = m.UpdateSessionData(sID, data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Update session data, set website to longhoang.de")
|
||||
|
||||
// Retrieve data from manager again
|
||||
updatedData, err := m.GetSessionData(sID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Get session data:", updatedData)
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
MIT License
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Copyleft from 2017 Long Hoang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
|
||||
@ -1,5 +1,42 @@
|
||||
# go-concurrency-exercises
|
||||
|
||||
Go concurrency exercises.
|
||||
# Go Concurrency Exercises [](https://travis-ci.org/loong/go-concurrency-exercises) [](https://goreportcard.com/report/github.com/loong/go-concurrency-exercises)
|
||||
Exercises for Golang's concurrency patterns.
|
||||
|
||||
https://github.com/loong/go-concurrency-exercises
|
||||
|
||||
## Why
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## How to take this challenge
|
||||
1. *Only edit `main.go`* to solve the problem. Do not touch any of the other files.
|
||||
2. If you find a `*_test.go` file, you can test the correctness of your solution with `go test`
|
||||
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` ;) )
|
||||
|
||||
## Overview
|
||||
| # | Name of the Challenge + URL |
|
||||
| - |:-------------|
|
||||
| 0 | [Limit your Crawler](https://github.com/loong/go-concurrency-exercises/tree/main/0-limit-crawler) |
|
||||
| 1 | [Producer-Consumer](https://github.com/loong/go-concurrency-exercises/tree/main/1-producer-consumer) |
|
||||
| 2 | [Race Condition in Caching Cache](https://github.com/loong/go-concurrency-exercises/tree/main/2-race-in-cache#race-condition-in-caching-szenario) |
|
||||
| 3 | [Limit Service Time for Free-tier Users](https://github.com/loong/go-concurrency-exercises/tree/main/3-limit-service-time) |
|
||||
| 4 | [Graceful SIGINT Killing](https://github.com/loong/go-concurrency-exercises/tree/main/4-graceful-sigint) |
|
||||
| 5 | [Clean Inactive Sessions to Prevent Memory Overflow](https://github.com/loong/go-concurrency-exercises/tree/main/5-session-cleaner) |
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyleft from 2017 Long Hoang
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
```
|
||||
|
||||
Loading…
Reference in New Issue