Files
Joe Elliottandzeekay e94374166e [Tempo 3.0] Remove compactor and v2 block encoding code (#6273)
* o7 compactor

Signed-off-by: Joe Elliott <[email protected]>

* wip: o7 v2

Signed-off-by: Joe Elliott <[email protected]>

* test cleanup

Signed-off-by: Joe Elliott <[email protected]>

* remove compactor from jsonnet

Signed-off-by: Joe Elliott <[email protected]>

* changelog

Signed-off-by: Joe Elliott <[email protected]>

* gen manifest

Signed-off-by: Joe Elliott <[email protected]>

* config cleanup

Signed-off-by: Joe Elliott <[email protected]>

* docs

Signed-off-by: Joe Elliott <[email protected]>

* jsonnet

Signed-off-by: Joe Elliott <[email protected]>

* remove encoding

Signed-off-by: Joe Elliott <[email protected]>

* remove data encoding

Signed-off-by: Joe Elliott <[email protected]>

* lint and cleanup

Signed-off-by: Joe Elliott <[email protected]>

* unflake TestWorker\?

Signed-off-by: Joe Elliott <[email protected]>

---------

Signed-off-by: Joe Elliott <[email protected]>
2026-02-22 00:55:18 -08:00

1265 lines
45 KiB
Go

package blockbuilder
import (
"context"
"crypto/rand"
"errors"
"fmt"
"testing"
"time"
"github.com/go-kit/log"
"github.com/grafana/dskit/flagext"
"github.com/grafana/dskit/ring"
"github.com/grafana/dskit/services"
"github.com/grafana/tempo/modules/storage"
"github.com/grafana/tempo/pkg/ingest"
"github.com/grafana/tempo/pkg/ingest/testkafka"
"github.com/grafana/tempo/pkg/util"
"github.com/grafana/tempo/pkg/util/test"
"github.com/grafana/tempo/tempodb"
"github.com/grafana/tempo/tempodb/backend"
"github.com/grafana/tempo/tempodb/backend/local"
"github.com/grafana/tempo/tempodb/blocklist"
"github.com/grafana/tempo/tempodb/encoding"
"github.com/grafana/tempo/tempodb/encoding/common"
"github.com/grafana/tempo/tempodb/wal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/twmb/franz-go/pkg/kadm"
"github.com/twmb/franz-go/pkg/kerr"
"github.com/twmb/franz-go/pkg/kgo"
"github.com/twmb/franz-go/pkg/kmsg"
"go.uber.org/atomic"
)
const (
testTopic = "test-topic"
testConsumerGroup = "test-consumer-group"
testPartition = int32(0)
)
func TestMain(m *testing.M) {
pollTimeout = 2 * time.Second // speed up the tests
m.Run()
}
// When the partition starts with no existing commit,
// the block-builder looks back to consume all available records from the start and ensures they are committed and flushed into a block.
func TestBlockbuilder_lookbackOnNoCommit(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, testTopic)
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
producedRecords := testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool {
return kafkaCommits.Load() > 0
}, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 1 && countFlushedTraces(store) == 1
}, time.Minute, time.Second)
// Check committed offset
requireLastCommitEquals(t, ctx, client, producedRecords[len(producedRecords)-1].Offset+1)
}
func TestBlockbuilder_without_partitions_assigned_returns_an_error(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, testTopic)
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{})
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
_, err = b.consume(ctx)
require.ErrorIs(t, err, errNoPartitionsAssigned)
}
func TestBlockbuilder_getAssignedPartitions(t *testing.T) {
cfg := blockbuilderConfig(t, "localhost", []int32{0, 2, 4, 6})
partitionRing := newPartitionRingReaderWithPartitions(map[int32]ring.PartitionDesc{
0: {Id: 0, State: ring.PartitionActive},
1: {Id: 1, State: ring.PartitionActive},
2: {Id: 2, State: ring.PartitionInactive},
3: {Id: 3, State: ring.PartitionActive},
4: {Id: 4, State: ring.PartitionPending},
5: {Id: 5, State: ring.PartitionDeleted},
20: {Id: 20, State: ring.PartitionActive},
})
b, err := New(cfg, testLogger(t), partitionRing, &mockOverrides{}, nil)
require.NoError(t, err)
partitions := b.getAssignedPartitions()
assert.Equal(t, []int32{0, 2}, partitions)
}
// Starting with a pre-existing commit,
// the block-builder resumes from the last known position, consuming new records,
// and ensures all of them are properly committed and flushed into blocks.
func TestBlockbuilder_startWithCommit(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 100, testTopic)
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
producedRecords := testkafka.SendTracesFor(t, ctx, client, 5*time.Second, 100*time.Millisecond, ingest.Encode) // Send for 5 seconds
commitedAt := len(producedRecords) / 2
// Commit half of the records
offsets := make(kadm.Offsets)
offsets.Add(kadm.Offset{
Topic: testTopic,
Partition: 0,
At: producedRecords[commitedAt].Offset,
})
admClient := kadm.NewClient(client)
require.NoError(t, admClient.CommitAllOffsets(ctx, cfg.IngestStorageConfig.Kafka.ConsumerGroup, offsets))
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
records := testkafka.SendTracesFor(t, ctx, client, 5*time.Second, 100*time.Millisecond, ingest.Encode) // Send for 5 seconds
producedRecords = append(producedRecords, records...)
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool {
return kafkaCommits.Load() > 0
}, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return countFlushedTraces(store) == len(producedRecords)-commitedAt
}, time.Minute, time.Second)
// Check committed offset
requireLastCommitEquals(t, ctx, client, producedRecords[len(producedRecords)-1].Offset+1)
}
// In case a block flush initially fails, the system retries until it succeeds.
func TestBlockbuilder_flushingFails(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, "test-topic")
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
storageWrites := atomic.NewInt32(0)
store := newStoreWrapper(newStore(ctx, t), func(ctx context.Context, block tempodb.WriteableBlock, store storage.Store) error {
// Fail the first block write
if storageWrites.Inc() == 1 {
return errors.New("failed to write block")
}
return store.WriteBlock(ctx, block)
})
cfg := blockbuilderConfig(t, address, []int32{0})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
producedRecords := testkafka.SendTracesFor(t, ctx, client, time.Second, 100*time.Millisecond, ingest.Encode) // Send for 1 second, <1 consumption cycles
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool { return kafkaCommits.Load() >= 1 }, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) >= 1
}, time.Minute, time.Second)
// Check committed offset
requireLastCommitEquals(t, ctx, client, producedRecords[len(producedRecords)-1].Offset+1)
}
// Receiving records with older timestamps the block-builder processes them in the current cycle,
// ensuring they're written into a new block despite "belonging" to another cycle.
func TestBlockbuilder_receivesOldRecords(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, "test-topic")
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
producedRecords := testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool {
return kafkaCommits.Load() == 1
}, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 1
}, time.Minute, time.Second)
// Re-send the same records with an older timestamp
// They should be processed in the next cycle and written to a new block regardless of the timestamp
for _, record := range producedRecords {
record.Timestamp = record.Timestamp.Add(-time.Hour)
}
res := client.ProduceSync(ctx, producedRecords...)
require.NoError(t, res.FirstErr())
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool {
l := kafkaCommits.Load()
return l == 2
}, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 2
}, time.Minute, time.Second)
// Check committed offset
requireLastCommitEquals(t, ctx, client, producedRecords[len(producedRecords)-1].Offset+1)
}
// FIXME - Test is unstable and will fail if records cross two consumption cycles,
//
// because it's asserting that there is exactly two commits, one of which fails.
// It can be 3 commits if the records cross two consumption cycles.
//
// On encountering a commit failure, the block-builder retries the operation and eventually succeeds.
//
// This would cause two blocks to be written, one for each cycle (one cycle fails at commit, the other succeeds).
// The block-builder deterministically generates the block ID based on the cycle end timestamp,
// so the block ID for the failed cycle is the same from the block ID for the successful cycle,
// and the failed block is overwritten by the successful one.
func TestBlockbuilder_committingFails(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, "test-topic")
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(req kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
if kafkaCommits.Load() == 1 { // First commit fails
res := kmsg.NewOffsetCommitResponse()
res.Version = req.GetVersion()
res.Topics = []kmsg.OffsetCommitResponseTopic{
{
Topic: testTopic,
Partitions: []kmsg.OffsetCommitResponseTopicPartition{
{
Partition: 0,
ErrorCode: kerr.RebalanceInProgress.Code,
},
},
},
}
return &res, nil, true
}
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
producedRecords := testkafka.SendTracesFor(t, ctx, client, time.Second, 100*time.Millisecond, ingest.Encode) // Send for 1 second, <1 consumption cycle
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool {
return kafkaCommits.Load() == 2 // First commit fails, second commit succeeds
}, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 1 // Only one block should have been written
}, time.Minute, time.Second)
// Check committed offset
requireLastCommitEquals(t, ctx, client, producedRecords[len(producedRecords)-1].Offset+1)
}
func TestBlockbuilder_retries_on_retriable_commit_error(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, "test-topic")
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(req kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
if kafkaCommits.Load() == 1 {
res := kmsg.NewOffsetCommitResponse()
res.Version = req.GetVersion()
res.Topics = []kmsg.OffsetCommitResponseTopic{
{
Topic: testTopic,
Partitions: []kmsg.OffsetCommitResponseTopicPartition{
{
Partition: 0,
ErrorCode: kerr.NotEnoughReplicas.Code, // Retryable error code
},
},
},
}
return &res, nil, true
}
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
logger := testLogger(t)
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
producedRecords := testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
lastRecordOffset := producedRecords[len(producedRecords)-1].Offset
b, err := New(cfg, logger, newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool {
return kafkaCommits.Load() == 2
}, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 1 // Only one block should have been written
}, time.Minute, time.Second)
requireLastCommitEquals(t, ctx, client, lastRecordOffset+1)
}
func TestBlockbuilder_retries_on_commit_error(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, "test-topic")
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(req kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
if kafkaCommits.Load() == 1 {
res := kmsg.NewOffsetCommitResponse()
res.Version = req.GetVersion()
res.Topics = []kmsg.OffsetCommitResponseTopic{
{
Topic: testTopic,
Partitions: []kmsg.OffsetCommitResponseTopicPartition{
{
Partition: 0,
},
},
},
}
return &res, fmt.Errorf("error committing offset"), true
}
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
producedRecords := testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
lastRecordOffset := producedRecords[len(producedRecords)-1].Offset
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
// Wait for record to be consumed and committed.
require.Eventually(t, func() bool {
return kafkaCommits.Load() == 2
}, time.Minute, time.Second)
// Wait for the block to be flushed.
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 1 // Only one block should have been written
}, time.Minute, time.Second)
requireLastCommitEquals(t, ctx, client, lastRecordOffset+1)
}
// TestBlockbuilder_noDoubleConsumption verifies that records are not consumed twice when there are no more records in the partition.
// This test ensures that the BlockBuilder correctly commits the offset as lastRec.Offset + 1 instead of just lastRec.Offset.
func TestBlockbuilder_noDoubleConsumption(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, testTopic)
// Track commits
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(_ kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
// Set a shorter consume cycle duration
cfg.ConsumeCycleDuration = 500 * time.Millisecond
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
// Send a single record
producedRecords := testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
lastRecordOffset := producedRecords[len(producedRecords)-1].Offset
// Create the block builder
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
// Wait for the record to be consumed and committed
require.Eventually(t, func() bool {
return kafkaCommits.Load() > 0
}, 30*time.Second, time.Second)
// Check that the offset was committed correctly (lastRec.Offset + 1)
requireLastCommitEquals(t, ctx, client, lastRecordOffset+1)
// Send another record
newRecords := testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
newRecordOffset := newRecords[len(newRecords)-1].Offset
// Wait for the new record to be consumed and committed
require.Eventually(t, func() bool {
return kafkaCommits.Load() > 1
}, 30*time.Second, time.Second)
// Verify that the new offset was committed correctly
requireLastCommitEquals(t, ctx, client, newRecordOffset+1)
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 2
}, 30*time.Second, time.Second)
// Verify the total number of traces is correct (1 from each batch)
require.Equal(t, 2, countFlushedTraces(store))
}
func TestBlockBuilder_honor_maxBytesPerCycle(t *testing.T) {
cases := []struct {
name string
maxBytesPerCycle int
expectedCommits int32
expectedWrites int32
}{
{
name: "Limited to 1 bytes per cycle",
maxBytesPerCycle: 1,
expectedCommits: 2,
expectedWrites: 2,
},
{
name: "Limited to 100_000 bytes per cycle",
maxBytesPerCycle: 100_000,
expectedCommits: 1,
expectedWrites: 1,
},
{
name: "Unlimited bytes per cycle",
maxBytesPerCycle: 0,
expectedCommits: 1,
expectedWrites: 1,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, "test-topic")
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
storageWrites := atomic.NewInt32(0)
store := newStoreWrapper(newStore(ctx, t), func(ctx context.Context, block tempodb.WriteableBlock, store storage.Store) error {
storageWrites.Inc()
return store.WriteBlock(ctx, block)
})
cfg := blockbuilderConfig(t, address, []int32{0})
cfg.MaxBytesPerCycle = uint64(tc.maxBytesPerCycle)
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
// We send two records with a size less than 30KB
testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
producedRecords := testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
require.Eventually(t, func() bool {
return kafkaCommits.Load() == tc.expectedCommits
}, time.Minute, time.Second)
require.Eventually(t, func() bool {
return storageWrites.Load() == tc.expectedWrites
}, 30*time.Second, time.Second)
requireLastCommitEquals(t, ctx, client, producedRecords[len(producedRecords)-1].Offset+1)
})
}
}
func TestBlockbuilder_usesRecordTimestampForBlockStartAndEnd(t *testing.T) {
// default ingestion slack is 2 minutes. create some convenient times to help the test below
now := time.Unix(1000000, 0)
oneMinuteAgo := now.Add(-time.Minute)
oneMinuteLater := now.Add(time.Minute)
twoMinutesAgo := now.Add(-2 * time.Minute)
threeMinutesAgo := now.Add(-3 * time.Minute)
tcs := []struct {
name string
startTime time.Time
endTime time.Time
recordTime time.Time
expectedStart time.Time
expectedEnd time.Time
}{
{ // records where the timestamp exactly matches the span timings
name: "exact match",
startTime: oneMinuteAgo,
endTime: oneMinuteAgo,
recordTime: oneMinuteAgo,
expectedStart: oneMinuteAgo,
expectedEnd: oneMinuteAgo,
},
{ // records where the timestamp doesn't match the span timings, but within the ingestion slack
name: "within ingestion slack",
startTime: oneMinuteAgo,
endTime: oneMinuteLater,
recordTime: now,
expectedStart: oneMinuteAgo,
expectedEnd: oneMinuteLater,
},
{ // records where the timestamp doesn't match the span timings and is outside the ingestion slack
name: "outside ingestion slack",
startTime: threeMinutesAgo,
endTime: now,
recordTime: now,
expectedStart: twoMinutesAgo, // default ingestion slack is 2 minutes
expectedEnd: now,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, testTopic)
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
// Create a trace with specific start/end times
traceID := generateTraceID(t)
startTimeNano := uint64(tc.startTime.UnixNano())
endTimeNano := uint64(tc.endTime.UnixNano())
req := test.MakePushBytesRequest(t, 1, traceID, startTimeNano, endTimeNano)
records, err := ingest.Encode(0, util.FakeTenantID, req, 1_000_000)
require.NoError(t, err)
// Set the record timestamp
for _, record := range records {
record.Timestamp = tc.recordTime
}
// Send the record
res := client.ProduceSync(ctx, records...)
require.NoError(t, res.FirstErr())
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
// Wait for record to be consumed and committed
require.Eventually(t, func() bool {
return kafkaCommits.Load() > 0
}, time.Minute, time.Second)
// Wait for the block to be flushed
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 1
}, time.Minute, time.Second)
// Verify block timestamps
metas := store.BlockMetas(util.FakeTenantID)
require.Len(t, metas, 1)
meta := metas[0]
require.Equal(t, tc.expectedStart.Unix(), meta.StartTime.Unix())
require.Equal(t, tc.expectedEnd.Unix(), meta.EndTime.Unix())
})
}
}
func TestBlockbuilder_marksOldBlocksCompacted(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, testTopic)
// Track commits
kafkaCommits := atomic.NewInt32(0)
k.ControlKey(kmsg.OffsetCommit, func(_ kmsg.Request) (kmsg.Response, error, bool) {
kafkaCommits.Inc()
return nil, nil, false
})
var (
cfg = blockbuilderConfig(t, address, []int32{0})
client = testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
)
// Send data for each tenant
var (
goodTenantID = "1"
badTenantID = "2"
producedRecords []*kgo.Record
)
producedRecords = append(producedRecords, testkafka.SendReq(ctx, t, client, ingest.Encode, goodTenantID)...)
producedRecords = append(producedRecords, testkafka.SendReq(ctx, t, client, ingest.Encode, badTenantID)...)
lastRecordOffset := producedRecords[len(producedRecords)-1].Offset
// Simulate failures on the first cycle
badWrites := atomic.NewInt32(0)
goodWrites := atomic.NewInt32(0)
goodBlockIDs := []backend.UUID{}
store := newStoreWrapper(newStore(ctx, t), func(ctx context.Context, block tempodb.WriteableBlock, store storage.Store) error {
switch block.BlockMeta().TenantID {
case badTenantID:
// First flush on tenant 2 fails
if badWrites.Inc() == 1 {
// Wait until flush on good tenant is complete
// and then return the error. Tenants are flushed in parallel
// so this is required to ensure we get a block flushed on the first cycle.
require.Eventually(t, func() bool {
return goodWrites.Load() > 0
}, 30*time.Second, 50*time.Millisecond)
return errors.New("failed to write block")
}
case goodTenantID:
// Save all blocks flushed for good tenant
defer goodWrites.Inc()
goodBlockIDs = append(goodBlockIDs, block.BlockMeta().BlockID)
}
return store.WriteBlock(ctx, block)
})
t.Cleanup(func() { require.NoError(t, services.StopAndAwaitTerminated(context.Background(), store)) })
// Create the block builder
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(context.Background(), b))
cancel(errors.New("test cleanup"))
store.Shutdown()
})
// Wait for the records to be consumed and committed
require.Eventually(t, func() bool {
return kafkaCommits.Load() > 0
}, 30*time.Second, 50*time.Millisecond)
// Check that the offset was committed correctly (lastRec.Offset + 1)
requireLastCommitEquals(t, ctx, client, lastRecordOffset+1)
// Repoll flushed blocks.
store.PollNow(ctx)
// Verify that each tenant only has 1 active block
require.Equal(t, 1, len(store.BlockMetas(goodTenantID)))
require.Equal(t, 1, len(store.BlockMetas(badTenantID)))
// Verify the good tenant flushed 2 attempts
require.Equal(t, 2, len(goodBlockIDs))
// Verify the first block is compacted
m, cm, err := store.BlockMeta(ctx, goodTenantID, goodBlockIDs[0])
require.NoError(t, err)
require.Nil(t, m)
require.NotNil(t, cm)
// Verify the second block is not compacted
m, cm, err = store.BlockMeta(ctx, goodTenantID, goodBlockIDs[1])
require.NoError(t, err)
require.NotNil(t, m)
require.Nil(t, cm)
}
func TestBlockbuilder_gracefulShutdown(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
k, address := testkafka.CreateCluster(t, 1, testTopic)
chConsumeStarted := make(chan struct{})
chConsumeDone := make(chan struct{})
chStopDone := make(chan struct{})
k.ControlKey(kmsg.OffsetCommit, func(kmsg.Request) (kmsg.Response, error, bool) {
close(chConsumeDone)
return nil, nil, false
})
k.ControlKey(kmsg.OffsetFetch, func(kmsg.Request) (kmsg.Response, error, bool) {
close(chConsumeStarted)
return nil, nil, false
})
store := newStore(ctx, t)
cfg := blockbuilderConfig(t, address, []int32{0}) // Fix: Properly specify partition
// Send initial traces to ensure the partition has records
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
testkafka.SendReq(ctx, t, client, ingest.Encode, util.FakeTenantID)
// Start sending traces in the background
go func() {
testkafka.SendTracesFor(t, ctx, client, 60*time.Second, time.Second, ingest.Encode)
}()
b, err := New(cfg, testLogger(t), newPartitionRingReader(), &mockOverrides{}, store)
require.NoError(t, err)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
// Wait for cycle to be ongoing
select {
case <-chConsumeStarted:
case <-time.After(cfg.ConsumeCycleDuration * 2):
t.Fatal("Consume cycle didn't start")
}
go func() {
defer close(chStopDone)
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
}()
// Check if shutdown waits for consume cycle to finish
select {
case <-chConsumeDone:
case <-chStopDone:
t.Fatal("Shutdown completed before consume cycle finished - not graceful!")
case <-time.After(10 * time.Second):
t.Fatal("Timed out waiting for consume cycle to finish")
}
// Now wait for the shutdown to complete after we've verified it's graceful
select {
case <-chStopDone:
// Good, shutdown completed after we checked
case <-time.After(5 * time.Second):
t.Fatal("Shutdown didn't complete after consume cycle finished")
}
// Verify blocks were written
require.Eventually(t, func() bool { return len(store.BlockMetas(util.FakeTenantID)) > 0 }, 30*time.Second, time.Second)
}
func blockbuilderConfig(t testing.TB, address string, assignedPartitions []int32) Config {
cfg := Config{}
flagext.DefaultValues(&cfg)
flagext.DefaultValues(&cfg.BlockConfig)
flagext.DefaultValues(&cfg.IngestStorageConfig.Kafka)
cfg.IngestStorageConfig.Kafka.Address = address
cfg.IngestStorageConfig.Kafka.Topic = testTopic
cfg.IngestStorageConfig.Kafka.ConsumerGroup = testConsumerGroup
cfg.AssignedPartitionsMap = map[string][]int32{cfg.InstanceID: assignedPartitions}
cfg.ConsumeCycleDuration = 5 * time.Second
cfg.WAL.Filepath = t.TempDir()
return cfg
}
var _ blocklist.JobSharder = (*ownEverythingSharder)(nil)
type ownEverythingSharder struct{}
func (o *ownEverythingSharder) Owns(string) bool { return true }
func newStore(ctx context.Context, t testing.TB) storage.Store {
store := newStoreWithLogger(ctx, t, testLogger(t), false)
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(context.Background(), store))
})
return store
}
func newStoreWithLogger(ctx context.Context, t testing.TB, log log.Logger, skipNoCompactBlocks bool) storage.Store {
tmpDir := t.TempDir()
s, err := storage.NewStore(storage.Config{
Trace: tempodb.Config{
Backend: backend.Local,
Local: &local.Config{
Path: tmpDir,
},
Block: &common.BlockConfig{
BloomFP: 0.01,
BloomShardSizeBytes: 100_000,
Version: encoding.LatestEncoding().Version(),
},
WAL: &wal.Config{
Filepath: tmpDir,
},
BlocklistPoll: 100 * time.Millisecond,
},
}, nil, log)
require.NoError(t, err)
s.EnablePolling(ctx, &ownEverythingSharder{}, skipNoCompactBlocks)
return s
}
var _ storage.Store = (*storeWrapper)(nil)
type storeWrapper struct {
storage.Store
writeBlock func(ctx context.Context, block tempodb.WriteableBlock, store storage.Store) error
}
func newStoreWrapper(s storage.Store, writeBlock func(ctx context.Context, block tempodb.WriteableBlock, store storage.Store) error) *storeWrapper {
return &storeWrapper{
Store: s,
writeBlock: writeBlock,
}
}
func (m *storeWrapper) WriteBlock(ctx context.Context, block tempodb.WriteableBlock) error {
if m.writeBlock != nil {
return m.writeBlock(ctx, block, m.Store)
}
return m.Store.WriteBlock(ctx, block)
}
var _ ring.PartitionRingReader = (*mockPartitionRingReader)(nil)
func newPartitionRingReader() *mockPartitionRingReader {
return &mockPartitionRingReader{
r: ring.NewPartitionRing(ring.PartitionRingDesc{
Partitions: map[int32]ring.PartitionDesc{
0: {State: ring.PartitionActive},
},
}),
}
}
func newPartitionRingReaderWithPartitions(partitions map[int32]ring.PartitionDesc) *mockPartitionRingReader {
return &mockPartitionRingReader{
r: ring.NewPartitionRing(ring.PartitionRingDesc{
Partitions: partitions,
}),
}
}
type mockPartitionRingReader struct {
r *ring.PartitionRing
}
func (m *mockPartitionRingReader) PartitionRing() *ring.PartitionRing {
return m.r
}
var _ Overrides = (*mockOverrides)(nil)
type mockOverrides struct {
dc backend.DedicatedColumns
}
func (m *mockOverrides) MaxBytesPerTrace(_ string) int { return 0 }
func (m *mockOverrides) DedicatedColumns(_ string) backend.DedicatedColumns { return m.dc }
func countFlushedTraces(store storage.Store) int {
count := 0
for _, meta := range store.BlockMetas(util.FakeTenantID) {
count += int(meta.TotalObjects)
}
return count
}
func generateTraceID(t testing.TB) []byte {
traceID := make([]byte, 16)
_, err := rand.Read(traceID)
require.NoError(t, err)
return traceID
}
// nolint: revive
func requireLastCommitEquals(t testing.TB, ctx context.Context, client *kgo.Client, expectedOffset int64) {
offsets, err := kadm.NewClient(client).FetchOffsetsForTopics(ctx, testConsumerGroup, testTopic)
require.NoError(t, err)
offset, ok := offsets.Lookup(testTopic, testPartition)
require.True(t, ok)
require.Equal(t, expectedOffset, offset.At)
}
func BenchmarkBlockBuilder(b *testing.B) {
var (
ctx = context.Background()
logger = log.NewNopLogger()
_, address = testkafka.CreateCluster(b, 1, testTopic)
store = newStoreWithLogger(ctx, b, logger, false)
cfg = blockbuilderConfig(b, address, []int32{0})
client = testkafka.NewKafkaClient(b, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
o = &mockOverrides{
dc: backend.DedicatedColumns{
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res0", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res1", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res2", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res3", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res4", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res5", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res6", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res7", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res8", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeResource, Name: "res9", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span0", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span1", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span2", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span3", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span4", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span5", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span6", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span7", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span8", Type: backend.DedicatedColumnTypeString},
backend.DedicatedColumn{Scope: backend.DedicatedColumnScopeSpan, Name: "span9", Type: backend.DedicatedColumnTypeString},
},
}
)
cfg.ConsumeCycleDuration = 1 * time.Hour
bb, err := New(cfg, logger, newPartitionRingReader(), o, store)
require.NoError(b, err)
defer func() { require.NoError(b, bb.stopping(nil)) }()
// Startup (without starting the background consume cycle)
err = bb.starting(ctx)
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Send more data
b.StopTimer()
size := 0
for i := 0; i < 1000; i++ {
for _, r := range testkafka.SendReq(ctx, b, client, ingest.Encode, util.FakeTenantID) {
size += len(r.Value)
}
}
b.StartTimer()
_, err = bb.consume(ctx)
require.NoError(b, err)
b.SetBytes(int64(size))
}
}
type slowStore struct {
storage.Store
wait chan struct{}
}
func (s *slowStore) WriteBlock(ctx context.Context, block tempodb.WriteableBlock) error {
s.wait <- struct{}{} // send a signal to a goroutine
<-s.wait // wait for the signal from the goroutine
return s.Store.WriteBlock(ctx, block)
}
// lock waits for the signal from WriteBlock locking the operation
func (s *slowStore) lock() {
<-s.wait
}
// unlock sends a signal to WriteBlock unlocking the operation
func (s *slowStore) unlock() {
s.wait <- struct{}{}
}
// TestBlockbuilder_twoPartitions_secondEmpty verifies correct handling of two Kafka
// partitions where the second partition is initially empty and receives data later.
// It uses a channel-gated store to step consumption, injects records between consume
// cycles (sleeping longer than ConsumeCycleDuration), and asserts that:
// - both partitions are assigned,
// - three blocks are flushed (p0 initial, p0 later, p1 later), and
// - committed offsets equal number of sent records.
// The test highly coupled with the blockbuilder implementation. In case it stuck
// due to channel, it is advised to debug consume cycle step by step.
// If the test hangs, it is most likely because storage received unexpected writes.
func TestBlockbuilder_twoPartitions_secondEmpty(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
reqTime := time.Now().Add(-1 * time.Minute) // to be sure it won't be filtered out by cycle duration check
// Create a Kafka cluster with 2 partitions
_, address := testkafka.CreateCluster(t, 2, testTopic)
// Setup block-builder
ch := make(chan struct{})
store := &slowStore{Store: newStore(ctx, t), wait: ch}
cfg := blockbuilderConfig(t, address, []int32{0, 1})
cfg.ConsumeCycleDuration = time.Second
partitionRing := newPartitionRingReaderWithPartitions(map[int32]ring.PartitionDesc{
0: {Id: 0, State: ring.PartitionActive},
1: {Id: 1, State: ring.PartitionActive},
})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
// First, produce to partition 0
testkafka.SendReqWithOpts(ctx, t, client, ingest.Encode, testkafka.ReqOpts{Partition: 0, Time: reqTime, TenantID: util.FakeTenantID})
// And only then create block builder
b, err := New(cfg, testLogger(t), partitionRing, &mockOverrides{}, store)
require.NoError(t, err)
// Verify builder is listening to both partitions
parts := b.getAssignedPartitions()
require.ElementsMatch(t, []int32{0, 1}, parts)
require.NoError(t, services.StartAndAwaitRunning(ctx, b))
t.Cleanup(func() {
require.NoError(t, services.StopAndAwaitTerminated(ctx, b))
})
// after initial consumption, add more records
store.lock()
testkafka.SendReqWithOpts(ctx, t, client, ingest.Encode, testkafka.ReqOpts{Partition: 0, Time: reqTime, TenantID: util.FakeTenantID})
store.unlock()
// after processing the first partition, add more records to the second partition
store.lock()
testkafka.SendReqWithOpts(ctx, t, client, ingest.Encode, testkafka.ReqOpts{Partition: 1, Time: reqTime, TenantID: util.FakeTenantID})
store.unlock()
// wait for the second partition to finish
store.lock()
store.unlock()
// Wait for the block to be flushed (one block per each consumePartition call)
require.Eventually(t, func() bool {
return len(store.BlockMetas(util.FakeTenantID)) == 3 && countFlushedTraces(store) == 3
}, 20*time.Second, time.Second)
// Verify offsets
offsets, err := kadm.NewClient(client).FetchOffsetsForTopics(ctx, testConsumerGroup, testTopic)
require.NoError(t, err)
for partition, expectedOffset := range map[int32]int64{
0: 2,
1: 1,
} {
offset, ok := offsets.Lookup(testTopic, partition)
require.True(t, ok, "partition %d should have a committed offset", partition)
require.Equal(t, expectedOffset, offset.At)
}
}
// TestBlockbuilder_threePartitions_oneWithNoLag verifies that when multiple partitions
// are assigned and one partition has no lag (is caught up), the block builder efficiently
// processes only the partitions with data without wasting time polling the caught-up partition.
// This test ensures that a caught-up partition doesn't block processing of lagging partitions.
func TestBlockbuilder_PartitionWithNoLag(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
t.Cleanup(func() { cancel(errors.New("test done")) })
reqTime := time.Now().Add(-1 * time.Minute) // ensure records won't be filtered out
cycleDuration := 7 * time.Second // increase if test is flaky
_, address := testkafka.CreateCluster(t, 2, testTopic)
// Setup block-builder
storageWrites := atomic.NewInt32(0)
store := newStoreWrapper(newStore(ctx, t), func(ctx context.Context, block tempodb.WriteableBlock, store storage.Store) error {
storageWrites.Inc()
return store.WriteBlock(ctx, block)
})
cfg := blockbuilderConfig(t, address, []int32{0, 1})
cfg.ConsumeCycleDuration = cycleDuration
partitionRing := newPartitionRingReaderWithPartitions(map[int32]ring.PartitionDesc{
0: {Id: 0, State: ring.PartitionActive},
1: {Id: 1, State: ring.PartitionInactive},
})
client := testkafka.NewKafkaClient(t, cfg.IngestStorageConfig.Kafka.Address, cfg.IngestStorageConfig.Kafka.Topic)
// Produce data to partition 0
for range 10 {
testkafka.SendReqWithOpts(ctx, t, client, ingest.Encode, testkafka.ReqOpts{Partition: 0, Time: reqTime, TenantID: util.FakeTenantID})
}
time.Sleep(2 * cycleDuration) // simulate lag
var lastRecordTime time.Time
for range 10 {
rec := testkafka.SendReqWithOpts(ctx, t, client, ingest.Encode, testkafka.ReqOpts{Partition: 0, Time: reqTime, TenantID: util.FakeTenantID})
lastRecordTime = rec[len(rec)-1].Timestamp
}
// Produce and commit one record to partition 1 and commit it (simulating it's already caught up)
rec := testkafka.SendReqWithOpts(ctx, t, client, ingest.Encode, testkafka.ReqOpts{Partition: 1, Time: reqTime, TenantID: util.FakeTenantID})
offsets := make(kadm.Offsets)
offsets.Add(kadm.Offset{
Topic: testTopic,
Partition: 1,
At: rec[len(rec)-1].Offset + 1, // Commit past the last record
})
admClient := kadm.NewClient(client)
require.NoError(t, admClient.CommitAllOffsets(ctx, cfg.IngestStorageConfig.Kafka.ConsumerGroup, offsets))
// Create and start the block builder
b, err := New(cfg, testLogger(t), partitionRing, &mockOverrides{}, store)
require.NoError(t, err)
// Verify builder is listening to all partitions
parts := b.getAssignedPartitions()
require.ElementsMatch(t, []int32{0, 1}, parts)
require.NoError(t, b.starting(ctx))
res, err := b.consume(ctx)
require.NoError(t, err)
elapsed := time.Since(lastRecordTime)
assert.InDelta(t, cycleDuration-elapsed, res, float64(100*time.Millisecond))
assert.Less(t, res, cycleDuration)
// Verify offsets - all partitions should be committed
offsetsResult, err := admClient.FetchOffsetsForTopics(ctx, testConsumerGroup, testTopic)
require.NoError(t, err)
for partition, expectedCount := range map[int32]int64{
0: 20,
1: 1,
} {
offset, ok := offsetsResult.Lookup(testTopic, partition)
require.True(t, ok, "partition %d should have a committed offset", partition)
assert.Equal(t, expectedCount, offset.At, "partition %d should have correct committed offset", partition)
}
assert.Equal(t, int32(2), storageWrites.Load(), "unexpected number of storage writes")
}
func testLogger(_ testing.TB) log.Logger {
// Uncomment when we need full detail.
// return test.NewTestingLogger(t)
return log.NewNopLogger()
}