From d0dac7cfe233cabaefb04f8c4ed2951f7674f2a3 Mon Sep 17 00:00:00 2001 From: spartan-vutran Date: Sun, 1 Jun 2025 22:37:06 +0700 Subject: [PATCH 1/7] chore: rebase --- .idea/.gitignore | 8 ++ .idea/Go.iml | 9 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 ++ concurrency/cancellation/README.md | 7 ++ concurrency/cancellation/cancellation.go | 3 + concurrency/cancellation/db.go | 27 ++++++ concurrency/cancellation/driver.go | 41 ++++++++ concurrency/cancellation/query_operation.go | 102 ++++++++++++++++++++ concurrency/cancellation/rows.go | 45 +++++++++ go.mod | 4 +- main.go | 7 ++ subset.go | 27 ++++++ 13 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/Go.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 concurrency/cancellation/README.md create mode 100644 concurrency/cancellation/cancellation.go create mode 100644 concurrency/cancellation/db.go create mode 100644 concurrency/cancellation/driver.go create mode 100644 concurrency/cancellation/query_operation.go create mode 100644 concurrency/cancellation/rows.go create mode 100644 main.go create mode 100644 subset.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Go.iml b/.idea/Go.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/Go.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..39d35e2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/concurrency/cancellation/README.md b/concurrency/cancellation/README.md new file mode 100644 index 0000000..c1469a4 --- /dev/null +++ b/concurrency/cancellation/README.md @@ -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. \ No newline at end of file diff --git a/concurrency/cancellation/cancellation.go b/concurrency/cancellation/cancellation.go new file mode 100644 index 0000000..a3f0ec4 --- /dev/null +++ b/concurrency/cancellation/cancellation.go @@ -0,0 +1,3 @@ +package cancellation + +// diff --git a/concurrency/cancellation/db.go b/concurrency/cancellation/db.go new file mode 100644 index 0000000..7146c78 --- /dev/null +++ b/concurrency/cancellation/db.go @@ -0,0 +1,27 @@ +package cancellation + +import ( + "context" + "fmt" + "log" +) + +// ----------------------------------------------------------------------------- +// Your db instance (that uses the EmulatedDriver) +// ----------------------------------------------------------------------------- + +// YourDB is your custom database instance that would use the EmulatedDriver. +type YourDB struct { + driver EmulatedDriver +} + +// NewYourDB creates a new instance of YourDB with the provided driver. +func NewYourDB(driver EmulatedDriver) *YourDB { + return &YourDB{driver: driver} +} + +// QueryContext is your implementation of the database query method that +// supports context cancellation. +func (db *YourDB) QueryContext(ctx context.Context, query string, args ...interface{}) (*simulatedRows, error) { + +} diff --git a/concurrency/cancellation/driver.go b/concurrency/cancellation/driver.go new file mode 100644 index 0000000..94850e7 --- /dev/null +++ b/concurrency/cancellation/driver.go @@ -0,0 +1,41 @@ +package cancellation + +import ( + "context" + "log" +) + +// EmulatedDriver is the interface that your 'db' instance would use to interact with +// the underlying database driver. +type EmulatedDriver interface { + // PrepareQuery initiates a query and returns a handle to the ongoing operation. + // It does NOT block until the query completes. + PrepareQuery(ctx context.Context, query string, args ...interface{}) (QueryOperation, error) +} + +// ----------------------------------------------------------------------------- +// Mock Implementation of the EmulatedDriver and QueryOperation +// ----------------------------------------------------------------------------- + +// mockEmulatedDriver is a concrete implementation of EmulatedDriver for testing. +type mockEmulatedDriver struct { + // You might add a connection pool or other driver-level state here +} + +// NewMockEmulatedDriver creates a new instance of the mock driver. +func NewMockEmulatedDriver() EmulatedDriver { + return &mockEmulatedDriver{} +} + +// PrepareQuery simulates preparing and starting a database query. +func (m *mockEmulatedDriver) PrepareQuery(ctx context.Context, query string, args ...interface{}) (QueryOperation, error) { + log.Printf("Mock Driver: Preparing and starting query: '%s'", query) + op := &mockQueryOperation{ + query: query, + finished: make(chan struct{}), + cancelSignal: make(chan struct{}, 1), // Buffered channel for non-blocking sends + } + + go op.run(ctx) // Start the "query" in a goroutine + return op, nil +} diff --git a/concurrency/cancellation/query_operation.go b/concurrency/cancellation/query_operation.go new file mode 100644 index 0000000..377645c --- /dev/null +++ b/concurrency/cancellation/query_operation.go @@ -0,0 +1,102 @@ +package cancellation + +import ( + "context" + "errors" + "log" + "sync" + "time" +) + +// QueryOperation represents an ongoing database query. +// It allows for waiting on the query's completion and explicitly canceling it. +type QueryOperation interface { + // Wait blocks until the query completes successfully or with an error. + // It returns the results (e.g., *simulatedRows) and any error. + Wait() (*simulatedRows, error) + + // Cancel attempts to interrupt the ongoing query. + // This method should be safe to call multiple times or if the query has already finished. + Cancel() error +} + +// mockQueryOperation is a concrete implementation of QueryOperation for testing. +type mockQueryOperation struct { + query string + result *simulatedRows + opErr error + finished chan struct{} // Closed when the operation completes (successfully or with error) + cancelSignal chan struct{} // Used to signal cancellation to the running operation goroutine + mu sync.Mutex // Protects access to result and opErr + canceled bool +} + +// run simulates the actual database query execution. +func (op *mockQueryOperation) run(ctx context.Context) { + defer close(op.finished) // Ensure 'finished' is always closed + + // Simulate query execution time + queryDuration := 3 * time.Second // Default query duration + if op.query == "FAST QUERY" { + queryDuration = 500 * time.Millisecond // A faster query + } + + log.Printf("Mock QueryOperation: Starting execution for '%s' (will take %v)", op.query, queryDuration) + + select { + case <-time.After(queryDuration): + // Query completed successfully + op.mu.Lock() + op.result = &simulatedRows{data: []string{"data_for_" + op.query}} + op.opErr = nil + op.mu.Unlock() + log.Printf("Mock QueryOperation: Query '%s' completed successfully.", op.query) + case <-op.cancelSignal: + // Cancellation requested by the caller + op.mu.Lock() + op.opErr = context.Canceled // Or a custom driver-specific cancellation error + op.canceled = true + op.mu.Unlock() + log.Printf("Mock QueryOperation: Query '%s' was explicitly canceled by the caller.", op.query) + case <-ctx.Done(): + // Context itself was canceled (e.g., timeout, parent context cancel) + op.mu.Lock() + op.opErr = ctx.Err() // This will be context.Canceled or context.DeadlineExceeded + op.canceled = true + op.mu.Unlock() + log.Printf("Mock QueryOperation: Query '%s' interrupted due to context cancellation: %v", op.query, ctx.Err()) + } +} + +// Wait blocks until the query completes. +func (op *mockQueryOperation) Wait() (*simulatedRows, error) { + <-op.finished // Wait for the operation to complete + op.mu.Lock() + defer op.mu.Unlock() + return op.result, op.opErr +} + +// Cancel attempts to interrupt the ongoing query by sending a signal. +func (op *mockQueryOperation) Cancel() error { + op.mu.Lock() + if op.canceled { // Already canceled or finished by context + op.mu.Unlock() + log.Printf("Mock QueryOperation: Attempted to cancel '%s' but it was already cancelled/finished.", op.query) + return nil // Or return a specific error if you want to differentiate + } + op.mu.Unlock() + + select { + case op.cancelSignal <- struct{}{}: // Send a cancellation signal + log.Printf("Mock QueryOperation: Sent explicit cancel signal for '%s'.", op.query) + return nil + case <-op.finished: + // Operation already finished before we could send the cancel signal + log.Printf("Mock QueryOperation: Attempted to cancel '%s' but it already finished.", op.query) + return nil + default: + // Should not happen if the buffer is 1 and handled correctly + log.Printf("Mock QueryOperation: Failed to send cancel signal for '%s'. Channel blocked or already sent.", op.query) + return errors.New("failed to send cancel signal") + } +} diff --git a/concurrency/cancellation/rows.go b/concurrency/cancellation/rows.go new file mode 100644 index 0000000..fce90d5 --- /dev/null +++ b/concurrency/cancellation/rows.go @@ -0,0 +1,45 @@ +package cancellation + +import ( + "errors" + "log" + "sync" +) + +// simulatedRows represents a simplified result set +type simulatedRows struct { + data []string + idx int + mu sync.Mutex // Protects access to data/idx +} + +func (sr *simulatedRows) Next() bool { + sr.mu.Lock() + defer sr.mu.Unlock() + if sr.idx < len(sr.data) { + sr.idx++ + return true + } + return false +} + +func (sr *simulatedRows) Scan(dest ...interface{}) error { + sr.mu.Lock() + defer sr.mu.Unlock() + if sr.idx-1 < len(sr.data) { + if len(dest) != 1 { + return errors.New("simulatedRows.Scan expects 1 destination") + } + if s, ok := dest[0].(*string); ok { + *s = sr.data[sr.idx-1] + return nil + } + return errors.New("simulatedRows.Scan: unsupported destination type") + } + return errors.New("simulatedRows.Scan: no more rows") +} + +func (sr *simulatedRows) Close() error { + log.Println("SimulatedRows: Closed.") + return nil +} diff --git a/go.mod b/go.mod index 8b894ce..960b864 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/loong/go-concurrency-exercises +module github.com/nathanverse/go-concurrency-exercises -go 1.19 +go 1.24.2 diff --git a/main.go b/main.go new file mode 100644 index 0000000..9b3358d --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println(subsets([]int{1, 2, 3})) +} diff --git a/subset.go b/subset.go new file mode 100644 index 0000000..8244693 --- /dev/null +++ b/subset.go @@ -0,0 +1,27 @@ +package main + +func subsets(nums []int) [][]int { + var res [][]int + res = append(res, []int{}) + for i, num := range nums { + parentArray := []int{num} + res = append(res, parentArray) + findSubsets(parentArray, &nums, i+1, &res) + } + + return res +} + +func findSubsets(curArray []int, nums *[]int, left int, res *[][]int) { + if left >= len(*nums) { + return + } + + for i := left; i <= len(*nums)-1; i++ { + newParSet := make([]int, len(curArray)) + copy(newParSet, curArray) + newParSet = append(newParSet, (*nums)[i]) + *res = append(*res, newParSet) + findSubsets(newParSet, nums, i+1, res) + } +} From 040d92766f71fa502501a9b21fed487d55564a63 Mon Sep 17 00:00:00 2001 From: spartan-vutran Date: Fri, 6 Jun 2025 08:16:25 +0700 Subject: [PATCH 2/7] chore: init --- .gitignore | 1 + concurrency/cancellation/cancellation.go | 3 - concurrency/cancellation/db.go | 27 ----- concurrency/cancellation/go.mod | 3 + concurrency/cancellation/imp/db.go | 74 +++++++++++++ concurrency/cancellation/{ => imp}/driver.go | 2 +- .../cancellation/{ => imp}/query_operation.go | 2 +- concurrency/cancellation/{ => imp}/rows.go | 2 +- concurrency/cancellation/main.go | 101 ++++++++++++++++++ 9 files changed, 182 insertions(+), 33 deletions(-) create mode 100644 .gitignore delete mode 100644 concurrency/cancellation/cancellation.go delete mode 100644 concurrency/cancellation/db.go create mode 100644 concurrency/cancellation/go.mod create mode 100644 concurrency/cancellation/imp/db.go rename concurrency/cancellation/{ => imp}/driver.go (98%) rename concurrency/cancellation/{ => imp}/query_operation.go (99%) rename concurrency/cancellation/{ => imp}/rows.go (97%) create mode 100644 concurrency/cancellation/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/concurrency/cancellation/cancellation.go b/concurrency/cancellation/cancellation.go deleted file mode 100644 index a3f0ec4..0000000 --- a/concurrency/cancellation/cancellation.go +++ /dev/null @@ -1,3 +0,0 @@ -package cancellation - -// diff --git a/concurrency/cancellation/db.go b/concurrency/cancellation/db.go deleted file mode 100644 index 7146c78..0000000 --- a/concurrency/cancellation/db.go +++ /dev/null @@ -1,27 +0,0 @@ -package cancellation - -import ( - "context" - "fmt" - "log" -) - -// ----------------------------------------------------------------------------- -// Your db instance (that uses the EmulatedDriver) -// ----------------------------------------------------------------------------- - -// YourDB is your custom database instance that would use the EmulatedDriver. -type YourDB struct { - driver EmulatedDriver -} - -// NewYourDB creates a new instance of YourDB with the provided driver. -func NewYourDB(driver EmulatedDriver) *YourDB { - return &YourDB{driver: driver} -} - -// QueryContext is your implementation of the database query method that -// supports context cancellation. -func (db *YourDB) QueryContext(ctx context.Context, query string, args ...interface{}) (*simulatedRows, error) { - -} diff --git a/concurrency/cancellation/go.mod b/concurrency/cancellation/go.mod new file mode 100644 index 0000000..0e1e79a --- /dev/null +++ b/concurrency/cancellation/go.mod @@ -0,0 +1,3 @@ +module go_concurrency/cancellation + +go 1.24.2 diff --git a/concurrency/cancellation/imp/db.go b/concurrency/cancellation/imp/db.go new file mode 100644 index 0000000..d797e21 --- /dev/null +++ b/concurrency/cancellation/imp/db.go @@ -0,0 +1,74 @@ +package imp + +import ( + "context" + "log" +) + +// ----------------------------------------------------------------------------- +// Your db instance (that uses the EmulatedDriver) +// ----------------------------------------------------------------------------- + +// YourDB is your custom database instance that would use the EmulatedDriver. +type YourDB struct { + driver EmulatedDriver +} + +// NewYourDB creates a new instance of YourDB with the provided driver. +func NewYourDB(driver EmulatedDriver) *YourDB { + return &YourDB{driver: driver} +} + +// QueryContext is your implementation of the database query method that +// supports context cancellation. +func (db *YourDB) QueryContext(ctx context.Context, query string, args ...interface{}) (*simulatedRows, error) { + queryOperation, err := db.driver.PrepareQuery(ctx, query, args) + if err != nil { + return nil, err + } + + resChannel := make(chan *simulatedRows) + errChannel := make(chan error) + finished := make(chan struct{}) + go func() { + defer close(finished) + res, err := queryOperation.Wait() + select { + case <-ctx.Done(): + log.Printf("Sub-goroutine for '%s': Context canceled. Not sending result/error.", query) + return // Exit the goroutine cleanly + default: + if err != nil { + errChannel <- err + } else { + resChannel <- res + } + } + }() + + var res *simulatedRows + select { + case <-ctx.Done(): + { + close(resChannel) + close(errChannel) + err := queryOperation.Cancel() + if err != nil { + return nil, err + } + + <-finished + return nil, ctx.Err() + } + case res = <-resChannel: + { + return res, nil + } + case err := <-errChannel: + { + return nil, err + } + } + + return nil, nil +} diff --git a/concurrency/cancellation/driver.go b/concurrency/cancellation/imp/driver.go similarity index 98% rename from concurrency/cancellation/driver.go rename to concurrency/cancellation/imp/driver.go index 94850e7..44978b8 100644 --- a/concurrency/cancellation/driver.go +++ b/concurrency/cancellation/imp/driver.go @@ -1,4 +1,4 @@ -package cancellation +package imp import ( "context" diff --git a/concurrency/cancellation/query_operation.go b/concurrency/cancellation/imp/query_operation.go similarity index 99% rename from concurrency/cancellation/query_operation.go rename to concurrency/cancellation/imp/query_operation.go index 377645c..87c67e0 100644 --- a/concurrency/cancellation/query_operation.go +++ b/concurrency/cancellation/imp/query_operation.go @@ -1,4 +1,4 @@ -package cancellation +package imp import ( "context" diff --git a/concurrency/cancellation/rows.go b/concurrency/cancellation/imp/rows.go similarity index 97% rename from concurrency/cancellation/rows.go rename to concurrency/cancellation/imp/rows.go index fce90d5..37a5aac 100644 --- a/concurrency/cancellation/rows.go +++ b/concurrency/cancellation/imp/rows.go @@ -1,4 +1,4 @@ -package cancellation +package imp import ( "errors" diff --git a/concurrency/cancellation/main.go b/concurrency/cancellation/main.go new file mode 100644 index 0000000..5283ba2 --- /dev/null +++ b/concurrency/cancellation/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "errors" + "fmt" + "go_concurrency/cancellation/imp" + "time" +) + +func main() { + // Initialize your DB with the mock driver + db := imp.NewYourDB(imp.NewMockEmulatedDriver()) + + // --- Test Case 1: Timeout (Query takes longer than context) --- + fmt.Println("\n--- Test Case 1: Timeout ---") + ctx1, cancel1 := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel1() + + start := time.Now() + rows1, err1 := db.QueryContext(ctx1, "SLOW QUERY") + duration := time.Since(start) + fmt.Printf("Query 1 completed in %v\n", duration) + + if err1 != nil { + if errors.Is(err1, context.DeadlineExceeded) { + fmt.Printf("Query 1 result: Expected error (Context deadline exceeded).\n") + } else { + fmt.Printf("Query 1 result: Unexpected error: %v\n", err1) + } + } else { + defer rows1.Close() + var data string + for rows1.Next() { + rows1.Scan(&data) + fmt.Printf("Data: %s\n", data) + } + fmt.Println("Query 1 result: Succeeded (unexpected).") + } + + // --- Test Case 2: Explicit Cancellation (Query is canceled before completion) --- + fmt.Println("\n--- Test Case 2: Explicit Cancellation ---") + ctx2, cancel2 := context.WithCancel(context.Background()) + + go func() { + time.Sleep(1 * time.Second) // Cancel after 1 second + fmt.Println("Main: Calling cancel2() for Query 2.") + cancel2() + }() + + start = time.Now() + rows2, err2 := db.QueryContext(ctx2, "ANOTHER SLOW QUERY") + duration = time.Since(start) + fmt.Printf("Query 2 completed in %v\n", duration) + + if err2 != nil { + if errors.Is(err2, context.Canceled) { + fmt.Printf("Query 2 result: Expected error (Context canceled).\n") + } else { + fmt.Printf("Query 2 result: Unexpected error: %v\n", err2) + } + } else { + defer rows2.Close() + var data string + for rows2.Next() { + rows2.Scan(&data) + fmt.Printf("Data: %s\n", data) + } + fmt.Println("Query 2 result: Succeeded (unexpected).") + } + + // --- Test Case 3: Query Completes Successfully (within context) --- + fmt.Println("\n--- Test Case 3: Success ---") + ctx3, cancel3 := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel3() + + start = time.Now() + rows3, err3 := db.QueryContext(ctx3, "FAST QUERY") // This query is designed to be faster + duration = time.Since(start) + fmt.Printf("Query 3 completed in %v\n", duration) + + if err3 != nil { + fmt.Printf("Query 3 result: Error: %v\n", err3) + } else { + defer rows3.Close() + var data string + found := false + for rows3.Next() { + rows3.Scan(&data) + fmt.Printf("Query 3 Data: %s\n", data) + found = true + } + if !found { + fmt.Println("Query 3 result: No rows found.") + } + fmt.Println("Query 3 result: Succeeded (expected).") + } + + // Give time for logs to print + time.Sleep(100 * time.Millisecond) +} From 60bf6c16cfccedcd3064dcb4db6f95ed4e5b73c8 Mon Sep 17 00:00:00 2001 From: spartan-vutran Date: Fri, 6 Jun 2025 08:20:08 +0700 Subject: [PATCH 3/7] chore: rename cancellation --- 0-limit-crawler/main.go | 2 +- {concurrency => 6-cancellation}/cancellation/README.md | 0 {concurrency => 6-cancellation}/cancellation/go.mod | 0 {concurrency => 6-cancellation}/cancellation/imp/db.go | 0 {concurrency => 6-cancellation}/cancellation/imp/driver.go | 0 .../cancellation/imp/query_operation.go | 0 {concurrency => 6-cancellation}/cancellation/imp/rows.go | 0 {concurrency => 6-cancellation}/cancellation/main.go | 0 8 files changed, 1 insertion(+), 1 deletion(-) rename {concurrency => 6-cancellation}/cancellation/README.md (100%) rename {concurrency => 6-cancellation}/cancellation/go.mod (100%) rename {concurrency => 6-cancellation}/cancellation/imp/db.go (100%) rename {concurrency => 6-cancellation}/cancellation/imp/driver.go (100%) rename {concurrency => 6-cancellation}/cancellation/imp/query_operation.go (100%) rename {concurrency => 6-cancellation}/cancellation/imp/rows.go (100%) rename {concurrency => 6-cancellation}/cancellation/main.go (100%) diff --git a/0-limit-crawler/main.go b/0-limit-crawler/main.go index ddadd14..0b0832a 100644 --- a/0-limit-crawler/main.go +++ b/0-limit-crawler/main.go @@ -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 diff --git a/concurrency/cancellation/README.md b/6-cancellation/cancellation/README.md similarity index 100% rename from concurrency/cancellation/README.md rename to 6-cancellation/cancellation/README.md diff --git a/concurrency/cancellation/go.mod b/6-cancellation/cancellation/go.mod similarity index 100% rename from concurrency/cancellation/go.mod rename to 6-cancellation/cancellation/go.mod diff --git a/concurrency/cancellation/imp/db.go b/6-cancellation/cancellation/imp/db.go similarity index 100% rename from concurrency/cancellation/imp/db.go rename to 6-cancellation/cancellation/imp/db.go diff --git a/concurrency/cancellation/imp/driver.go b/6-cancellation/cancellation/imp/driver.go similarity index 100% rename from concurrency/cancellation/imp/driver.go rename to 6-cancellation/cancellation/imp/driver.go diff --git a/concurrency/cancellation/imp/query_operation.go b/6-cancellation/cancellation/imp/query_operation.go similarity index 100% rename from concurrency/cancellation/imp/query_operation.go rename to 6-cancellation/cancellation/imp/query_operation.go diff --git a/concurrency/cancellation/imp/rows.go b/6-cancellation/cancellation/imp/rows.go similarity index 100% rename from concurrency/cancellation/imp/rows.go rename to 6-cancellation/cancellation/imp/rows.go diff --git a/concurrency/cancellation/main.go b/6-cancellation/cancellation/main.go similarity index 100% rename from concurrency/cancellation/main.go rename to 6-cancellation/cancellation/main.go From c0ab614bab9eebd9feb3ba72bc63c6192ef8aab3 Mon Sep 17 00:00:00 2001 From: spartan-vutran Date: Sat, 5 Jul 2025 12:02:11 +0700 Subject: [PATCH 4/7] chore: implement algo --- 7-lfu-cache/cache_test.go | 36 +++++++ 7-lfu-cache/go.mod | 3 + 7-lfu-cache/main.go | 205 ++++++++++++++++++++++++++++++++++++++ playground/main.go | 1 + 4 files changed, 245 insertions(+) create mode 100644 7-lfu-cache/cache_test.go create mode 100644 7-lfu-cache/go.mod create mode 100644 7-lfu-cache/main.go create mode 100644 playground/main.go diff --git a/7-lfu-cache/cache_test.go b/7-lfu-cache/cache_test.go new file mode 100644 index 0000000..2b3b0f4 --- /dev/null +++ b/7-lfu-cache/cache_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "slices" + "testing" +) + +func TestCache(t *testing.T) { + cache, err := NewLFUCache(3, func(key string) (string, error) { + return "", nil + }) + if err != nil { + t.Fatal(err) + } + + err = cache.Set("vu", "10") + err = cache.Set("nghia", "20") + err = cache.Set("luan", "5") + + value, err := cache.Get("vu") + if value != "10" { + t.Errorf("value should be 10, got %s", value) + } + + value, err = cache.Get("nghia") + if value != "20" { + t.Errorf("value should be 20, got %s", value) + } + + err = cache.Set("xanh", "30") + + keys := cache.GetKeys() + if slices.Contains(keys, "luan") { + t.Errorf("keys should not contain luan") + } +} diff --git a/7-lfu-cache/go.mod b/7-lfu-cache/go.mod new file mode 100644 index 0000000..cb2b8b6 --- /dev/null +++ b/7-lfu-cache/go.mod @@ -0,0 +1,3 @@ +module lfu_cache + +go 1.24.2 diff --git a/7-lfu-cache/main.go b/7-lfu-cache/main.go new file mode 100644 index 0000000..13a576d --- /dev/null +++ b/7-lfu-cache/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "container/list" + "errors" + "sync" +) + +var EMPTY_ERROR = errors.New("EMPTY ERROR") + +type Cache interface { + Get(key string) (string, error) + Set(key, value string) error +} + +type ( + LoaderFunc func(key string) (string, error) +) + +type baseCache struct { + mu sync.RWMutex + size int + loaderFunc LoaderFunc + loadGroup LoadGroup +} + +type LFUCache struct { + baseCache + cache map[string]*lfuItem + list *list.List // list of lruEntry +} + +type lruEntry struct { + freq int + items map[*lfuItem]struct{} +} + +type lfuItem struct { + value string + key string + el *list.Element // Reference to lruEntry +} + +type LoadGroup struct { +} + +func NewLFUCache(size int, loaderFunc LoaderFunc) (*LFUCache, error) { + if size <= 0 { + return nil, errors.New("size must be greater than zero") + } + + cache := &LFUCache{ + cache: make(map[string]*lfuItem), + list: list.New(), + } + + cache.baseCache.size = size + cache.baseCache.loaderFunc = loaderFunc + cache.baseCache.loadGroup = LoadGroup{} + + return cache, nil +} + +func (cache *LFUCache) Get(key string) (string, error) { + if item, ok := cache.cache[key]; ok { + // Move item to the higher bucket + err := cache.moveToHigherBucket(item) + if err != nil { + return "", err + } + + return item.value, nil + } + + // Miss, so load value + value, err := cache.loaderFunc(key) + if err != nil { + return "", err + } + + err = cache.Set(key, value) + if err != nil { + return "", err + } + + return value, nil +} + +func (cache *LFUCache) GetKeys() []string { + keys := make([]string, 0) + for k, _ := range cache.cache { + keys = append(keys, k) + } + + return keys +} + +func (cache *LFUCache) Set(key, value string) error { + if item, ok := cache.cache[key]; ok { + item.value = value + return nil + } + + if len(cache.cache) >= cache.size { + err := cache.evict() + if err != nil && !errors.Is(err, EMPTY_ERROR) { + return err + } + } + + cache.insert(key, value) + return nil +} + +// insert inserts key, value knowing that there is always slot for it +func (cache *LFUCache) insert(key, value string) { + insertedItem := &lfuItem{ + value: value, + key: key, + } + + cache.cache[key] = insertedItem + + var firstEntry *lruEntry + var firstElement *list.Element + if cache.list.Front() == nil || cache.list.Front().Value.(*lruEntry).freq != 0 { + firstEntry = &lruEntry{ + freq: 0, + items: make(map[*lfuItem]struct{}), + } + + firstElement = cache.list.PushFront(firstEntry) + } else { + firstElement = cache.list.Front() + firstEntry = firstElement.Value.(*lruEntry) + } + + firstEntry.items[insertedItem] = struct{}{} + insertedItem.el = firstElement +} + +func getItemToEvict(mapp map[*lfuItem]struct{}) (*lfuItem, error) { + for key, _ := range mapp { + return key, nil + } + + return nil, EMPTY_ERROR +} + +func (cache *LFUCache) evict() error { + zeroBucket := cache.list.Front() + if zeroBucket == nil { + return EMPTY_ERROR + } + + items := zeroBucket.Value.(*lruEntry).items + itemToRemove, err := getItemToEvict(items) + if err != nil { + return err + } + + delete(items, itemToRemove) + if len(items) == 0 { + cache.list.Remove(zeroBucket) + } + + delete(cache.cache, itemToRemove.key) + + return nil +} + +func (cache *LFUCache) moveToHigherBucket(item *lfuItem) error { + if item == nil { + return errors.New("item is nil") + } + + curBucket := item.el + curBucketEntry := curBucket.Value.(*lruEntry) + nextFreq := curBucketEntry.freq + 1 + delete(curBucketEntry.items, item) + + var nextBucket *list.Element + if item.el.Next() == nil || item.el.Next().Value.(*lruEntry).freq > nextFreq { + nextBucketEntry := &lruEntry{ + freq: nextFreq, + items: make(map[*lfuItem]struct{}), + } + + nextBucketEntry.items[item] = struct{}{} + nextBucket = cache.list.InsertAfter(nextBucketEntry, item.el) + } else { + nextBucket = item.el.Next() + nextBucketEntry := nextBucket.Value.(*lruEntry) + nextBucketEntry.items[item] = struct{}{} + } + + item.el = nextBucket + + // Remove last bucket in case it is empty + if len(curBucketEntry.items) == 0 { + cache.list.Remove(curBucket) + } + + return nil +} diff --git a/playground/main.go b/playground/main.go new file mode 100644 index 0000000..efa303e --- /dev/null +++ b/playground/main.go @@ -0,0 +1 @@ +package playground From d1b5a2930dafa36adcb029b670d2d399330db9fd Mon Sep 17 00:00:00 2001 From: spartan-vutran Date: Sat, 5 Jul 2025 13:14:55 +0700 Subject: [PATCH 5/7] chore: add test --- 7-lfu-cache/cache_test.go | 42 +++++++++++++++++++++++++++++++++++++++ 7-lfu-cache/main.go | 9 +++++++++ 2 files changed, 51 insertions(+) diff --git a/7-lfu-cache/cache_test.go b/7-lfu-cache/cache_test.go index 2b3b0f4..20e6d23 100644 --- a/7-lfu-cache/cache_test.go +++ b/7-lfu-cache/cache_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "slices" "testing" ) @@ -34,3 +35,44 @@ func TestCache(t *testing.T) { t.Errorf("keys should not contain luan") } } + +func TestCache1(t *testing.T) { + cache, err := NewLFUCache(3, func(key string) (string, error) { + return "", nil + }) + if err != nil { + t.Fatal(err) + } + + err = cache.Set("vu", "10") + err = cache.Set("nghia", "20") + err = cache.Set("luan", "5") + + for i := 0; i < 10; i++ { + cache.Get("vu") + } + + for i := 0; i < 9; i++ { + cache.Get("nghia") + } + + for i := 0; i < 8; i++ { + cache.Get("luan") + } + + i := 8 + for e := cache.GetBuckets().Front(); e != nil; e = e.Next() { + fmt.Printf("Value: %v (Type: %T)\n", e.Value, e.Value) + bucketFreq := cache.GetFreq(e) + if bucketFreq != i { + t.Errorf("bucketFreq should be %d, got %d", i, bucketFreq) + } + i += 1 + } + + err = cache.Set("xanh", "30") + keys := cache.GetKeys() + if slices.Contains(keys, "luan") { + t.Errorf("keys should not contain luan") + } +} diff --git a/7-lfu-cache/main.go b/7-lfu-cache/main.go index 13a576d..d9e7326 100644 --- a/7-lfu-cache/main.go +++ b/7-lfu-cache/main.go @@ -61,6 +61,15 @@ func NewLFUCache(size int, loaderFunc LoaderFunc) (*LFUCache, error) { return cache, nil } +// For testing +func (cache *LFUCache) GetBuckets() *list.List { + return cache.list +} + +func (cache *LFUCache) GetFreq(buckets *list.Element) int { + return buckets.Value.(*lruEntry).freq +} + func (cache *LFUCache) Get(key string) (string, error) { if item, ok := cache.cache[key]; ok { // Move item to the higher bucket From dec9502fdf48c5fcdf50a3e3d1e5c6072d472a81 Mon Sep 17 00:00:00 2001 From: spartan-vutran Date: Sat, 5 Jul 2025 15:37:24 +0700 Subject: [PATCH 6/7] chore: cover concurrent cache --- 2-race-in-cache/main.go | 160 ++++++++++++++++++++++++++++++-------- 7-lfu-cache/cache_test.go | 80 +++++++++++++++++++ 7-lfu-cache/main.go | 11 ++- playground/go.mod | 5 ++ playground/go.sum | 2 + playground/main.go | 46 ++++++++++- 6 files changed, 270 insertions(+), 34 deletions(-) create mode 100644 playground/go.mod create mode 100644 playground/go.sum diff --git a/2-race-in-cache/main.go b/2-race-in-cache/main.go index 7618dd1..01fdbd0 100644 --- a/2-race-in-cache/main.go +++ b/2-race-in-cache/main.go @@ -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 } diff --git a/7-lfu-cache/cache_test.go b/7-lfu-cache/cache_test.go index 20e6d23..5bf5c78 100644 --- a/7-lfu-cache/cache_test.go +++ b/7-lfu-cache/cache_test.go @@ -1,9 +1,15 @@ package main import ( + "errors" "fmt" + "math/rand" + "os" "slices" + "strings" + "sync" "testing" + "time" ) func TestCache(t *testing.T) { @@ -76,3 +82,77 @@ func TestCache1(t *testing.T) { t.Errorf("keys should not contain luan") } } + +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func generaterandomstringMathrand(length int) string { + if length <= 0 { + return "" + } + + // Use strings.Builder for efficient string concatenation. + // It pre-allocates memory, avoiding multiple re-allocations. + var sb strings.Builder + sb.Grow(length) // Pre-allocate capacity for efficiency + + charsetLen := len(charset) + for i := 0; i < length; i++ { + // Pick a random index from the charset + randomIndex := rand.Intn(charsetLen) + // Append the character at that index + sb.WriteByte(charset[randomIndex]) + } + + return sb.String() +} + +// --- Test Main for Global Setup --- +func TestMain(m *testing.M) { + // Seed the global random number generator once for all tests in this package. + // This is CRUCIAL for reproducible random behavior across test runs. + rand.New(rand.NewSource(time.Now().UnixNano())) + + // Run all tests + code := m.Run() + + // Exit with the test result code + os.Exit(code) +} + +func TestCacheConcurrency(t *testing.T) { + cache, _ := NewLFUCache(5, func(key string) (string, error) { + return "", errors.New("Loader hasn't been implemented yet") + }) + + keyValueMap := []string{"vu", "nghia", "luan", "xanh", "orange", "thuong", + "tien", "lemon", "durian", "rambutant", "pear", "mango", "apple"} + + var wg sync.WaitGroup + maxSetOperations := 10000 + maxGetOperations := 5000 + // Setter + for i := 0; i < 3; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < maxSetOperations; i++ { + randomNumber := rand.Intn(len(keyValueMap)) + 0 + cache.Set(keyValueMap[randomNumber], generaterandomstringMathrand(5)) + } + }() + } + + // 5 getters + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < maxGetOperations; j++ { + randomNumber := rand.Intn(len(keyValueMap)) + 0 + cache.Get(keyValueMap[randomNumber]) + } + }() + } + + wg.Wait() +} diff --git a/7-lfu-cache/main.go b/7-lfu-cache/main.go index d9e7326..f4fc737 100644 --- a/7-lfu-cache/main.go +++ b/7-lfu-cache/main.go @@ -18,7 +18,7 @@ type ( ) type baseCache struct { - mu sync.RWMutex + mu sync.Mutex size int loaderFunc LoaderFunc loadGroup LoadGroup @@ -71,15 +71,19 @@ func (cache *LFUCache) GetFreq(buckets *list.Element) int { } func (cache *LFUCache) Get(key string) (string, error) { + cache.mu.Lock() if item, ok := cache.cache[key]; ok { // Move item to the higher bucket + v := item.value err := cache.moveToHigherBucket(item) if err != nil { return "", err } + cache.mu.Unlock() - return item.value, nil + return v, nil } + cache.mu.Unlock() // Miss, so load value value, err := cache.loaderFunc(key) @@ -105,6 +109,9 @@ func (cache *LFUCache) GetKeys() []string { } func (cache *LFUCache) Set(key, value string) error { + cache.mu.Lock() + defer cache.mu.Unlock() + if item, ok := cache.cache[key]; ok { item.value = value return nil diff --git a/playground/go.mod b/playground/go.mod new file mode 100644 index 0000000..0b01687 --- /dev/null +++ b/playground/go.mod @@ -0,0 +1,5 @@ +module go/playground + +go 1.24.2 + +require golang.org/x/sync v0.15.0 diff --git a/playground/go.sum b/playground/go.sum new file mode 100644 index 0000000..f98f706 --- /dev/null +++ b/playground/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= diff --git a/playground/main.go b/playground/main.go index efa303e..09423ef 100644 --- a/playground/main.go +++ b/playground/main.go @@ -1 +1,45 @@ -package playground +package main + +import ( + "context" + "errors" + "fmt" + "time" + + "golang.org/x/sync/errgroup" +) + +func hello() error { // hello needs to return error to be compatible with errgroup.Go + fmt.Println("hello world") + time.Sleep(time.Second * 10) + fmt.Println("hello world ended") + return nil // Return nil or an actual error +} + +func hello1() error { + fmt.Println("hello world 1") + time.Sleep(time.Second * 1) + return errors.New("hello world 1 goes wrong") +} + +func main() { + g, cancel := errgroup.WithContext(context.Background()) + g.SetLimit(2) // Set a limit of 2 concurrent goroutines + + // Pass the functions as values, not the result of their execution + g.Go(hello) // Correct way: pass the function hello + g.Go(hello1) // Correct way: pass the function hello1 + + select { + case <-cancel.Done(): + { + err := cancel.Err() + if err != nil { + fmt.Printf("Error occurred %s", err.Error()) + } + } + } + + _ := g.Wait() + fmt.Println("Done") +} From 37c23991e51906916e9747dabb4c4bb1e240e249 Mon Sep 17 00:00:00 2001 From: spartan-vutran Date: Tue, 8 Jul 2025 08:36:07 +0700 Subject: [PATCH 7/7] feat: done benchmark cpu task --- .gitignore | 3 +- 0-limit-crawler/check_test.go | 2 +- 0-limit-crawler/mockfetcher.go | 2 +- 1-producer-consumer/mockstream.go | 2 +- 2-race-in-cache/check_test.go | 4 +- 2-race-in-cache/mockdb.go | 4 +- 2-race-in-cache/mockserver.go | 4 +- 3-limit-service-time/mockserver.go | 2 +- 4-graceful-sigint/mockprocess.go | 4 +- 5-session-cleaner/helper.go | 2 +- .../cancellation/imp/query_operation.go | 2 +- 7-lfu-cache/load_group.go | 71 ++++++ 7-lfu-cache/main.go | 8 +- go.mod | 7 + go.sum | 25 ++ playground/main.go | 40 --- queue/Makefile | 19 ++ queue/bench/cpu_bound/bench_16.txt | 9 + queue/bench/cpu_bound/bench_32.txt | 9 + queue/bench/cpu_bound/bench_64.txt | 9 + queue/bench/cpu_bound/bench_8.txt | 15 ++ queue/bench/cpu_bound/bench_numCPU.txt | 9 + queue/bench/cpu_bound/benchmark_test.go | 121 +++++++++ queue/bench/cpu_bound/cpu.pprof | Bin 0 -> 1634 bytes queue/cmd/bench/main.go | 27 ++ queue/go.mod | 10 + queue/go.sum | 25 ++ queue/internal/base.go | 144 +++++++++++ queue/main.go | 52 ++++ queue/runner/client.go | 233 ++++++++++++++++++ queue/runner/server.go | 42 ++++ queue/server/server.go | 157 ++++++++++++ queue/tasks/base.go | 82 ++++++ queue/trace.out | Bin 0 -> 135550 bytes 34 files changed, 1087 insertions(+), 58 deletions(-) create mode 100644 7-lfu-cache/load_group.go create mode 100644 queue/Makefile create mode 100644 queue/bench/cpu_bound/bench_16.txt create mode 100644 queue/bench/cpu_bound/bench_32.txt create mode 100644 queue/bench/cpu_bound/bench_64.txt create mode 100644 queue/bench/cpu_bound/bench_8.txt create mode 100644 queue/bench/cpu_bound/bench_numCPU.txt create mode 100644 queue/bench/cpu_bound/benchmark_test.go create mode 100644 queue/bench/cpu_bound/cpu.pprof create mode 100644 queue/cmd/bench/main.go create mode 100644 queue/go.mod create mode 100644 queue/go.sum create mode 100644 queue/internal/base.go create mode 100644 queue/main.go create mode 100644 queue/runner/client.go create mode 100644 queue/runner/server.go create mode 100644 queue/server/server.go create mode 100644 queue/tasks/base.go create mode 100644 queue/trace.out diff --git a/.gitignore b/.gitignore index 723ef36..250c385 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea \ No newline at end of file +.idea +.vendor \ No newline at end of file diff --git a/0-limit-crawler/check_test.go b/0-limit-crawler/check_test.go index 8451bc0..cc1f178 100644 --- a/0-limit-crawler/check_test.go +++ b/0-limit-crawler/check_test.go @@ -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 diff --git a/0-limit-crawler/mockfetcher.go b/0-limit-crawler/mockfetcher.go index c94e1dd..becc3a6 100644 --- a/0-limit-crawler/mockfetcher.go +++ b/0-limit-crawler/mockfetcher.go @@ -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 diff --git a/1-producer-consumer/mockstream.go b/1-producer-consumer/mockstream.go index 93b9da6..58c6791 100644 --- a/1-producer-consumer/mockstream.go +++ b/1-producer-consumer/mockstream.go @@ -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 diff --git a/2-race-in-cache/check_test.go b/2-race-in-cache/check_test.go index 45a756a..7f21666 100644 --- a/2-race-in-cache/check_test.go +++ b/2-race-in-cache/check_test.go @@ -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 @@ -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() diff --git a/2-race-in-cache/mockdb.go b/2-race-in-cache/mockdb.go index 8bd1a4c..38f724c 100644 --- a/2-race-in-cache/mockdb.go +++ b/2-race-in-cache/mockdb.go @@ -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 @@ -12,7 +12,7 @@ import ( ) // MockDB used to simulate a database model -type MockDB struct{ +type MockDB struct { Calls int32 } diff --git a/2-race-in-cache/mockserver.go b/2-race-in-cache/mockserver.go index a60fab2..1a432c2 100644 --- a/2-race-in-cache/mockserver.go +++ b/2-race-in-cache/mockserver.go @@ -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 @@ -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) } } diff --git a/3-limit-service-time/mockserver.go b/3-limit-service-time/mockserver.go index f435c9d..211a6da 100644 --- a/3-limit-service-time/mockserver.go +++ b/3-limit-service-time/mockserver.go @@ -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 diff --git a/4-graceful-sigint/mockprocess.go b/4-graceful-sigint/mockprocess.go index fdf5ecd..c040cf9 100644 --- a/4-graceful-sigint/mockprocess.go +++ b/4-graceful-sigint/mockprocess.go @@ -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 @@ -15,7 +15,7 @@ import ( // MockProcess for example type MockProcess struct { - mu sync.Mutex + mu sync.Mutex isRunning bool } diff --git a/5-session-cleaner/helper.go b/5-session-cleaner/helper.go index 74f93f3..6e8cc5f 100644 --- a/5-session-cleaner/helper.go +++ b/5-session-cleaner/helper.go @@ -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 diff --git a/6-cancellation/cancellation/imp/query_operation.go b/6-cancellation/cancellation/imp/query_operation.go index 87c67e0..b310678 100644 --- a/6-cancellation/cancellation/imp/query_operation.go +++ b/6-cancellation/cancellation/imp/query_operation.go @@ -38,7 +38,7 @@ func (op *mockQueryOperation) run(ctx context.Context) { // Simulate query execution time queryDuration := 3 * time.Second // Default query duration if op.query == "FAST QUERY" { - queryDuration = 500 * time.Millisecond // A faster query + queryDuration = 500 * time.Millisecond // Iteration faster query } log.Printf("Mock QueryOperation: Starting execution for '%s' (will take %v)", op.query, queryDuration) diff --git a/7-lfu-cache/load_group.go b/7-lfu-cache/load_group.go new file mode 100644 index 0000000..a7f7d66 --- /dev/null +++ b/7-lfu-cache/load_group.go @@ -0,0 +1,71 @@ +package main + +import ( + "container/list" + "sync" +) + +type LoadGroup struct { + calls map[string]*call + mutex sync.Mutex + cache *Cache +} + +type call struct { + mu sync.RWMutex + result *string + err *error +} + +// Get ensures one loading task is run even if multiple threads are waiting on the same key +// /* +func (l *LoadGroup) Get(key string, loaderFunc LoaderFunc) (string, error) { + l.mutex.Lock() + cache := *(l.cache) + vc, err := cache.GetWithoutLoad(key) + if err != nil { + // + } + + if len(vc) != 0 { + l.mutex.Unlock() + return vc, nil + } + + if call, ok := l.calls[key]; ok { + l.mutex.Unlock() + + call.mu.RLock() + result := call.result + call.mu.RUnlock() + return *result, nil + } + + call := &call{ + result: new(string), + } + + l.calls[key] = call + call.mu.Lock() + l.mutex.Unlock() + + // TODO: handling panic + v, err := loaderFunc(key) + if err != nil { + + } + call.result = &v + call.mu.Unlock() + + // Remove call and update cache + l.mutex.Lock() + err = cache.Set(key, v) + if err != nil { + // TODO: handling error + l.mutex.Unlock() + return "", err + } + + delete(l.calls, key) + l.mutex.Unlock() +} diff --git a/7-lfu-cache/main.go b/7-lfu-cache/main.go index f4fc737..61924bf 100644 --- a/7-lfu-cache/main.go +++ b/7-lfu-cache/main.go @@ -10,6 +10,7 @@ var EMPTY_ERROR = errors.New("EMPTY ERROR") type Cache interface { Get(key string) (string, error) + GetWithoutLoad(key string) (string, error) Set(key, value string) error } @@ -41,9 +42,6 @@ type lfuItem struct { el *list.Element // Reference to lruEntry } -type LoadGroup struct { -} - func NewLFUCache(size int, loaderFunc LoaderFunc) (*LFUCache, error) { if size <= 0 { return nil, errors.New("size must be greater than zero") @@ -99,6 +97,10 @@ func (cache *LFUCache) Get(key string) (string, error) { return value, nil } +func load(key string) (string, error) { + +} + func (cache *LFUCache) GetKeys() []string { keys := make([]string, 0) for k, _ := range cache.cache { diff --git a/go.mod b/go.mod index 960b864..12abc80 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/nathanverse/go-concurrency-exercises go 1.24.2 + +require github.com/pkg/profile v1.7.0 + +require ( + github.com/felixge/fgprof v0.9.3 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect +) diff --git a/go.sum b/go.sum index e69de29..6e67449 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/playground/main.go b/playground/main.go index 09423ef..7905807 100644 --- a/playground/main.go +++ b/playground/main.go @@ -1,45 +1,5 @@ package main -import ( - "context" - "errors" - "fmt" - "time" - - "golang.org/x/sync/errgroup" -) - -func hello() error { // hello needs to return error to be compatible with errgroup.Go - fmt.Println("hello world") - time.Sleep(time.Second * 10) - fmt.Println("hello world ended") - return nil // Return nil or an actual error -} - -func hello1() error { - fmt.Println("hello world 1") - time.Sleep(time.Second * 1) - return errors.New("hello world 1 goes wrong") -} - func main() { - g, cancel := errgroup.WithContext(context.Background()) - g.SetLimit(2) // Set a limit of 2 concurrent goroutines - - // Pass the functions as values, not the result of their execution - g.Go(hello) // Correct way: pass the function hello - g.Go(hello1) // Correct way: pass the function hello1 - - select { - case <-cancel.Done(): - { - err := cancel.Err() - if err != nil { - fmt.Printf("Error occurred %s", err.Error()) - } - } - } - _ := g.Wait() - fmt.Println("Done") } diff --git a/queue/Makefile b/queue/Makefile new file mode 100644 index 0000000..8aca680 --- /dev/null +++ b/queue/Makefile @@ -0,0 +1,19 @@ +.PHONY: server client + +ADDR ?= :8080 +CAPACITY ?= 200 +WORKERS ?= 8 +TOTAL ?= 100 +CONCURRENCY ?= 4 +ITERATIONS ?= 3 +PPROF_PORT ?= 8081 +PPROF_FILE ?= mem.pprof + +server: + go run main.go -mode=server -addr=$(ADDR) -capacity=$(CAPACITY) -workers=$(WORKERS) + +client: + go run main.go -mode=client -addr=$(ADDR) -total=$(TOTAL) -concurrency=$(CONCURRENCY) -iterations=$(ITERATIONS) + +profmem: + go tool pprof -http=:$(PPROF_PORT) $(PPROF_FILE) diff --git a/queue/bench/cpu_bound/bench_16.txt b/queue/bench/cpu_bound/bench_16.txt new file mode 100644 index 0000000..ec96a13 --- /dev/null +++ b/queue/bench/cpu_bound/bench_16.txt @@ -0,0 +1,9 @@ +goos: darwin +goarch: arm64 +pkg: vu/benchmark/queue +cpu: Apple M2 +BenchmarkQueueHashFixedIterations-8 4 255125438 ns/op +BenchmarkQueueHashFixedIterations-8 4 262671688 ns/op +BenchmarkQueueHashFixedIterations-8 4 254897771 ns/op +PASS +ok vu/benchmark/queue 23.770s diff --git a/queue/bench/cpu_bound/bench_32.txt b/queue/bench/cpu_bound/bench_32.txt new file mode 100644 index 0000000..bfb704b --- /dev/null +++ b/queue/bench/cpu_bound/bench_32.txt @@ -0,0 +1,9 @@ +goos: darwin +goarch: arm64 +pkg: vu/benchmark/queue +cpu: Apple M2 +BenchmarkQueueHashFixedIterations-8 4 253688323 ns/op +BenchmarkQueueHashFixedIterations-8 4 254959531 ns/op +BenchmarkQueueHashFixedIterations-8 4 254726896 ns/op +PASS +ok vu/benchmark/queue 23.966s diff --git a/queue/bench/cpu_bound/bench_64.txt b/queue/bench/cpu_bound/bench_64.txt new file mode 100644 index 0000000..94ac2a7 --- /dev/null +++ b/queue/bench/cpu_bound/bench_64.txt @@ -0,0 +1,9 @@ +goos: darwin +goarch: arm64 +pkg: vu/benchmark/queue +cpu: Apple M2 +BenchmarkQueueHashFixedIterations-8 4 254658396 ns/op +BenchmarkQueueHashFixedIterations-8 4 254396916 ns/op +BenchmarkQueueHashFixedIterations-8 4 265479604 ns/op +PASS +ok vu/benchmark/queue 24.486s diff --git a/queue/bench/cpu_bound/bench_8.txt b/queue/bench/cpu_bound/bench_8.txt new file mode 100644 index 0000000..dbbd5e4 --- /dev/null +++ b/queue/bench/cpu_bound/bench_8.txt @@ -0,0 +1,15 @@ +2025/12/15 15:13:16 profile: cpu profiling enabled, cpu.pprof +2025/12/15 15:13:18 profile: cpu profiling disabled, cpu.pprof +goos: darwin +goarch: arm64 +pkg: vu/benchmark/queue +cpu: Apple M2 +BenchmarkQueueHashFixedIterations-8 2025/12/15 15:13:18 profile: cpu profiling enabled, cpu.pprof +2025/12/15 15:13:20 profile: cpu profiling disabled, cpu.pprof +2025/12/15 15:13:20 profile: cpu profiling enabled, cpu.pprof +2025/12/15 15:13:22 profile: cpu profiling disabled, cpu.pprof +2025/12/15 15:13:22 profile: cpu profiling enabled, cpu.pprof +2025/12/15 15:13:24 profile: cpu profiling disabled, cpu.pprof + 4 253199010 ns/op +PASS +ok vu/benchmark/queue 8.583s diff --git a/queue/bench/cpu_bound/bench_numCPU.txt b/queue/bench/cpu_bound/bench_numCPU.txt new file mode 100644 index 0000000..8947c96 --- /dev/null +++ b/queue/bench/cpu_bound/bench_numCPU.txt @@ -0,0 +1,9 @@ +goos: darwin +goarch: arm64 +pkg: vu/benchmark/queue +cpu: Apple M2 +BenchmarkQueueHashFixedIterations-8 4 265989531 ns/op +BenchmarkQueueHashFixedIterations-8 4 254028333 ns/op +BenchmarkQueueHashFixedIterations-8 4 255124583 ns/op +PASS +ok vu/benchmark/queue 23.775s diff --git a/queue/bench/cpu_bound/benchmark_test.go b/queue/bench/cpu_bound/benchmark_test.go new file mode 100644 index 0000000..6852620 --- /dev/null +++ b/queue/bench/cpu_bound/benchmark_test.go @@ -0,0 +1,121 @@ +package cpu_bound + +import ( + "encoding/json" + "github.com/pkg/profile" + "strconv" + "testing" + "time" + + "vu/benchmark/queue/internal" + "vu/benchmark/queue/tasks" +) + +func TestBench(t *testing.T) { + defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop() + const iterations = 3_000_000_000 + + poolSize := 8 + capacity := 1_000 + + payload, err := json.Marshal(tasks.BurnCPUTaskInput{Iteration: iterations}) + if err != nil { + t.Fatalf("marshal hash input: %v", err) + } + + queue := internal.NewQueue(capacity, poolSize, true) + pending := make(chan (<-chan internal.Output), capacity) + for i := 0; i < 5; i++ { + go func() { + for ch := range pending { + out := <-ch + + if out.Err != nil { + t.Logf("task failed: %v", out.Err) + } else { + //t.Logf("task ok: %s", out.Res) + } + } + }() + } + + i := 0 + for i < 100 { + task := tasks.Task{ + Id: strconv.Itoa(i), + Type: tasks.BurnCPUTaskType, + Input: payload, + } + + ch, err := queue.Put(&task) + if err != nil { + //t.Logf("Error when put task %d: %v", i, err) + time.Sleep(2 * time.Second) + continue + } + + //t.Logf("put task %d", i) + pending <- ch + i++ + } + + queue.Shutdown() + close(pending) + time.Sleep(1 * time.Second) +} + +// Benchmark queue throughput for the hash task using a fixed iteration count. +func BenchmarkQueueHashFixedIterations(b *testing.B) { + defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop() + const iterations = 3_000_000_000 + + poolSize := 8 + capacity := 1_000 + + payload, err := json.Marshal(tasks.BurnCPUTaskInput{Iteration: iterations}) + if err != nil { + b.Fatalf("marshal hash input: %v", err) + } + + queue := internal.NewQueue(capacity, poolSize, true) + pending := make(chan (<-chan internal.Output), capacity) + for i := 0; i < 5; i++ { + go func() { + for ch := range pending { + out := <-ch + + if out.Err != nil { + b.Logf("task failed: %v", out.Err) + } else { + //b.Logf("task ok: %s", out.Res) + } + } + }() + } + + i := 0 + b.ResetTimer() + for i < b.N { + task := tasks.Task{ + Id: strconv.Itoa(i), + Type: tasks.BurnCPUTaskType, + Input: payload, + } + + ch, err := queue.Put(&task) + if err != nil { + //b.Logf("Error when put task %d: %v", i, err) + time.Sleep(2 * time.Second) + continue + } + + //b.Logf("put task %d", i) + pending <- ch + i++ + } + + queue.Shutdown() + b.StopTimer() + close(pending) + time.Sleep(1 * time.Second) +} diff --git a/queue/bench/cpu_bound/cpu.pprof b/queue/bench/cpu_bound/cpu.pprof new file mode 100644 index 0000000000000000000000000000000000000000..74de8f60b9ee142cc90b086320578f994a3fb43e GIT binary patch literal 1634 zcmV-o2A%mIiwFP!00004|CCg1Y#c`!=DvL6JKx>A=dah_lTDh8uVpt`3PMO!C~ee= zrp2o3Km0kyd(-pvZf7q$yEf+6MMWxVK?+c*gtS6x5&4mp&{hHhNTq7UM<6OHAS6Qw zA4OD6K|+lp1QJ3!>$4rz4&s;3^SsadJn!?)&OKi_z44#dU;6bAv5+MxNEfms1KEX3 zuU>lc-cRb^pXxpTr>qGig{Q8cI|*i7b{tZ*0!cv*b98_mGl8V><;S1XXc|7F(KO^S zPYLr)AQ^o3vk97kMU7@)5Cu*1ZuE*Yehn!id8zyP7j&}A$awB8$B>{WDqXqwZSp6oS?|grA#10`2DR=7a&zFLki;67IKB+PXq=cXUOVXhT zPp5AvDQ%XN@%$H)+)D6lR(meNBu>&2dnRiFso*dFaare9hI@2WWthS#I?b2~1n|kH zf2Gk1+@aA5%-{^2WtNG=z%T#)4>}KkD`hiB4A?*|0WgPil(Ng^T9FvA5BE{ZE}?0V zDn9b!A2p{6PtED#Dwt?e%AS}r4Kj>Zevsh9@I2M{Fzm_YO;e7cqIvK z1PyeXs?d)pU#@^r9Ud_aGK%NFlq4_;-%S!2gY+<4o1)PfJF}#&KeDD!z5o_}2B1^q6}!pHQ$vv3#>(<7{G8e|S1`P^grPR~KFth>*FgAV-w zdwZx_>Kn>V&&HmxXj!JRY>|6Oo979dgxH*3l@CcP=bF$W`0-;F#WLuGrr3AALW>Gm>LTI298Q}l~0R;z1Y^Hxi? zLhGSOx<{OmAd+Fr6BbwN(p?CI@VcQhZ$~PyJHE?1cGI_2;Mze_XLpm5IuE=L-EJwT z(r@7L#%fCm=a9W8SFNjhg9mFZX*GSv=rh&2T2i9%iN%|7J>l)(ZmDi^9lE!wC%jm0 zvMCzPs4v(lKwsj%#E7^zQu49Eba;T34a)RLI@7bIaasx$?FmoEFmc z8!g$iA6CA!q*yx^ABe?}hr-!q7dO-=|LOIQ-T+wl@j$Kcjx*8s)(uwz!5ekgmyP-w zZ-qN{AUv)*LUf&jdzSMO7SFhO zwxyv$9)vqIV!XR887IBIee0Bv4Xf$v)6F}x^#Mq+<}W`i+|b#-cNCt!u5lOz{lPBk zZQUe0UDfoR>GwVg)`LQYN8`uHxx@Z{p0wqd6F-slP^d8OUWtFSWYY>Ch@=#ObKt!r zY?k!@#B4tTI`-^tw-q3n-~@MyqWQrUO=$GL1chn){vN9@q+ZXVe6dVOn# gdP~+ q.capacity { + q.mutex.Unlock() + return nil, errors.New("queue is full") + } + + if q.closed { + q.mutex.Unlock() + return nil, errors.New("queue is closed") + } + + defer q.mutex.Unlock() + + q.wg.Add(1) + q.size += 1 + + channel := make(chan Output, 1) + + q.channel <- _taskWrapper{ + task: task, + channel: channel, + } + return channel, nil +} + +func (q *_queue) Shutdown() error { + q.mutex.Lock() + q.closed = true + q.mutex.Unlock() + + q.wg.Wait() + return nil +} + +func (q *_queue) init() { + // Avoid spamming stdout when running benchmarks so measurements stay clean. + logWorkers := q.shouldLogWorker() + + for i := 0; i < q.poolSize; i++ { + workerID := i + 1 + go func(id int) { + for task := range q.channel { + if logWorkers { + fmt.Printf("Worker %d, pick up tasks %s\n", id, task.task.Id) + } + res, err := Execute(task) + task.channel <- Output{Res: res, Err: err} + close(task.channel) + + q.mutex.Lock() + q.size-- + q.mutex.Unlock() + q.wg.Done() + } + }(workerID) + } +} + +func NewQueue(capacity int, poolSize int, logDisabled bool) IQueue { + channel := make(chan _taskWrapper, capacity) + + queue := &_queue{ + capacity: capacity, + channel: channel, + poolSize: poolSize, + logDisabled: logDisabled, + } + + queue.init() + + return queue +} + +func Execute(task _taskWrapper) ([]byte, error) { + switch task.task.Type { + case tasks.SumTaskType: + return tasks.SumTask(task.task.Input) + case tasks.HashTaskType: + input := tasks.HashTaskInput{} + if err := json.Unmarshal(task.task.Input, &input); err != nil { + return nil, err + } + return tasks.HashTask(input.Iteration), nil + case tasks.BurnCPUTaskType: + return tasks.BurnCPUTask(task.task.Input) + default: + { + return nil, errors.New("invalid tasks type") + } + } +} + +// shouldLogWorker reports whether worker-level logging is enabled. +// Benchmarks pass -test.bench which adds the test.bench flag; if it is +// non-empty we suppress logs to keep benchmark output clean. +func (q *_queue) shouldLogWorker() bool { + //benchFlag := flag.Lookup("test.bench") + //if benchFlag != nil || q.logDisabled { + // return false + //} + + if q.logDisabled { + return false + } + + return true +} diff --git a/queue/main.go b/queue/main.go new file mode 100644 index 0000000..d4cb087 --- /dev/null +++ b/queue/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "flag" + "fmt" + "os" + "vu/benchmark/queue/runner" +) + +//go run main.go -mode=server -workers=4 -capacity=10 -addr=:8082 + +func main() { + mode := flag.String("mode", "server", "choose server or client mode") + addr := flag.String("addr", ":8080", "tcp listen address") + + // Server options. + capacity := flag.Int("capacity", 100, "queue capacity") + workers := flag.Int("workers", 8, "number of worker goroutines") + + // Client options. + total := flag.Int("total", 1000, "total tasks to run") + concurrency := flag.Int("concurrency", 8, "concurrent client workers") + iterations := flag.Int("iterations", 100000, "hash iterations per tasks") + + flag.Parse() + + switch *mode { + case "server": + err := runner.RunServer(runner.ServerConfig{ + Addr: *addr, + Capacity: *capacity, + Workers: *workers, + }) + if err != nil { + os.Exit(1) + } + case "client": + err := runner.RunClient(runner.ClientConfig{ + Addr: *addr, + Total: *total, + Concurrency: *concurrency, + Iterations: *iterations, + }) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + default: + fmt.Fprintf(os.Stderr, "unknown mode %q (expected server or client)\n", *mode) + os.Exit(1) + } +} diff --git a/queue/runner/client.go b/queue/runner/client.go new file mode 100644 index 0000000..420cd0e --- /dev/null +++ b/queue/runner/client.go @@ -0,0 +1,233 @@ +package runner + +import ( + "encoding/json" + "fmt" + "net" + "strconv" + "sync" + "sync/atomic" + "time" + "vu/benchmark/queue/tasks" +) + +// ClientConfig collects options for pushing tasks into the queue server. +type ClientConfig struct { + Addr string + Total int + Concurrency int + Iterations int +} + +func RunClient(cfg ClientConfig) error { + payload, err := json.Marshal(tasks.HashTaskInput{Iteration: cfg.Iterations}) + if err != nil { + return err + } + + var sent int64 + var completed int64 + + var wg sync.WaitGroup + errCh := make(chan error, 1) + var once sync.Once + + start := time.Now() + + for i := 0; i < cfg.Concurrency; i++ { + wg.Add(1) + go func(index int) { + fmt.Printf("Starting goroutine %d\n", index+1) + defer wg.Done() + + conn, err := net.Dial("tcp", cfg.Addr) + if err != nil { + recordError(errCh, &once, err) + return + } + defer conn.Close() + + encoder := json.NewEncoder(conn) + decoder := json.NewDecoder(conn) + var lastTask int64 + for { + if lastTask == 0 { + lastTask = atomic.AddInt64(&sent, 1) + } + + if lastTask > int64(cfg.Total) { + return + } + + fmt.Printf("Goroutine %d runs tasks %d\n", index+1, int(lastTask)) + + task := tasks.Task{ + Id: strconv.FormatInt(lastTask, 10), + Type: tasks.HashTaskType, + Input: payload, + } + + // ---- NEW: retry loop per tasks ---- + for retry := 0; retry < 5; retry++ { + + if err := encoder.Encode(task); err != nil { + fmt.Printf("Goroutine %d: encode error %v — reconnecting\n", index+1, err) + conn.Close() + + // reconnect + var err2 error + conn, err2 = net.Dial("tcp", cfg.Addr) + if err2 != nil { + fmt.Printf("Goroutine %d: reconnect failed %v\n", index+1, err2) + time.Sleep(time.Second) + continue + } + encoder = json.NewEncoder(conn) + decoder = json.NewDecoder(conn) + continue + } + + var resp clientResponse + if err := decoder.Decode(&resp); err != nil { + fmt.Printf("Goroutine %d: decode error %v — reconnecting\n", index+1, err) + conn.Close() + + // reconnect + var err2 error + conn, err2 = net.Dial("tcp", cfg.Addr) + if err2 != nil { + fmt.Printf("Goroutine %d: reconnect failed %v\n", index+1, err2) + time.Sleep(time.Second) + continue + } + encoder = json.NewEncoder(conn) + decoder = json.NewDecoder(conn) + continue + } + + if resp.Error != "" { + fmt.Printf("Goroutine %d: server error %s — retry\n", index+1, resp.Error) + time.Sleep(200 * time.Millisecond) + continue + } + + // success + atomic.AddInt64(&completed, 1) + lastTask = 0 + break + } + } + }(i) + } + wg.Wait() + duration := time.Since(start) + + select { + case err := <-errCh: + return fmt.Errorf("benchmark aborted after %v: %w", duration, err) + default: + tput := float64(completed) / duration.Seconds() + fmt.Printf("completed %d tasks in %v (throughput: %.2f tasks/sec)\n", completed, duration, tput) + return nil + } +} + +// RunClient dials the server and pushes hash tasks, returning an error if the run aborts early. +//func RunClient(cfg ClientConfig) error { +// payload, err := json.Marshal(tasks.HashTaskInput{Iteration: cfg.Iterations}) +// if err != nil { +// return err +// } +// +// var sent int64 +// var completed int64 +// +// var wg sync.WaitGroup +// errCh := make(chan error, 1) +// var once sync.Once +// +// start := time.Now() +// +// for i := 0; i < cfg.Concurrency; i++ { +// wg.Add(1) +// go func(index int) { +// fmt.Printf("Starting goroutine %d\n", index+1) +// defer wg.Done() +// +// conn, err := net.Dial("tcp", cfg.Addr) +// if err != nil { +// recordError(errCh, &once, err) +// return +// } +// defer conn.Close() +// +// encoder := json.NewEncoder(conn) +// decoder := json.NewDecoder(conn) +// +// var lastTask int64 +// for { +// if lastTask == 0 { +// lastTask = atomic.AddInt64(&sent, 1) +// } +// +// if lastTask > int64(cfg.Total) { +// return +// } +// +// fmt.Printf("Goroutine %d runs tasks %d\n", index+1, int(lastTask)) +// +// tasks := tasks.Task{ +// Id: strconv.FormatInt(lastTask, 10), +// Type: tasks.HashTaskType, +// Input: payload, +// } +// +// if err := encoder.Encode(tasks); err != nil { +// fmt.Printf("Goroutine %d, met encoder.Encode error %v\n", index+1, err) +// recordError(errCh, &once, err) +// return +// } +// +// var resp clientResponse +// if err := decoder.Decode(&resp); err != nil { +// fmt.Printf("Goroutine %d, met decoder.Decode error %v\n", index+1, err) +// recordError(errCh, &once, err) +// return +// } +// if resp.Error != "" { +// fmt.Printf("Error received: %s. Retry after 2s\n", resp.Error) +// recordError(errCh, &once, errors.New(resp.Error)) +// time.Sleep(2 * time.Second) +// continue // retry +// } +// +// lastTask = 0 +// atomic.AddInt64(&completed, 1) +// } +// }(i) +// } +// +// wg.Wait() +// duration := time.Since(start) +// +// select { +// case err := <-errCh: +// return fmt.Errorf("benchmark aborted after %v: %w", duration, err) +// default: +// tput := float64(completed) / duration.Seconds() +// fmt.Printf("completed %d tasks in %v (throughput: %.2f tasks/sec)\n", completed, duration, tput) +// return nil +// } +//} + +type clientResponse struct { + ID string `json:"id"` + Result []byte `json:"result"` + Error string `json:"error"` +} + +func recordError(ch chan<- error, once *sync.Once, err error) { + once.Do(func() { + ch <- err + }) +} diff --git a/queue/runner/server.go b/queue/runner/server.go new file mode 100644 index 0000000..2d63655 --- /dev/null +++ b/queue/runner/server.go @@ -0,0 +1,42 @@ +package runner + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "vu/benchmark/queue/internal" + "vu/benchmark/queue/server" +) + +// ServerConfig collects the tunables for running the queue server. +type ServerConfig struct { + Addr string + Capacity int + Workers int +} + +// RunServer starts the TCP server and blocks until shutdown. +func RunServer(cfg ServerConfig) error { + queue := internal.NewQueue(cfg.Capacity, cfg.Workers, false) + + done := make(chan struct{}) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + close(done) + fmt.Println("signal received, shutting down") + }() + + fmt.Printf("Queue server listening on %s\n", cfg.Addr) + err := server.Serve(cfg.Addr, queue, done) + if err != nil { + fmt.Println("server error:", err) + } + + queue.Shutdown() + fmt.Println("queue drained, server exiting") + return err +} diff --git a/queue/server/server.go b/queue/server/server.go new file mode 100644 index 0000000..26bde72 --- /dev/null +++ b/queue/server/server.go @@ -0,0 +1,157 @@ +package server + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net" + "sync" + "sync/atomic" + "time" + "vu/benchmark/queue/internal" + "vu/benchmark/queue/tasks" +) + +type response struct { + ID string `json:"id"` + Result []byte `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +var waitingGoroutines int64 + +// Serve listens for TCP connections and forwards incoming tasks to the queue. +func Serve(addr string, queue internal.IQueue, done <-chan struct{}) error { + listener, err := net.Listen("tcp", addr) + if err != nil { + return err + } + defer listener.Close() + + var wg sync.WaitGroup + + // Stop accepting new connections when shutdown is signaled. + go func() { + <-done + listener.Close() + }() + + go func() { + for { + select { + case <-done: + { + break + } + default: + time.Sleep(5 * time.Second) + fmt.Println("Goroutines count: ", atomic.LoadInt64(&waitingGoroutines)) + } + } + }() + + for { + conn, err := listener.Accept() + if err != nil { + select { + case <-done: + wg.Wait() + return nil + default: + } + + if ne, ok := err.(net.Error); ok && ne.Temporary() { + fmt.Println("temporary accept error:", err) + continue + } + + wg.Wait() + return err + } + + wg.Add(1) + go func(c net.Conn) { + idx := atomic.AddInt64(&waitingGoroutines, 1) + defer func() { + wg.Done() + fmt.Printf("Goroutine %d exits\n", idx+1) + atomic.AddInt64(&waitingGoroutines, -1) + }() + fmt.Printf("Goroutine %d accpet connection\n", idx+1) + handleConnection(c, queue, done) + }(conn) + } +} + +func handleConnection(conn net.Conn, queue internal.IQueue, done <-chan struct{}) { + defer conn.Close() + + decoder := json.NewDecoder(conn) + encoder := json.NewEncoder(conn) + + // Results coming back from workers + results := make(chan response, 16) + + // Writer goroutine + writeDone := make(chan struct{}) + go func() { + defer close(writeDone) + for resp := range results { + encoder.Encode(resp) + } + }() + + for { + var task tasks.Task + + // Detect shutdown + select { + case <-done: + // Tell writer goroutine to exit + close(results) + <-writeDone + return + default: + } + + // Read next tasks + if err := decoder.Decode(&task); err != nil { + if errors.Is(err, io.EOF) { + // client closed connection normally + close(results) + <-writeDone + return + } + fmt.Printf("decode error from %s: %v\n", conn.RemoteAddr(), err) + + // send error to client before closing + results <- response{Error: err.Error()} + close(results) + <-writeDone + return + } + + ch, err := queue.Put(&task) + if err != nil { + results <- response{ID: task.Id, Error: err.Error()} + continue + } + + // Spawn worker response waiters + go func(id string, workerCh <-chan internal.Output) { + output := <-workerCh + resp := response{ID: id, Result: output.Res} + if output.Err != nil { + resp.Error = output.Err.Error() + resp.Result = nil + } + + // Avoid panic if `results` is already closed during shutdown + select { + case results <- resp: + case <-done: + } + }(task.Id, ch) + } +} diff --git a/queue/tasks/base.go b/queue/tasks/base.go new file mode 100644 index 0000000..7815daf --- /dev/null +++ b/queue/tasks/base.go @@ -0,0 +1,82 @@ +package tasks + +import ( + "crypto/sha256" + "encoding/json" + "fmt" +) + +const ( + SumTaskType = "sum" + HashTaskType = "hash" + BurnCPUTaskType = "BurnCPUTask" +) + +type Task struct { + Id string `json:"id"` + Type string `json:"type"` + Input []byte `json:"input"` +} + +type SumTaskInput struct { + A int `json:"a"` + B int `json:"b"` +} + +type SumTaskOutput struct { + Res int `json:"res"` +} + +func SumTask(input []byte) ([]byte, error) { + inputData := SumTaskInput{} + err := json.Unmarshal(input, &inputData) + if err != nil { + fmt.Println("Unmarshal error:", err) + return nil, err + } + + res := SumTaskOutput{Res: inputData.A + inputData.B} + return json.Marshal(res) +} + +type HashTaskInput struct { + Iteration int `json:"iteration"` +} + +type HashTaskOutput struct { + Res string `json:"res"` +} + +func HashTask(iterations int) []byte { + data := []byte("benchmark") + var sum [32]byte + + for i := 0; i < iterations; i++ { + sum = sha256.Sum256(data) + } + + return sum[:] +} + +type BurnCPUTaskInput struct { + Iteration int `json:"iteration"` +} + +type BurnCPUTaskOutput struct { + Res int `json:"res"` +} + +func BurnCPUTask(input []byte) ([]byte, error) { + inputType := BurnCPUTaskInput{} + if err := json.Unmarshal(input, &inputType); err != nil { + return nil, err + } + + var x uint64 = 1 + for i := 0; i < inputType.Iteration; i++ { + x = x*1664525 + 1013904223 // LCG, prevents optimization + } + + bytes, _ := json.Marshal(inputType) + return bytes, nil +} diff --git a/queue/trace.out b/queue/trace.out new file mode 100644 index 0000000000000000000000000000000000000000..25d15195354bb4b9dc1f0785a70fa15d76952b3f GIT binary patch literal 135550 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�sGN=8?PR)ADwyEBKD(OAL?2ZLV3%4a$7pVeyoR( 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

R*Eub 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 literal 0 HcmV?d00001