Skip to content

Instantly share code, notes, and snippets.

@lateefj
Last active October 18, 2019 23:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lateefj/dfff4399996928c4e18c78f5f7a98f19 to your computer and use it in GitHub Desktop.
Save lateefj/dfff4399996928c4e18c78f5f7a98f19 to your computer and use it in GitHub Desktop.
Simple Shared Map Example In Go With Timeouts
// 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))
}
}
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