Add batcher task
This commit is contained in:
parent
b19a92ab27
commit
0975bc3736
4 changed files with 159 additions and 0 deletions
14
batcher/README.md
Normal file
14
batcher/README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# batcher
|
||||||
|
|
||||||
|
`*slow.Value` - это аналог `atomic.Value`, с двумя ограничениями:
|
||||||
|
- Вызов `Load()` всегда занимает не меньше 1ms
|
||||||
|
- Нельзя делать больше одного вызова `Load()` в один момент времени.
|
||||||
|
|
||||||
|
Реализуйте `*Batcher`, который оборачивает `*slow.Value` и ускоряет чтения за счёт батчинга.
|
||||||
|
|
||||||
|
Например, если 1000 горутин одновременно сделают вызов `(*Batcher).Load()`, то можно прочитать
|
||||||
|
значение один раз и раздать всем ожидающим горутинам. Это будет в 1000 раз быстрее,
|
||||||
|
чем вызывать `(*slow.Value).Load()` напрямую в каждой горутине.
|
||||||
|
|
||||||
|
Ваша реализация не должна создавать _stale read_. Тоесть, `Load()` всегда должен возвращать значение
|
||||||
|
последнего `Store()` на момент начала вызова `Load()`.
|
12
batcher/batcher.go
Normal file
12
batcher/batcher.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// +build !solution
|
||||||
|
|
||||||
|
package batcher
|
||||||
|
|
||||||
|
import "gitlab.com/slon/shad-go/batcher/slow"
|
||||||
|
|
||||||
|
type Batcher struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBatcher(v *slow.Value) *Batcher {
|
||||||
|
return nil
|
||||||
|
}
|
97
batcher/batcher_test.go
Normal file
97
batcher/batcher_test.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package batcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gitlab.com/slon/shad-go/batcher/slow"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSimple(t *testing.T) {
|
||||||
|
var value slow.Value
|
||||||
|
b := NewBatcher(&value)
|
||||||
|
|
||||||
|
value.Store(1)
|
||||||
|
require.Equal(t, 1, b.Load())
|
||||||
|
require.Equal(t, 1, value.Load())
|
||||||
|
|
||||||
|
value.Store(2)
|
||||||
|
require.Equal(t, 2, b.Load())
|
||||||
|
require.Equal(t, 2, value.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaleRead(t *testing.T) {
|
||||||
|
const (
|
||||||
|
N = 100
|
||||||
|
K = 100
|
||||||
|
M = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
var value slow.Value
|
||||||
|
b := NewBatcher(&value)
|
||||||
|
|
||||||
|
var counter int32
|
||||||
|
value.Store(counter)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * time.Duration(i/N))
|
||||||
|
for j := 0; j < K; j++ {
|
||||||
|
counterValue := atomic.LoadInt32(&counter)
|
||||||
|
batcherValue := b.Load().(int32)
|
||||||
|
|
||||||
|
if batcherValue < counterValue {
|
||||||
|
t.Errorf("load returned old value: counter=%d, batcher=%d", counterValue, batcherValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < M*K; i++ {
|
||||||
|
// value is always greater than counter
|
||||||
|
value.Store(int32(i))
|
||||||
|
atomic.StoreInt32(&counter, int32(i))
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond / M)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpeed(t *testing.T) {
|
||||||
|
const (
|
||||||
|
N = 100
|
||||||
|
K = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
var value slow.Value
|
||||||
|
b := NewBatcher(&value)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for i := 0; i < K; i++ {
|
||||||
|
b.Load()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
require.Truef(t, time.Since(start) < time.Second, "batching it too slow")
|
||||||
|
}
|
36
batcher/slow/value.go
Normal file
36
batcher/slow/value.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// +build !change
|
||||||
|
|
||||||
|
package slow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Value struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
value interface{}
|
||||||
|
readRunning int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Value) Load() interface{} {
|
||||||
|
if atomic.SwapInt32(&s.readRunning, 1) == 1 {
|
||||||
|
panic("another load is running")
|
||||||
|
}
|
||||||
|
defer atomic.StoreInt32(&s.readRunning, 0)
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
value := s.value
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Value) Store(v interface{}) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.value = v
|
||||||
|
}
|
Loading…
Reference in a new issue