Last active
October 18, 2019 23:26
-
-
Save lateefj/dfff4399996928c4e18c78f5f7a98f19 to your computer and use it in GitHub Desktop.
Simple Shared Map Example In Go With Timeouts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Example on sharing a map with multiple goroutines | |
package main | |
import ( | |
"errors" | |
"fmt" | |
"sync" | |
"time" | |
) | |
type SyncMap struct { | |
Mutex sync.RWMutex // Read Write Mutex to allow for multiple readers | |
Map map[string]time.Time // Example data map | |
} | |
func NewSyncMap() *SyncMap { | |
return &SyncMap{Mutex: sync.RWMutex{}, Map: make(map[string]time.Time)} | |
} | |
// put is only called by other functions on the struct. Reducing the write locks to this one function. | |
func (sm *SyncMap) put(key string, value time.Time, notify chan time.Time) { | |
// Upfront make sure the lock gets unlocked before exit of the function call | |
defer sm.Mutex.Unlock() | |
// Keep any other readers or writers waiting | |
sm.Mutex.Lock() | |
// Set the value | |
sm.Map[key] = value | |
// Internally we use channels to send response back to support timeouts | |
notify <- value | |
} | |
// delete is only called by other functions on the struct. Reducing the write locks to a second function. | |
func (sm *SyncMap) delete(key string, notify chan string) { | |
// Defer unlock up front that should be tied to the locking call | |
defer sm.Mutex.Unlock() | |
// Keep any other readers or writers waiting | |
sm.Mutex.Lock() | |
// Set the value | |
delete(sm.Map, key) | |
// Internally we use channels to send response back to support timeouts | |
notify <- key | |
} | |
// get centralizes read locks. Since many readers can read concurrently this provides central lock code for that. | |
func (sm *SyncMap) get(key string, result chan *time.Time) { | |
// Unlock at the end and keeps writers from writing while this is locked | |
defer sm.Mutex.RUnlock() | |
// Reads can be concurrent so this is a read lock and should provide for great read performance | |
sm.Mutex.RLock() | |
// Internally we use channels to send response back to support timeouts | |
v, exists := sm.Map[key] | |
if exists { | |
result <- &v | |
} else { // If doesn't exist in the map return nil | |
result <- nil | |
} | |
} | |
// Get is a wait for eva' eva which is tragic if we care about time | |
func (sm *SyncMap) Get(key string) *time.Time { | |
// Build a channel for the response | |
result := make(chan *time.Time, 1) | |
// Retrieve it | |
sm.get(key, result) | |
// Return the result | |
return <-result | |
} | |
// Pet is a wait for eva' eva' to set a value in the map | |
func (sm *SyncMap) Put(key string, value time.Time) { | |
result := make(chan time.Time, 1) | |
sm.put(key, value, result) | |
<-result | |
} | |
// Delete is a wait for eve' eva' delete key/value pair in the map | |
func (sm *SyncMap) Delete(key string) { | |
result := make(chan string, 1) | |
sm.delete(key, result) | |
<-result | |
} | |
// GetUntil gets a value until the timeout is triggered | |
func (sm *SyncMap) GetUntil(key string, timeout time.Duration) (*time.Time, error) { | |
result := make(chan *time.Time, 1) | |
sm.get(key, result) | |
select { | |
case v := <-result: | |
return v, nil | |
case <-time.After(timeout): | |
return nil, errors.New(fmt.Sprintf("Timeout exceeded for key %s and time limit %v", key, timeout)) | |
} | |
} | |
// PutUntil puts a value until the timeout is reaged | |
func (sm *SyncMap) PutUntil(key string, value time.Time, timeout time.Duration) error { | |
result := make(chan time.Time, 1) | |
sm.put(key, value, result) | |
select { | |
case <-result: | |
return nil | |
case <-time.After(timeout): | |
return errors.New(fmt.Sprintf("Timeout exceeded for PutUntil with key %s, value %v and timeout %v", key, value, timeout)) | |
} | |
} | |
// DeleteUntil deletes a value until a timeout is reached | |
func (sm *SyncMap) DeleteUntil(key string, timeout time.Duration) error { | |
result := make(chan string, 1) | |
sm.delete(key, result) | |
select { | |
case <-result: | |
return nil | |
case <-time.After(timeout): | |
return errors.New(fmt.Sprintf("Timeout exceeded for DeleteUntil with key %s and timeout %v", key, timeout)) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"testing" | |
"time" | |
) | |
func TestForeverWait(t *testing.T) { | |
size := 100 | |
sm := NewSyncMap() | |
k := "test" | |
v := time.Now() | |
// Put a bunch of data in | |
for x := 0; x < size; x++ { | |
sm.Put(fmt.Sprintf("%s-%d", k, x), v) | |
} | |
// Make sure all the data exists | |
for x := 0; x < size; x++ { | |
key := fmt.Sprintf("%s-%d", k, x) | |
value := sm.Get(key) | |
if value == nil { | |
t.Fatalf("Failed to find key %s", key) | |
} | |
if !value.Equal(v) { | |
t.Fatalf("Expected value %v for key %s to be %v", value, key, v) | |
} | |
} | |
// Delete all the data | |
for x := 0; x < size; x++ { | |
key := fmt.Sprintf("%s-%d", k, x) | |
sm.Delete(key) | |
} | |
// After all deleted expect it to be gone | |
for x := 0; x < size; x++ { | |
key := fmt.Sprintf("%s-%d", k, x) | |
value := sm.Get(key) | |
if value != nil { | |
t.Fatalf("Expected key %s to be deleted but was found with value %v", key, value) | |
} | |
} | |
} | |
func TestTimeout(t *testing.T) { | |
sm := NewSyncMap() | |
size := 100 | |
k := "test" | |
v := time.Now() | |
timeout := 1 * time.Nanosecond | |
// Put a bunch of data in | |
for x := 0; x < size; x++ { | |
err := sm.PutUntil(fmt.Sprintf("%s-%d", k, x), v, timeout) | |
if err != nil { | |
t.Errorf("Put Timeout %s", err) | |
} | |
} | |
// Make sure all the data exists | |
for x := 0; x < size; x++ { | |
key := fmt.Sprintf("%s-%d", k, x) | |
value, err := sm.GetUntil(key, timeout) | |
if err != nil { | |
t.Errorf("Get Timeout %s", err) | |
} | |
if value == nil { | |
t.Fatalf("Failed to find key %s", key) | |
} | |
if !value.Equal(v) { | |
t.Fatalf("Expected value %v for key %s to be %v", value, key, v) | |
} | |
} | |
// Delete all the data | |
for x := 0; x < size; x++ { | |
key := fmt.Sprintf("%s-%d", k, x) | |
err := sm.DeleteUntil(key, timeout) | |
if err != nil { | |
t.Errorf("Delete timeout%s", err) | |
} | |
} | |
// After all deleted expect it to be gone | |
for x := 0; x < size; x++ { | |
key := fmt.Sprintf("%s-%d", k, x) | |
value, err := sm.GetUntil(key, timeout) | |
if err != nil { | |
t.Errorf("Get Timeout %s", err) | |
} | |
if value != nil { | |
t.Fatalf("Expected key %s to be deleted but was found with value %v", key, value) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment