Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions pkg/ingester/ingester.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ func (r tsdbCloseCheckResult) shouldClose() bool {

type userTSDB struct {
db *tsdb.DB
blocksToDeleteReady atomic.Bool
userID string
logger log.Logger
activeSeries *ActiveSeries
Expand Down Expand Up @@ -600,7 +601,7 @@ func (u *userTSDB) PostDeletion(metrics map[chunks.HeadSeriesRef]labels.Labels)

// blocksToDelete filters the input blocks and returns the blocks which are safe to be deleted from the ingester.
func (u *userTSDB) blocksToDelete(blocks []*tsdb.Block) map[ulid.ULID]struct{} {
if u.db == nil {
if !u.blocksToDeleteReady.Load() {
return nil
}
deletable := tsdb.DefaultBlocksToDelete(u.db)(blocks)
Expand Down Expand Up @@ -3113,26 +3114,14 @@ func (i *Ingester) createTSDB(userID string) (*userTSDB, error) {
return nil, errors.Wrapf(err, "failed to compact TSDB: %s", udir)
}

userDB.db = db
// We set the limiter here because we don't want to limit
// series during WAL replay.
userDB.limiter = i.limiter

if db.Head().NumSeries() > 0 {
// If there are series in the head, use max time from head. If this time is too old,
// TSDB will be eligible for flushing and closing sooner, unless more data is pushed to it quickly.
userDB.setLastUpdate(util.TimeFromMillis(db.Head().MaxTime()))
} else {
// If head is empty (eg. new TSDB), don't close it right after.
userDB.setLastUpdate(time.Now())
}

// Thanos shipper requires at least 1 external label to be set. For this reason,
// we set the tenant ID as external label and we'll filter it out when reading
// the series from the storage.
l := labels.FromStrings(cortex_tsdb.TenantIDExternalLabel, userID, cortex_tsdb.IngesterIDExternalLabel, i.TSDBState.shipperIngesterID)

// Create a new shipper for this database
// Create a new shipper for this database before making the TSDB visible to
// callbacks. TSDB's reload goroutine can call blocksToDelete concurrently,
// and blocksToDelete reads userDB.shipper once userDB.db is set.
if i.cfg.BlocksStorageConfig.TSDB.IsBlocksShippingEnabled() {
udirRoot, err := os.OpenRoot(udir)
if err != nil {
Expand All @@ -3151,8 +3140,25 @@ func (i *Ingester) createTSDB(userID string) (*userTSDB, error) {
shipper.WithAllowOutOfOrderUploads(true), // Allow out of order uploads. It's fine in Cortex's context.
shipper.WithSkipCorruptedBlocks(true), // We allow out of order uploads. This is the same behavior. We should track error with metrics
)
userDB.shipperMetadataFilePath = filepath.Join(userDB.db.Dir(), filepath.Clean(shipper.DefaultMetaFilename))
userDB.shipperMetadataFilePath = filepath.Join(db.Dir(), filepath.Clean(shipper.DefaultMetaFilename))
}

userDB.db = db

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this assignment after the shipper changes the order of two plain writes, but it doesn't synchronize them with the reader. tsdb.Open has already started the reload goroutine (BlockReloadInterval is 1m), which calls blocksToDelete and reads u.db and u.shipper without a lock or atomic, so -race still reports this write against the u.db == nil read. I reproduced it with a small probe: a goroutine calling u.blocksToDelete(nil) in a loop while the test assigns u.db = db fails under go test -race with a data race on exactly this pair. Could we publish with synchronization instead, e.g. an atomic.Bool set after db and shipper are assigned and checked at the top of blocksToDelete, and add a -race test for it?

userDB.blocksToDeleteReady.Store(true)
// We set the limiter here because we don't want to limit
// series during WAL replay.
userDB.limiter = i.limiter

if db.Head().NumSeries() > 0 {
// If there are series in the head, use max time from head. If this time is too old,
// TSDB will be eligible for flushing and closing sooner, unless more data is pushed to it quickly.
userDB.setLastUpdate(util.TimeFromMillis(db.Head().MaxTime()))
} else {
// If head is empty (eg. new TSDB), don't close it right after.
userDB.setLastUpdate(time.Now())
}

if userDB.shipper != nil {
// Initialise the shipper blocks cache.
if err := userDB.updateCachedShippedBlocks(); err != nil {
level.Error(userLogger).Log("msg", "failed to update cached shipped blocks after shipper initialisation", "err", err)
Expand Down
35 changes: 35 additions & 0 deletions pkg/ingester/ingester_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7525,6 +7525,7 @@ func TestIngester_UserTSDB_BlocksToDelete(t *testing.T) {
shippedBlocks: shippedBlocks,
blockRetentionPeriod: 2 * time.Hour.Milliseconds(),
}
userDB.blocksToDeleteReady.Store(true)

blocksToDelete := userDB.blocksToDelete(blocks)
require.Equal(t, 4, len(blocksToDelete))
Expand Down Expand Up @@ -7557,6 +7558,7 @@ func TestIngester_UserTSDB_BlocksToDelete(t *testing.T) {
shippedBlocks: shippedBlocks,
blockRetentionPeriod: 2 * time.Hour.Milliseconds(),
}
userDB.blocksToDeleteReady.Store(true)

blocksToDelete := userDB.blocksToDelete(blocks)
require.Equal(t, 3, len(blocksToDelete))
Expand Down Expand Up @@ -7585,6 +7587,7 @@ func TestIngester_UserTSDB_BlocksToDelete(t *testing.T) {
shippedBlocks: shippedBlocks,
blockRetentionPeriod: 2 * time.Hour.Milliseconds(),
}
userDB.blocksToDeleteReady.Store(true)

blocksToDelete := userDB.blocksToDelete(blocks)
require.Equal(t, 0, len(blocksToDelete))
Expand Down Expand Up @@ -7617,6 +7620,7 @@ func TestIngester_UserTSDB_BlocksToDelete(t *testing.T) {
shippedBlocks: shippedBlocks,
blockRetentionPeriod: 2 * time.Hour.Milliseconds(),
}
userDB.blocksToDeleteReady.Store(true)

blocksToDelete := userDB.blocksToDelete(blocks)
require.Equal(t, 3, len(blocksToDelete))
Expand Down Expand Up @@ -7650,6 +7654,7 @@ func TestIngester_UserTSDB_BlocksToDelete(t *testing.T) {
shippedBlocks: shippedBlocks,
blockRetentionPeriod: 2 * time.Hour.Milliseconds(),
}
userDB.blocksToDeleteReady.Store(true)

blocksToDelete := userDB.blocksToDelete(blocks)
require.Equal(t, 3, len(blocksToDelete))
Expand All @@ -7674,6 +7679,7 @@ func TestIngester_UserTSDB_BlocksToDelete(t *testing.T) {
db: db,
blockRetentionPeriod: 2 * time.Hour.Milliseconds(),
}
userDB.blocksToDeleteReady.Store(true)

blocksToDelete := userDB.blocksToDelete(blocks)
require.Equal(t, 4, len(blocksToDelete))
Expand All @@ -7684,6 +7690,35 @@ func TestIngester_UserTSDB_BlocksToDelete(t *testing.T) {
})
}

func TestIngester_UserTSDB_BlocksToDeletePublication(t *testing.T) {
db, err := tsdb.Open(t.TempDir(), promslog.NewNopLogger(), prometheus.NewPedanticRegistry(), &tsdb.Options{}, nil)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, db.Close()) })

userDB := &userTSDB{}
stop := make(chan struct{})
done := make(chan struct{})

go func() {
defer close(done)
for {
select {
case <-stop:
return
default:
userDB.blocksToDelete(nil)
}
}
}()

userDB.db = db
userDB.shipper = &shipperMock{}
userDB.blocksToDeleteReady.Store(true)

close(stop)
<-done
}

func TestIngester_UpdateLabelSetMetrics(t *testing.T) {
cfg := defaultIngesterTestConfig(t)
cfg.BlocksStorageConfig.TSDB.BlockRanges = []time.Duration{2 * time.Hour}
Expand Down