diff --git a/keylock/README.md b/keylock/README.md new file mode 100644 index 0000000..079c9ad --- /dev/null +++ b/keylock/README.md @@ -0,0 +1,17 @@ +# keylock + +Напишите примитив синхронизации, позволяющий "лочить" строки из множества. + +```go +package keylock + +type KeyLock interface { + // LockKeys locks all keys from provided set. + // + // Upon successful completion, function guarantees that no other call with intersecting set of keys + // will finish, until unlock() is called. + // + // If cancel channel is closed, function returns immediately. + LockKeys(keys []string, cancel <-chan struct{}) (canceled bool, unlock func()) +} +``` \ No newline at end of file diff --git a/keylock/keylock.go b/keylock/keylock.go new file mode 100644 index 0000000..c8d0a43 --- /dev/null +++ b/keylock/keylock.go @@ -0,0 +1,13 @@ +// +build !solution + +package keylock + +type KeyLock struct{} + +func New() *KeyLock { + panic("implement me") +} + +func (l *KeyLock) LockKeys(keys []string, cancel <-chan struct{}) (canceled bool, unlock func()) { + panic("implement me") +} diff --git a/keylock/keylock_test.go b/keylock/keylock_test.go new file mode 100644 index 0000000..793af49 --- /dev/null +++ b/keylock/keylock_test.go @@ -0,0 +1,112 @@ +package keylock_test + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "gitlab.com/slon/shad-go/keylock" +) + +func timeout(d time.Duration) <-chan struct{} { + ch := make(chan struct{}) + go func() { + time.Sleep(d) + close(ch) + }() + return ch +} + +func TestKeyLock_Simple(t *testing.T) { + defer goleak.VerifyNone(t) + + l := keylock.New() + + locked, unlock := l.LockKeys([]string{"a", "b"}, nil) + require.True(t, locked) + + locked, _ = l.LockKeys([]string{"", "b", "c"}, timeout(time.Millisecond*10)) + require.False(t, locked) + + unlock() + + locked, _ = l.LockKeys([]string{"", "b", "c"}, nil) + require.True(t, locked) +} + +func TestKeyLock_Progress(t *testing.T) { + defer goleak.VerifyNone(t) + l := keylock.New() + + locked, unlock := l.LockKeys([]string{"a", "b"}, nil) + require.True(t, locked) + defer unlock() + + go func() { + _, _ = l.LockKeys([]string{"b", "c"}, nil) + }() + + time.Sleep(time.Millisecond * 10) + locked, _ = l.LockKeys([]string{"d"}, nil) + require.True(t, locked) +} + +func TestKeyLock_DeadlockFree(t *testing.T) { + const N = 10000 + + defer goleak.VerifyNone(t) + l := keylock.New() + + var wg sync.WaitGroup + wg.Add(3) + + checkLock := func(keys []string) { + defer wg.Done() + + for i := 0; i < N; i++ { + locked, unlock := l.LockKeys(keys, nil) + if !locked { + t.Error("spurious lock failure") + return + } + unlock() + } + } + + go checkLock([]string{"a", "b", "c"}) + go checkLock([]string{"b", "c", "a"}) + go checkLock([]string{"c", "a", "b"}) + + wg.Wait() +} + +func TestKeyLock_SingleKeyStress(t *testing.T) { + const ( + N = 1000 + G = 100 + ) + + defer goleak.VerifyNone(t) + l := keylock.New() + + var wg sync.WaitGroup + wg.Add(G) + + for i := 0; i < G; i++ { + go func() { + defer wg.Done() + + for i := 0; i < N; i++ { + locked, unlock := l.LockKeys([]string{"a"}, timeout(time.Millisecond)) + if locked { + unlock() + } + } + }() + } + + wg.Wait() +} diff --git a/keylock/speed_test.go b/keylock/speed_test.go new file mode 100644 index 0000000..783e200 --- /dev/null +++ b/keylock/speed_test.go @@ -0,0 +1,63 @@ +package keylock + +import ( + "math/rand" + "strconv" + "sync" + "testing" +) + +func BenchmarkMutex_Baseline(b *testing.B) { + var mu sync.Mutex + + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + mu.Lock() + _ = 0 + mu.Unlock() + } + }) +} + +func BenchmarkKeyLock_SingleKey(b *testing.B) { + l := New() + + keys := []string{"a"} + + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, unlock := l.LockKeys(keys, nil) + unlock() + } + }) +} + +func BenchmarkKeyLock_MultipleKeys(b *testing.B) { + l := New() + + keys := []string{"a", "b", "c", "d"} + + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, unlock := l.LockKeys(keys, nil) + unlock() + } + }) +} + +func BenchmarkKeyLock_DifferentKeys(b *testing.B) { + l := New() + + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + keys := []string{strconv.Itoa(rand.Int())} + + for pb.Next() { + _, unlock := l.LockKeys(keys, nil) + unlock() + } + }) +}