diff --git a/05-costly-connections-with-unsafe-storage/README.md b/05-costly-connections-with-unsafe-storage/README.md new file mode 100644 index 0000000..fe98669 --- /dev/null +++ b/05-costly-connections-with-unsafe-storage/README.md @@ -0,0 +1,13 @@ +# Costly Connections With Unsafe Storage + +Implement a function `sendAndSave(requests []string, maxConn int)` that: + +* Manages up to `maxConn` concurrent connections. +* Ensures connections are properly established before sending requests. +* Sends multiple requests using the Send method and saves responses using `UnsafeStorage`. +* Prevents corrupt data storage by ensuring safe handling of `UnsafeStorage.Save`. +* Properly handles locking and concurrency to avoid race conditions and deadlocks. + +## Tags +`Concurrency` + diff --git a/05-costly-connections-with-unsafe-storage/solution/solution.go b/05-costly-connections-with-unsafe-storage/solution/solution.go new file mode 100644 index 0000000..35f2c47 --- /dev/null +++ b/05-costly-connections-with-unsafe-storage/solution/solution.go @@ -0,0 +1,109 @@ +package main + +import ( + "errors" + "sync" + "time" +) + +type Connection struct { + ready bool + sync.Mutex +} + +func NewConnection() *Connection { + return &Connection{} +} + +func (c *Connection) Connect() { + c.Lock() + defer c.Unlock() + + <-time.After(2 * time.Second) + c.ready = true +} + +func (c *Connection) Disconnect() { + c.Lock() + defer c.Unlock() + + <-time.After(2 * time.Second) + c.ready = false +} + +func (c *Connection) Send(req string) (string, error) { + c.Lock() + defer c.Unlock() + + if !c.ready { + return "", errors.New("connection is not ready") + } + + // Sending request + <-time.After(1 * time.Second) + return "resp:" + req, nil +} + +type UnsafeStorage struct { + sem chan struct{} + data []string + sync.Mutex +} + +func NewUnsafeStorage() *UnsafeStorage { + return &UnsafeStorage{sem: make(chan struct{}, 1)} +} + +func (s *UnsafeStorage) Save(data string) { + select { + case s.sem <- struct{}{}: + <-s.sem + default: + data = "" // corrupt string + } + <-time.After(1 * time.Second) + + s.Lock() + defer s.Unlock() + s.data = append(s.data, data) +} + +var storage = NewUnsafeStorage() + +// sendAndSave should send all requests concurrently using at most `maxConn` simultaneous connections. +// Responses must be saved using UnsafeStorage. Be careful: UnsafeStorage is not safe for concurrent use. +func sendAndSave(requests []string, maxConn int) { + var wg sync.WaitGroup + wg.Add(len(requests)) + + reqCh, respCh := make(chan string, len(requests)), make(chan string, len(requests)) + for _, req := range requests { + reqCh <- req + } + close(reqCh) + + for range maxConn { + go func() { + conn := NewConnection() + conn.Connect() + defer conn.Disconnect() + + for req := range reqCh { + resp, err := conn.Send(req) + if err == nil { + respCh <- resp + } + wg.Done() + } + }() + } + + go func() { + wg.Wait() + close(respCh) + }() + + for resp := range respCh { + storage.Save(resp) + } +} diff --git a/05-costly-connections-with-unsafe-storage/task.go b/05-costly-connections-with-unsafe-storage/task.go new file mode 100644 index 0000000..2459ac6 --- /dev/null +++ b/05-costly-connections-with-unsafe-storage/task.go @@ -0,0 +1,76 @@ +package main + +import ( + "errors" + "sync" + "time" +) + +type Connection struct { + ready bool + sync.Mutex +} + +func NewConnection() *Connection { + return &Connection{} +} + +func (c *Connection) Connect() { + c.Lock() + defer c.Unlock() + + <-time.After(2 * time.Second) + c.ready = true +} + +func (c *Connection) Disconnect() { + c.Lock() + defer c.Unlock() + + <-time.After(2 * time.Second) + c.ready = false +} + +func (c *Connection) Send(req string) (string, error) { + c.Lock() + defer c.Unlock() + + if !c.ready { + return "", errors.New("connection is not ready") + } + + // Sending request + <-time.After(1 * time.Second) + return "resp:" + req, nil +} + +type UnsafeStorage struct { + sem chan struct{} + data []string + sync.Mutex +} + +func NewUnsafeStorage() *UnsafeStorage { + return &UnsafeStorage{sem: make(chan struct{}, 1)} +} + +func (s *UnsafeStorage) Save(data string) { + select { + case s.sem <- struct{}{}: + <-s.sem + default: + data = "" // corrupt string + } + <-time.After(1 * time.Second) + + s.Lock() + defer s.Unlock() + s.data = append(s.data, data) +} + +var storage = NewUnsafeStorage() + +// sendAndSave should send all requests concurrently using at most `maxConn` simultaneous connections. +// Responses must be saved using UnsafeStorage. Be careful: UnsafeStorage is not safe for concurrent use. +func sendAndSave(requests []string, maxConn int) { +} diff --git a/05-costly-connections-with-unsafe-storage/task_test.go b/05-costly-connections-with-unsafe-storage/task_test.go new file mode 100644 index 0000000..a05d84f --- /dev/null +++ b/05-costly-connections-with-unsafe-storage/task_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "testing" + "time" +) + +func TestSendAndSave(t *testing.T) { + requests := []string{"req1", "req2", "req3", "req4", "req5"} + maxConn := 2 + expecTime := 8 * time.Second + + start := time.Now() + sendAndSave(requests, maxConn) + execTime := time.Since(start).Round(time.Second) + + if len(storage.data) != len(requests) { + t.Errorf("Expected %d saved items, got %d", len(requests), len(storage.data)) + } + + for i, data := range storage.data { + if data == "" { + t.Errorf("data at index %d is corrupted (empty string)", i) + } + } + + if execTime > expecTime { + t.Errorf("func takes too long expected: %d seconds, got %d seconds", expecTime, execTime) + } +} diff --git a/README.md b/README.md index 44d08b5..2f55f0e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Here is a list of the problems available in the repository. Problems are organiz * [Equivalent Binary Trees](02-equivalent-binary-trees/) * [Web Crawler](03-web-crawler/) * [Non-Blocking Cache](04-non-blocking-cache/) +* [Costly Connection With Unsafe Storage](05-costly-connections-with-unsafe-storage/) ## Contributing