-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Consistent hashing: consistently retry another host (#119)
* Consistent hashing: consistently retry another host This implements a retry policy to consistently retry another host within the SRV set if we get a failure talking to the original host. We still use the original fallback strategy if this retry fails. (Should we?) * extract consistent hashing code This extracts the consistent hashing code into a new package and writes more tests for it. This removes a bunch of duplication from ConsistentHashingMode. As a consequence this jumbles the consistent hashing algorithm (because I've embedded the Key into a top-level CacheKey struct which includes Attempt for retry support). At this stage this is safe because nothing live is using it. * refactor rename method avoid implicit return remove unneeded conditional on m.DomainsToCache * lint
- Loading branch information
1 parent
5eceeda
commit 3e641a5
Showing
4 changed files
with
251 additions
and
52 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Package consistent implements consistent hashing for cache nodes. | ||
package consistent | ||
|
||
import ( | ||
"fmt" | ||
"slices" | ||
|
||
"github.com/dgryski/go-jump" | ||
"github.com/mitchellh/hashstructure/v2" | ||
) | ||
|
||
type cacheKey struct { | ||
Key any | ||
Attempt int | ||
} | ||
|
||
// HashBucket returns a bucket from [0,buckets). If you want to implement a | ||
// retry, you can pass previousBuckets, which indicates buckets which must be | ||
// avoided in the output. HashBucket will modify the previousBuckets slice by | ||
// sorting it. | ||
func HashBucket(key any, buckets int, previousBuckets ...int) (int, error) { | ||
if len(previousBuckets) >= buckets { | ||
return -1, fmt.Errorf("No more buckets left: %d buckets available but %d already attempted", buckets, previousBuckets) | ||
} | ||
// we set IgnoreZeroValue so that we can add fields to the hash key | ||
// later without breaking things. | ||
// note that it's not safe to share a HashOptions so we create a fresh one each time. | ||
hashopts := &hashstructure.HashOptions{IgnoreZeroValue: true} | ||
hash, err := hashstructure.Hash(cacheKey{Key: key, Attempt: len(previousBuckets)}, hashstructure.FormatV2, hashopts) | ||
if err != nil { | ||
return -1, fmt.Errorf("error calculating hash of key: %w", err) | ||
} | ||
|
||
// jump is an implementation of Google's Jump Consistent Hash. | ||
// | ||
// See http://arxiv.org/abs/1406.2294 for details. | ||
bucket := int(jump.Hash(hash, buckets-len(previousBuckets))) | ||
slices.Sort(previousBuckets) | ||
for _, prev := range previousBuckets { | ||
if bucket >= prev { | ||
bucket++ | ||
} | ||
} | ||
return bucket, 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 |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package consistent_test | ||
|
||
import ( | ||
"slices" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/replicate/pget/pkg/consistent" | ||
) | ||
|
||
func TestHashingDoesNotChangeWhenZeroValueFieldsAreAdded(t *testing.T) { | ||
a, err := consistent.HashBucket(struct{}{}, 1024) | ||
require.NoError(t, err) | ||
b, err := consistent.HashBucket(struct{ I int }{}, 1024) | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, a, b) | ||
} | ||
|
||
func TestRetriesScatterBuckets(t *testing.T) { | ||
// This test is tricky! We want an example of hash keys which map to the | ||
// same bucket, but after one retry map to different buckets. | ||
// | ||
// These two keys happen to have this property for 10 buckets: | ||
strA := "abcdefg" | ||
strB := "1234567" | ||
a, err := consistent.HashBucket(strA, 10) | ||
require.NoError(t, err) | ||
b, err := consistent.HashBucket(strB, 10) | ||
require.NoError(t, err) | ||
|
||
// strA and strB to map to the same bucket | ||
require.Equal(t, a, b) | ||
|
||
aRetry, err := consistent.HashBucket(strA, 10, a) | ||
require.NoError(t, err) | ||
bRetry, err := consistent.HashBucket(strB, 10, b) | ||
require.NoError(t, err) | ||
|
||
// but after retry they map to different buckets | ||
assert.NotEqual(t, aRetry, bRetry) | ||
} | ||
|
||
func FuzzRetriesMostNotRepeatIndices(f *testing.F) { | ||
f.Add("test.replicate.delivery", 5) | ||
f.Add("test.replicate.delivery", 0) | ||
f.Fuzz(func(t *testing.T, key string, excessBuckets int) { | ||
if excessBuckets < 0 { | ||
t.Skip("invalid value") | ||
} | ||
attempts := 20 | ||
buckets := attempts + excessBuckets | ||
if buckets < 0 { | ||
t.Skip("integer overflow") | ||
} | ||
previous := []int{} | ||
for i := 0; i < attempts; i++ { | ||
next, err := consistent.HashBucket(key, buckets, previous...) | ||
require.NoError(t, err) | ||
|
||
// we must be in range | ||
assert.Less(t, next, buckets) | ||
assert.GreaterOrEqual(t, next, 0) | ||
|
||
// we shouldn't repeat any previous value | ||
assert.NotContains(t, previous, next) | ||
|
||
previous = append(previous, next) | ||
slices.Sort(previous) | ||
} | ||
}) | ||
} |
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.