This repository has been archived by the owner on Dec 27, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Consumer cached proxy implementation (#41)
* Consumer cached proxy implementation * Code review fixes * Code review fixes: * Unit-testing FIRST principal * Fixed bug with loadKeys for maxCount > bufferSize
- Loading branch information
1 parent
d9016af
commit 24d5945
Showing
9 changed files
with
315 additions
and
10 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package keys | ||
|
||
import ( | ||
"errors" | ||
) | ||
|
||
var _ Consumer = (*ConsumerCached)(nil) | ||
|
||
type bufferEntry struct { | ||
key string | ||
err error | ||
} | ||
|
||
type ConsumerCached struct { | ||
delegate Consumer | ||
bufferSize int | ||
buffer chan bufferEntry | ||
} | ||
|
||
func (p ConsumerCached) ConsumeInBatch(maxCount uint) ([]string, error) { | ||
keys := make([]string, 0, maxCount) | ||
|
||
for ; maxCount > 0; maxCount-- { | ||
// there is probability to get the list of keys with the size less than bufferSize | ||
// in this case we listen to done channel to perform loop break | ||
done := make(chan bool) | ||
|
||
// we should load new keys as soon as our buffer is empty | ||
if len(p.buffer) == 0 { | ||
go func() { | ||
p.loadKeys() | ||
done <- true | ||
}() | ||
} | ||
|
||
select { | ||
case entry := <-p.buffer: | ||
if entry.err != nil { | ||
return keys, entry.err | ||
} | ||
|
||
keys = append(keys, entry.key) | ||
case <-done: | ||
break | ||
} | ||
|
||
} | ||
|
||
return keys, nil | ||
} | ||
|
||
func (p ConsumerCached) loadKeys() { | ||
keys, err := p.delegate.ConsumeInBatch(uint(p.bufferSize)) | ||
|
||
if err != nil { | ||
p.buffer <- bufferEntry{ | ||
key: "", | ||
err: err, | ||
} | ||
} | ||
|
||
for _, key := range keys { | ||
p.buffer <- bufferEntry{ | ||
key: key, | ||
err: nil, | ||
} | ||
} | ||
} | ||
|
||
// NewCachedConsumer returns the cached proxy implementation of Consumer interface | ||
func NewCachedConsumer(bufferSize int, delegate Consumer) (ConsumerCached, error) { | ||
if bufferSize < 1 { | ||
return ConsumerCached{}, errors.New("buffer size can't be less than 1") | ||
} | ||
|
||
return ConsumerCached{ | ||
bufferSize: bufferSize, | ||
buffer: make(chan bufferEntry, bufferSize), | ||
delegate: delegate, | ||
}, nil | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,144 @@ | ||
package keys | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"github.com/byliuyang/kgs/app/entity" | ||
"github.com/byliuyang/kgs/app/usecase/repo/repotest" | ||
|
||
"github.com/byliuyang/app/mdtest" | ||
) | ||
|
||
func TestCachedConsumer(t *testing.T) { | ||
keys := []entity.Key{ | ||
"aaaa", "aaab", "aaac", "aaad", "aaae", "aaaf", "aaag", "aaah", | ||
"baaa", "baab", "baac", "baad", "baae", "baaf", "baag", "baah", | ||
"caaa", "caab", "caac", "caad", "caae", "caaf", "caag", "caah", | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
expected fakeConsumerResult | ||
skipBefore uint | ||
count uint | ||
}{ | ||
{ | ||
name: "should load keys and return three items from the buffer #1", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"aaaa", "aaab", "aaac"}, | ||
err: nil, | ||
}, | ||
skipBefore: 0, | ||
count: 3, | ||
}, | ||
{ | ||
name: "should load keys twice and return the entire buffer #2", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"baaa", "baab", "baac", "baad", "baae", "baaf", "baag", "baah"}, | ||
err: nil, | ||
}, | ||
skipBefore: 8, | ||
count: 8, | ||
}, | ||
{ | ||
name: "should return two items from the buffer #1", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"aaad", "aaae"}, | ||
err: nil, | ||
}, | ||
skipBefore: 3, | ||
count: 2, | ||
}, | ||
{ | ||
name: "should return one item from the buffer #1", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"aaaf"}, | ||
err: nil, | ||
}, | ||
skipBefore: 5, | ||
count: 1, | ||
}, | ||
{ | ||
name: "should return the first two items from the buffer #1, load new keys and return one key from the buffer #2", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"aaag", "aaah", "baaa"}, | ||
err: nil, | ||
}, | ||
skipBefore: 6, | ||
count: 3, | ||
}, | ||
{ | ||
name: "should return the first seven items from the buffer #2, load new keys and return three items from the buffer #3", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"baab", "baac", "baad", "baae", "baaf", "baag", "baah", "caaa", "caab", "caac"}, | ||
err: nil, | ||
}, | ||
skipBefore: 9, | ||
count: 10, | ||
}, | ||
{ | ||
name: "should return four items from the buffer #3", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"caad", "caae", "caaf", "caag"}, | ||
err: nil, | ||
}, | ||
skipBefore: 19, | ||
count: 4, | ||
}, | ||
{ | ||
name: "should return the last item from the buffer #3 and return an error", | ||
expected: fakeConsumerResult{ | ||
keys: []string{"caah"}, | ||
err: fakeConsumerError, | ||
}, | ||
skipBefore: 23, | ||
count: 10, | ||
}, | ||
{ | ||
name: "should return empty list because there are no more keys", | ||
expected: fakeConsumerResult{ | ||
keys: []string{}, | ||
err: nil, | ||
}, | ||
skipBefore: 24, | ||
count: 10, | ||
}, | ||
} | ||
|
||
for _, testCase := range testCases { | ||
t.Run(testCase.name, func(t *testing.T) { | ||
availableKeysRepo := repotest.NewAvailableKeyFake() | ||
allocatedKeysRepo := repotest.NewAllocatedKeyFake() | ||
|
||
for _, key := range keys { | ||
err := availableKeysRepo.Create(key) | ||
mdtest.Equal(t, nil, err) | ||
} | ||
|
||
mockConsumer := NewConsumerPersist( | ||
&availableKeysRepo, | ||
&allocatedKeysRepo, | ||
) | ||
|
||
consumer, err := NewCachedConsumer(8, mockConsumer) | ||
mdtest.Equal(t, err, nil) | ||
|
||
_, err = consumer.ConsumeInBatch(testCase.skipBefore) | ||
mdtest.Equal(t, err, nil) | ||
|
||
allocatedKeysRepo.FakeError(testCase.expected.err) | ||
actual, err := consumer.ConsumeInBatch(testCase.count) | ||
|
||
mdtest.Equal(t, testCase.expected.keys, actual) | ||
mdtest.Equal(t, testCase.expected.err, err) | ||
}) | ||
} | ||
} | ||
|
||
var fakeConsumerError = errors.New("some trouble with delegate") | ||
|
||
type fakeConsumerResult struct { | ||
keys []string | ||
err error | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package repotest | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/byliuyang/kgs/app/entity" | ||
"github.com/byliuyang/kgs/app/usecase/repo" | ||
) | ||
|
||
var _ repo.AllocatedKey = (*AllocatedKeyFake)(nil) | ||
|
||
type AllocatedKeyFake struct { | ||
keys map[entity.Key]struct{} | ||
err error | ||
} | ||
|
||
func (a AllocatedKeyFake) CreateInBatch(keys []entity.Key) error { | ||
for _, key := range keys { | ||
if err := a.create(key); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return a.err | ||
} | ||
|
||
func (a *AllocatedKeyFake) FakeError(err error) { | ||
a.err = err | ||
} | ||
|
||
func (a AllocatedKeyFake) create(key entity.Key) error { | ||
if _, ok := a.keys[key]; ok { | ||
return fmt.Errorf("key exists: %s", string(key)) | ||
} | ||
|
||
a.keys[key] = struct{}{} | ||
|
||
return nil | ||
} | ||
|
||
func NewAllocatedKeyFake() AllocatedKeyFake { | ||
return AllocatedKeyFake{ | ||
keys: make(map[entity.Key]struct{}), | ||
} | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package provider | ||
|
||
import ( | ||
"github.com/byliuyang/kgs/app/usecase/keys" | ||
) | ||
|
||
// CacheSize specifies the size of the local cache for fetched keys | ||
type CacheSize int | ||
|
||
// NewConsumer creates a buffered cached keys Consumer | ||
func NewConsumer(bufferSize CacheSize, delegate keys.ConsumerPersist) (keys.ConsumerCached, error) { | ||
return keys.NewCachedConsumer(int(bufferSize), delegate) | ||
} |
This file contains 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
Oops, something went wrong.