Skip to content

Commit 12412e6

Browse files
authored
Store Gateway: Add pre add block ownership check (#6483)
* pre add block hook Signed-off-by: Ben Ye <[email protected]> * fix go sum Signed-off-by: Ben Ye <[email protected]> * fix lint Signed-off-by: Ben Ye <[email protected]> --------- Signed-off-by: Ben Ye <[email protected]>
1 parent b3a7a55 commit 12412e6

File tree

5 files changed

+239
-0
lines changed

5 files changed

+239
-0
lines changed

pkg/storegateway/bucket_stores.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,11 @@ func (u *BucketStores) getOrCreateStore(userID string) (*store.BucketStore, erro
627627
store.WithLazyExpandedPostings(u.cfg.BucketStore.LazyExpandedPostingsEnabled),
628628
store.WithPostingGroupMaxKeySeriesRatio(u.cfg.BucketStore.LazyExpandedPostingGroupMaxKeySeriesRatio),
629629
store.WithDontResort(true), // Cortex doesn't need to resort series in store gateway.
630+
store.WithBlockLifecycleCallback(&shardingBlockLifecycleCallbackAdapter{
631+
userID: userID,
632+
strategy: u.shardingStrategy,
633+
logger: userLogger,
634+
}),
630635
}
631636
if u.logLevel.String() == "debug" {
632637
bucketStoreOpts = append(bucketStoreOpts, store.WithDebugLogging())

pkg/storegateway/bucket_stores_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,14 @@ func (u *userShardingStrategy) FilterBlocks(ctx context.Context, userID string,
889889
return nil
890890
}
891891

892+
func (u *userShardingStrategy) OwnBlock(userID string, _ thanos_metadata.Meta) (bool, error) {
893+
if util.StringsContain(u.users, userID) {
894+
return true, nil
895+
}
896+
897+
return false, nil
898+
}
899+
892900
// failFirstGetBucket is an objstore.Bucket wrapper which fails the first Get() request with a mocked error.
893901
type failFirstGetBucket struct {
894902
objstore.Bucket

pkg/storegateway/gateway_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,11 @@ func (m *mockShardingStrategy) FilterBlocks(ctx context.Context, userID string,
13071307
return args.Error(0)
13081308
}
13091309

1310+
func (m *mockShardingStrategy) OwnBlock(userID string, meta metadata.Meta) (bool, error) {
1311+
args := m.Called(userID, meta)
1312+
return args.Bool(0), args.Error(1)
1313+
}
1314+
13101315
func createBucketIndex(t *testing.T, bkt objstore.Bucket, userID string) *bucketindex.Index {
13111316
updater := bucketindex.NewUpdater(bkt, userID, nil, log.NewNopLogger())
13121317
idx, _, _, err := updater.UpdateIndex(context.Background(), nil)

pkg/storegateway/sharding_strategy.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package storegateway
22

33
import (
44
"context"
5+
"errors"
56

67
"github.com/go-kit/log"
78
"github.com/go-kit/log/level"
@@ -19,6 +20,10 @@ const (
1920
shardExcludedMeta = "shard-excluded"
2021
)
2122

23+
var (
24+
errBlockNotOwned = errors.New("block not owned")
25+
)
26+
2227
type ShardingStrategy interface {
2328
// FilterUsers whose blocks should be loaded by the store-gateway. Returns the list of user IDs
2429
// that should be synced by the store-gateway.
@@ -28,6 +33,9 @@ type ShardingStrategy interface {
2833
// The provided loaded map contains blocks which have been previously returned by this function and
2934
// are now loaded or loading in the store-gateway.
3035
FilterBlocks(ctx context.Context, userID string, metas map[ulid.ULID]*metadata.Meta, loaded map[ulid.ULID]struct{}, synced block.GaugeVec) error
36+
37+
// OwnBlock checks if the block is owned by the current instance.
38+
OwnBlock(userID string, meta metadata.Meta) (bool, error)
3139
}
3240

3341
// ShardingLimits is the interface that should be implemented by the limits provider,
@@ -71,6 +79,10 @@ func (s *NoShardingStrategy) FilterBlocks(_ context.Context, _ string, _ map[uli
7179
return nil
7280
}
7381

82+
func (s *NoShardingStrategy) OwnBlock(_ string, meta metadata.Meta) (bool, error) {
83+
return true, nil
84+
}
85+
7486
// DefaultShardingStrategy is a sharding strategy based on the hash ring formed by store-gateways.
7587
// Not go-routine safe.
7688
type DefaultShardingStrategy struct {
@@ -102,6 +114,17 @@ func (s *DefaultShardingStrategy) FilterBlocks(_ context.Context, _ string, meta
102114
return nil
103115
}
104116

117+
func (s *DefaultShardingStrategy) OwnBlock(_ string, meta metadata.Meta) (bool, error) {
118+
key := cortex_tsdb.HashBlockID(meta.ULID)
119+
120+
// Check if the block is owned by the store-gateway
121+
set, err := s.r.Get(key, BlocksOwnerSync, nil, nil, nil)
122+
if err != nil {
123+
return false, err
124+
}
125+
return set.Includes(s.instanceAddr), nil
126+
}
127+
105128
// ShuffleShardingStrategy is a shuffle sharding strategy, based on the hash ring formed by store-gateways,
106129
// where each tenant blocks are sharded across a subset of store-gateway instances.
107130
type ShuffleShardingStrategy struct {
@@ -151,6 +174,18 @@ func (s *ShuffleShardingStrategy) FilterBlocks(_ context.Context, userID string,
151174
return nil
152175
}
153176

177+
func (s *ShuffleShardingStrategy) OwnBlock(userID string, meta metadata.Meta) (bool, error) {
178+
subRing := GetShuffleShardingSubring(s.r, userID, s.limits, s.zoneStableShuffleSharding)
179+
key := cortex_tsdb.HashBlockID(meta.ULID)
180+
181+
// Check if the block is owned by the store-gateway
182+
set, err := subRing.Get(key, BlocksOwnerSync, nil, nil, nil)
183+
if err != nil {
184+
return false, err
185+
}
186+
return set.Includes(s.instanceAddr), nil
187+
}
188+
154189
func filterBlocksByRingSharding(r ring.ReadRing, instanceAddr string, metas map[ulid.ULID]*metadata.Meta, loaded map[ulid.ULID]struct{}, synced block.GaugeVec, logger log.Logger) {
155190
bufDescs, bufHosts, bufZones := ring.MakeBuffersForGet()
156191

@@ -275,3 +310,20 @@ func (a *shardingBucketReaderAdapter) Iter(ctx context.Context, dir string, f fu
275310

276311
return a.InstrumentedBucketReader.Iter(ctx, dir, f, options...)
277312
}
313+
314+
type shardingBlockLifecycleCallbackAdapter struct {
315+
userID string
316+
strategy ShardingStrategy
317+
logger log.Logger
318+
}
319+
320+
func (a *shardingBlockLifecycleCallbackAdapter) PreAdd(meta metadata.Meta) error {
321+
own, err := a.strategy.OwnBlock(a.userID, meta)
322+
// If unable to check if block is owned or not because of ring error, mark it as owned
323+
// and ignore the error.
324+
if err != nil || own {
325+
return nil
326+
}
327+
level.Info(a.logger).Log("msg", "block not owned from pre check", "block", meta.ULID.String())
328+
return errBlockNotOwned
329+
}

pkg/storegateway/sharding_strategy_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package storegateway
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"strconv"
78
"testing"
@@ -11,7 +12,9 @@ import (
1112
"github.com/oklog/ulid"
1213
"github.com/prometheus/client_golang/prometheus"
1314
"github.com/prometheus/client_golang/prometheus/testutil"
15+
"github.com/prometheus/prometheus/tsdb"
1416
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/mock"
1518
"github.com/stretchr/testify/require"
1619
"github.com/thanos-io/thanos/pkg/block/metadata"
1720
"github.com/thanos-io/thanos/pkg/extprom"
@@ -272,6 +275,11 @@ func TestDefaultShardingStrategy(t *testing.T) {
272275

273276
for instanceAddr, expectedBlocks := range testData.expectedBlocks {
274277
filter := NewDefaultShardingStrategy(r, instanceAddr, log.NewNopLogger(), nil)
278+
for _, block := range expectedBlocks {
279+
owned, err := filter.OwnBlock("user-1", metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block}})
280+
require.NoError(t, err)
281+
require.True(t, owned)
282+
}
275283
synced := extprom.NewTxGaugeVec(nil, prometheus.GaugeOpts{}, []string{"state"})
276284
synced.WithLabelValues(shardExcludedMeta).Set(0)
277285

@@ -657,6 +665,11 @@ func TestShuffleShardingStrategy(t *testing.T) {
657665
// Assert on filter blocks.
658666
for _, expected := range testData.expectedBlocks {
659667
filter := NewShuffleShardingStrategy(r, expected.instanceID, expected.instanceAddr, testData.limits, log.NewNopLogger(), allowedTenants, zoneStableShuffleSharding) //nolint:govet
668+
for _, block := range expected.blocks {
669+
owned, err := filter.OwnBlock(userID, metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block}})
670+
require.NoError(t, err)
671+
require.True(t, owned)
672+
}
660673
synced := extprom.NewTxGaugeVec(nil, prometheus.GaugeOpts{}, []string{"state"})
661674
synced.WithLabelValues(shardExcludedMeta).Set(0)
662675

@@ -693,3 +706,159 @@ type shardingLimitsMock struct {
693706
func (m *shardingLimitsMock) StoreGatewayTenantShardSize(_ string) float64 {
694707
return m.storeGatewayTenantShardSize
695708
}
709+
710+
func TestDefaultShardingStrategy_OwnBlock(t *testing.T) {
711+
t.Parallel()
712+
// The following block IDs have been picked to have increasing hash values
713+
// in order to simplify the tests.
714+
block1 := ulid.MustNew(1, nil) // hash: 283204220
715+
block2 := ulid.MustNew(2, nil)
716+
block1Hash := cortex_tsdb.HashBlockID(block1)
717+
registeredAt := time.Now()
718+
block2Hash := cortex_tsdb.HashBlockID(block2)
719+
720+
ctx := context.Background()
721+
store, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
722+
t.Cleanup(func() { assert.NoError(t, closer.Close()) })
723+
724+
// Initialize the ring state.
725+
require.NoError(t, store.CAS(ctx, "test", func(in interface{}) (interface{}, bool, error) {
726+
d := ring.NewDesc()
727+
d.AddIngester("instance-1", "127.0.0.1", "zone-a", []uint32{block1Hash + 1}, ring.ACTIVE, registeredAt)
728+
d.AddIngester("instance-2", "127.0.0.2", "zone-b", []uint32{block2Hash + 1}, ring.ACTIVE, registeredAt)
729+
return d, true, nil
730+
}))
731+
732+
cfg := ring.Config{
733+
ReplicationFactor: 1,
734+
HeartbeatTimeout: time.Minute,
735+
ZoneAwarenessEnabled: true,
736+
}
737+
738+
r, err := ring.NewWithStoreClientAndStrategy(cfg, "test", "test", store, ring.NewIgnoreUnhealthyInstancesReplicationStrategy(), nil, nil)
739+
require.NoError(t, err)
740+
require.NoError(t, services.StartAndAwaitRunning(ctx, r))
741+
defer services.StopAndAwaitTerminated(ctx, r) //nolint:errcheck
742+
743+
// Wait until the ring client has synced.
744+
require.NoError(t, ring.WaitInstanceState(ctx, r, "instance-1", ring.ACTIVE))
745+
filter := NewDefaultShardingStrategy(r, "127.0.0.1", log.NewNopLogger(), nil)
746+
owned, err := filter.OwnBlock("", metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block1}})
747+
require.NoError(t, err)
748+
require.True(t, owned)
749+
// Owned by 127.0.0.2
750+
owned, err = filter.OwnBlock("", metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block2}})
751+
require.NoError(t, err)
752+
require.False(t, owned)
753+
754+
filter2 := NewDefaultShardingStrategy(r, "127.0.0.2", log.NewNopLogger(), nil)
755+
owned, err = filter2.OwnBlock("", metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block2}})
756+
require.NoError(t, err)
757+
require.True(t, owned)
758+
}
759+
760+
func TestShuffleShardingStrategy_OwnBlock(t *testing.T) {
761+
t.Parallel()
762+
// The following block IDs have been picked to have increasing hash values
763+
// in order to simplify the tests.
764+
block1 := ulid.MustNew(1, nil) // hash: 283204220
765+
block2 := ulid.MustNew(2, nil)
766+
block1Hash := cortex_tsdb.HashBlockID(block1)
767+
registeredAt := time.Now()
768+
block2Hash := cortex_tsdb.HashBlockID(block2)
769+
770+
ctx := context.Background()
771+
store, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
772+
t.Cleanup(func() { assert.NoError(t, closer.Close()) })
773+
774+
// Initialize the ring state.
775+
require.NoError(t, store.CAS(ctx, "test", func(in interface{}) (interface{}, bool, error) {
776+
d := ring.NewDesc()
777+
d.AddIngester("instance-1", "127.0.0.1", "zone-a", []uint32{block1Hash + 1}, ring.ACTIVE, registeredAt)
778+
d.AddIngester("instance-2", "127.0.0.2", "zone-b", []uint32{block2Hash + 1}, ring.ACTIVE, registeredAt)
779+
d.AddIngester("instance-3", "127.0.0.3", "zone-c", []uint32{block2Hash + 2}, ring.ACTIVE, registeredAt)
780+
return d, true, nil
781+
}))
782+
783+
cfg := ring.Config{
784+
ReplicationFactor: 1,
785+
HeartbeatTimeout: time.Minute,
786+
ZoneAwarenessEnabled: true,
787+
}
788+
limits := &shardingLimitsMock{storeGatewayTenantShardSize: 2}
789+
790+
r, err := ring.NewWithStoreClientAndStrategy(cfg, "test", "test", store, ring.NewIgnoreUnhealthyInstancesReplicationStrategy(), nil, nil)
791+
require.NoError(t, err)
792+
require.NoError(t, services.StartAndAwaitRunning(ctx, r))
793+
defer services.StopAndAwaitTerminated(ctx, r) //nolint:errcheck
794+
795+
// Wait until the ring client has synced.
796+
require.NoError(t, ring.WaitInstanceState(ctx, r, "instance-1", ring.ACTIVE))
797+
filter := NewShuffleShardingStrategy(r, "instance-1", "127.0.0.1", limits, log.NewNopLogger(), nil, true)
798+
filter2 := NewShuffleShardingStrategy(r, "instance-2", "127.0.0.2", limits, log.NewNopLogger(), nil, true)
799+
800+
owned, err := filter.OwnBlock("user-1", metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block1}})
801+
require.NoError(t, err)
802+
require.True(t, owned)
803+
// Owned by 127.0.0.2
804+
owned, err = filter.OwnBlock("user-1", metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block2}})
805+
require.NoError(t, err)
806+
require.False(t, owned)
807+
808+
owned, err = filter2.OwnBlock("user-1", metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block2}})
809+
require.NoError(t, err)
810+
require.True(t, owned)
811+
}
812+
813+
func TestShardingBlockLifecycleCallbackAdapter(t *testing.T) {
814+
userID := "user-1"
815+
logger := log.NewNopLogger()
816+
block := ulid.MustNew(1, nil)
817+
meta := metadata.Meta{BlockMeta: tsdb.BlockMeta{ULID: block}}
818+
819+
for _, tc := range []struct {
820+
name string
821+
shardingStrategy func() ShardingStrategy
822+
expectErr bool
823+
}{
824+
{
825+
name: "own block",
826+
shardingStrategy: func() ShardingStrategy {
827+
s := &mockShardingStrategy{}
828+
s.On("OwnBlock", mock.Anything, mock.Anything).Return(true, nil)
829+
return s
830+
},
831+
},
832+
{
833+
name: "own block has error, still own block",
834+
shardingStrategy: func() ShardingStrategy {
835+
s := &mockShardingStrategy{}
836+
s.On("OwnBlock", mock.Anything, mock.Anything).Return(false, errors.New("some error"))
837+
return s
838+
},
839+
},
840+
{
841+
name: "not own block",
842+
shardingStrategy: func() ShardingStrategy {
843+
s := &mockShardingStrategy{}
844+
s.On("OwnBlock", mock.Anything, mock.Anything).Return(false, nil)
845+
return s
846+
},
847+
expectErr: true,
848+
},
849+
} {
850+
t.Run(tc.name, func(t *testing.T) {
851+
a := &shardingBlockLifecycleCallbackAdapter{
852+
userID: userID,
853+
logger: logger,
854+
strategy: tc.shardingStrategy(),
855+
}
856+
err := a.PreAdd(meta)
857+
if tc.expectErr {
858+
require.Error(t, err)
859+
} else {
860+
require.NoError(t, err)
861+
}
862+
})
863+
}
864+
}

0 commit comments

Comments
 (0)