@ -187,7 +187,7 @@ func (noopWriter) Close() error { return nil }
type noopCleaner struct { }
func ( noopCleaner ) Cleanup ( series ID [ ] byte , userID [ ] byte ) error { return nil }
func ( noopCleaner ) Cleanup ( u serID [ ] byte , lbls labels . Labels ) error { return nil }
func Test_EmptyTable ( t * testing . T ) {
schema := allSchemas [ 0 ]
@ -207,7 +207,7 @@ func Test_EmptyTable(t *testing.T) {
err := tables [ 0 ] . DB . Update ( func ( tx * bbolt . Tx ) error {
it , err := newChunkIndexIterator ( tx . Bucket ( bucketName ) , schema . config )
require . NoError ( t , err )
empty , err := markforDelete ( context . Background ( ) , noopWriter { } , it , noopCleaner { } ,
empty , err := markforDelete ( context . Background ( ) , tables [ 0 ] . name , noopWriter { } , it , noopCleaner { } ,
NewExpirationChecker ( & fakeLimits { perTenant : map [ string ] retentionLimit { "1" : { retentionPeriod : 0 } , "2" : { retentionPeriod : 0 } } } ) , nil )
require . NoError ( t , err )
require . True ( t , empty )
@ -407,3 +407,292 @@ func TestChunkRewriter(t *testing.T) {
} )
}
}
type seriesCleanedRecorder struct {
// map of userID -> map of labels hash -> struct{}
deletedSeries map [ string ] map [ uint64 ] struct { }
}
func newSeriesCleanRecorder ( ) * seriesCleanedRecorder {
return & seriesCleanedRecorder { map [ string ] map [ uint64 ] struct { } { } }
}
func ( s * seriesCleanedRecorder ) Cleanup ( userID [ ] byte , lbls labels . Labels ) error {
s . deletedSeries [ string ( userID ) ] = map [ uint64 ] struct { } { lbls . Hash ( ) : { } }
return nil
}
type chunkExpiry struct {
isExpired bool
nonDeletedIntervals [ ] model . Interval
}
type mockExpirationChecker struct {
ExpirationChecker
chunksExpiry map [ string ] chunkExpiry
}
func newMockExpirationChecker ( chunksExpiry map [ string ] chunkExpiry ) mockExpirationChecker {
return mockExpirationChecker { chunksExpiry : chunksExpiry }
}
func ( m mockExpirationChecker ) Expired ( ref ChunkEntry , now model . Time ) ( bool , [ ] model . Interval ) {
ce := m . chunksExpiry [ string ( ref . ChunkID ) ]
return ce . isExpired , ce . nonDeletedIntervals
}
func ( m mockExpirationChecker ) DropFromIndex ( ref ChunkEntry , tableEndTime model . Time , now model . Time ) bool {
return false
}
func TestMarkForDelete_SeriesCleanup ( t * testing . T ) {
now := model . Now ( )
schema := allSchemas [ 2 ]
userID := "1"
todaysTableInterval := ExtractIntervalFromTableName ( schema . config . IndexTables . TableFor ( now ) )
for _ , tc := range [ ] struct {
name string
chunks [ ] chunk . Chunk
expiry [ ] chunkExpiry
expectedDeletedSeries [ ] map [ uint64 ] struct { }
expectedEmpty [ ] bool
} {
{
name : "no chunk and series deleted" ,
chunks : [ ] chunk . Chunk {
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , now . Add ( - 30 * time . Minute ) , now ) ,
} ,
expiry : [ ] chunkExpiry {
{
isExpired : false ,
} ,
} ,
expectedDeletedSeries : [ ] map [ uint64 ] struct { } {
nil ,
} ,
expectedEmpty : [ ] bool {
false ,
} ,
} ,
{
name : "only one chunk in store which gets deleted" ,
chunks : [ ] chunk . Chunk {
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , now . Add ( - 30 * time . Minute ) , now ) ,
} ,
expiry : [ ] chunkExpiry {
{
isExpired : true ,
} ,
} ,
expectedDeletedSeries : [ ] map [ uint64 ] struct { } {
nil ,
} ,
expectedEmpty : [ ] bool {
true ,
} ,
} ,
{
name : "only one chunk in store which gets partially deleted" ,
chunks : [ ] chunk . Chunk {
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , now . Add ( - 30 * time . Minute ) , now ) ,
} ,
expiry : [ ] chunkExpiry {
{
isExpired : true ,
nonDeletedIntervals : [ ] model . Interval { {
Start : now . Add ( - 15 * time . Minute ) ,
End : now ,
} } ,
} ,
} ,
expectedDeletedSeries : [ ] map [ uint64 ] struct { } {
nil ,
} ,
expectedEmpty : [ ] bool {
false ,
} ,
} ,
{
name : "one of two chunks deleted" ,
chunks : [ ] chunk . Chunk {
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , now . Add ( - 30 * time . Minute ) , now ) ,
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "2" } } , now . Add ( - 30 * time . Minute ) , now ) ,
} ,
expiry : [ ] chunkExpiry {
{
isExpired : false ,
} ,
{
isExpired : true ,
} ,
} ,
expectedDeletedSeries : [ ] map [ uint64 ] struct { } {
{ labels . Labels { labels . Label { Name : "foo" , Value : "2" } } . Hash ( ) : struct { } { } } ,
} ,
expectedEmpty : [ ] bool {
false ,
} ,
} ,
{
name : "one of two chunks partially deleted" ,
chunks : [ ] chunk . Chunk {
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , now . Add ( - 30 * time . Minute ) , now ) ,
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "2" } } , now . Add ( - 30 * time . Minute ) , now ) ,
} ,
expiry : [ ] chunkExpiry {
{
isExpired : false ,
} ,
{
isExpired : true ,
nonDeletedIntervals : [ ] model . Interval { {
Start : now . Add ( - 15 * time . Minute ) ,
End : now ,
} } ,
} ,
} ,
expectedDeletedSeries : [ ] map [ uint64 ] struct { } {
nil ,
} ,
expectedEmpty : [ ] bool {
false ,
} ,
} ,
{
name : "one big chunk partially deleted for yesterdays table without rewrite" ,
chunks : [ ] chunk . Chunk {
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , todaysTableInterval . Start . Add ( - time . Hour ) , now ) ,
} ,
expiry : [ ] chunkExpiry {
{
isExpired : true ,
nonDeletedIntervals : [ ] model . Interval { {
Start : todaysTableInterval . Start ,
End : now ,
} } ,
} ,
} ,
expectedDeletedSeries : [ ] map [ uint64 ] struct { } {
nil , nil ,
} ,
expectedEmpty : [ ] bool {
true , false ,
} ,
} ,
{
name : "one big chunk partially deleted for yesterdays table with rewrite" ,
chunks : [ ] chunk . Chunk {
createChunk ( t , userID , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , todaysTableInterval . Start . Add ( - time . Hour ) , now ) ,
} ,
expiry : [ ] chunkExpiry {
{
isExpired : true ,
nonDeletedIntervals : [ ] model . Interval { {
Start : todaysTableInterval . Start . Add ( - 30 * time . Minute ) ,
End : now ,
} } ,
} ,
} ,
expectedDeletedSeries : [ ] map [ uint64 ] struct { } {
nil , nil ,
} ,
expectedEmpty : [ ] bool {
false , false ,
} ,
} ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
store := newTestStore ( t )
require . NoError ( t , store . Put ( context . TODO ( ) , tc . chunks ) )
chunksExpiry := map [ string ] chunkExpiry { }
for i , chunk := range tc . chunks {
chunksExpiry [ chunk . ExternalKey ( ) ] = tc . expiry [ i ]
}
expirationChecker := newMockExpirationChecker ( chunksExpiry )
store . Stop ( )
tables := store . indexTables ( )
require . Len ( t , tables , len ( tc . expectedDeletedSeries ) )
chunkClient := objectclient . NewClient ( newTestObjectClient ( store . chunkDir ) , objectclient . Base64Encoder )
for i , table := range tables {
seriesCleanRecorder := newSeriesCleanRecorder ( )
err := table . DB . Update ( func ( tx * bbolt . Tx ) error {
it , err := newChunkIndexIterator ( tx . Bucket ( bucketName ) , schema . config )
require . NoError ( t , err )
cr , err := newChunkRewriter ( chunkClient , schema . config , table . name , tx . Bucket ( bucketName ) )
require . NoError ( t , err )
empty , err := markforDelete ( context . Background ( ) , table . name , noopWriter { } , it , seriesCleanRecorder ,
expirationChecker , cr )
require . NoError ( t , err )
require . Equal ( t , tc . expectedEmpty [ i ] , empty )
return nil
} )
require . NoError ( t , err )
require . EqualValues ( t , tc . expectedDeletedSeries [ i ] , seriesCleanRecorder . deletedSeries [ userID ] )
}
} )
}
}
func TestMarkForDelete_DropChunkFromIndex ( t * testing . T ) {
schema := allSchemas [ 2 ]
store := newTestStore ( t )
now := model . Now ( )
todaysTableInterval := ExtractIntervalFromTableName ( schema . config . IndexTables . TableFor ( now ) )
retentionPeriod := now . Sub ( todaysTableInterval . Start ) / 2
// chunks in retention
c1 := createChunk ( t , "1" , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , todaysTableInterval . Start , now )
c2 := createChunk ( t , "1" , labels . Labels { labels . Label { Name : "foo" , Value : "2" } } , todaysTableInterval . Start . Add ( - 7 * 24 * time . Hour ) , now )
// chunks out of retention
c3 := createChunk ( t , "1" , labels . Labels { labels . Label { Name : "foo" , Value : "1" } } , todaysTableInterval . Start , now . Add ( - retentionPeriod ) )
c4 := createChunk ( t , "1" , labels . Labels { labels . Label { Name : "foo" , Value : "3" } } , todaysTableInterval . Start . Add ( - 12 * time . Hour ) , todaysTableInterval . Start . Add ( - 10 * time . Hour ) )
c5 := createChunk ( t , "1" , labels . Labels { labels . Label { Name : "foo" , Value : "4" } } , todaysTableInterval . Start , now . Add ( - retentionPeriod ) )
require . NoError ( t , store . Put ( context . TODO ( ) , [ ] chunk . Chunk {
c1 , c2 , c3 , c4 , c5 ,
} ) )
store . Stop ( )
tables := store . indexTables ( )
require . Len ( t , tables , 8 )
for i , table := range tables {
err := table . DB . Update ( func ( tx * bbolt . Tx ) error {
it , err := newChunkIndexIterator ( tx . Bucket ( bucketName ) , schema . config )
require . NoError ( t , err )
empty , err := markforDelete ( context . Background ( ) , table . name , noopWriter { } , it , noopCleaner { } ,
NewExpirationChecker ( fakeLimits { perTenant : map [ string ] retentionLimit { "1" : { retentionPeriod : retentionPeriod } } } ) , nil )
require . NoError ( t , err )
if i == 7 {
require . False ( t , empty )
} else {
require . True ( t , empty , "table %s must be empty" , table . name )
}
return nil
} )
require . NoError ( t , err )
require . NoError ( t , table . Close ( ) )
}
store . open ( )
// verify the chunks which were not supposed to be deleted are still there
require . True ( t , store . HasChunk ( c1 ) )
require . True ( t , store . HasChunk ( c2 ) )
// verify the chunks which were supposed to be deleted are gone
require . False ( t , store . HasChunk ( c3 ) )
require . False ( t , store . HasChunk ( c4 ) )
require . False ( t , store . HasChunk ( c5 ) )
}