Skip to content

Commit 156ccde

Browse files
committed
[ENH]: soft delete databases, hard delete from garbage collector
1 parent cf6858c commit 156ccde

File tree

12 files changed

+215
-68
lines changed

12 files changed

+215
-68
lines changed

go/pkg/sysdb/coordinator/coordinator.go

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

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

67
"github.com/chroma-core/chroma/go/pkg/common"
78
"github.com/chroma-core/chroma/go/pkg/proto/coordinatorpb"
@@ -255,3 +256,15 @@ func (s *Coordinator) BatchGetCollectionVersionFilePaths(ctx context.Context, re
255256
func (s *Coordinator) BatchGetCollectionSoftDeleteStatus(ctx context.Context, req *coordinatorpb.BatchGetCollectionSoftDeleteStatusRequest) (*coordinatorpb.BatchGetCollectionSoftDeleteStatusResponse, error) {
256257
return s.catalog.BatchGetCollectionSoftDeleteStatus(ctx, req.CollectionIds)
257258
}
259+
260+
func (s *Coordinator) FinishDatabaseDeletion(ctx context.Context, req *coordinatorpb.FinishDatabaseDeletionRequest) (*coordinatorpb.FinishDatabaseDeletionResponse, error) {
261+
numDeleted, err := s.catalog.FinishDatabaseDeletion(ctx, time.Unix(req.CutoffTime.Seconds, int64(req.CutoffTime.Nanos)))
262+
if err != nil {
263+
return nil, err
264+
}
265+
266+
res := &coordinatorpb.FinishDatabaseDeletionResponse{
267+
NumDeleted: numDeleted,
268+
}
269+
return res, nil
270+
}

go/pkg/sysdb/coordinator/table_catalog.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -184,27 +184,36 @@ func (tc *Catalog) DeleteDatabase(ctx context.Context, deleteDatabase *model.Del
184184
if len(databases) == 0 {
185185
return common.ErrDatabaseNotFound
186186
}
187-
err = tc.metaDomain.DatabaseDb(txCtx).Delete(databases[0].ID)
187+
err = tc.metaDomain.DatabaseDb(txCtx).SoftDelete(databases[0].ID)
188188
if err != nil {
189189
return err
190190
}
191+
192+
collections, err := tc.metaDomain.CollectionDb(txCtx).GetCollections(nil, nil, deleteDatabase.Tenant, deleteDatabase.Name, nil, nil)
193+
if err != nil {
194+
return err
195+
}
196+
197+
for _, collection := range collections {
198+
collectionID, err := types.Parse(collection.Collection.ID)
199+
if err != nil {
200+
return err
201+
}
202+
203+
err = tc.softDeleteCollection(txCtx, &model.DeleteCollection{
204+
ID: collectionID,
205+
TenantID: deleteDatabase.Tenant,
206+
DatabaseName: deleteDatabase.Name,
207+
})
208+
if err != nil {
209+
return err
210+
}
211+
}
212+
191213
return nil
192214
})
193215
}
194216

195-
func (tc *Catalog) GetAllDatabases(ctx context.Context, ts types.Timestamp) ([]*model.Database, error) {
196-
databases, err := tc.metaDomain.DatabaseDb(ctx).GetAllDatabases()
197-
if err != nil {
198-
log.Error("error getting all databases", zap.Error(err))
199-
return nil, err
200-
}
201-
result := make([]*model.Database, 0, len(databases))
202-
for _, database := range databases {
203-
result = append(result, convertDatabaseToModel(database))
204-
}
205-
return result, nil
206-
}
207-
208217
func (tc *Catalog) CreateTenant(ctx context.Context, createTenant *model.CreateTenant, ts types.Timestamp) (*model.Tenant, error) {
209218
var result *model.Tenant
210219

@@ -664,7 +673,7 @@ func (tc *Catalog) softDeleteCollection(ctx context.Context, deleteCollection *m
664673

665674
// Generate new name with timestamp and random number
666675
oldName := *collections[0].Collection.Name
667-
newName := fmt.Sprintf("_deleted_%s_%s", oldName, *types.FromUniqueID(deleteCollection.ID))
676+
newName := fmt.Sprintf("_deleted_%s_%s", oldName, deleteCollection.ID.String())
668677

669678
dbCollection := &dbmodel.Collection{
670679
ID: deleteCollection.ID.String(),
@@ -2196,3 +2205,7 @@ func (tc *Catalog) GetVersionFileNamesForCollection(ctx context.Context, tenantI
21962205

21972206
return collectionEntry.VersionFileName, nil
21982207
}
2208+
2209+
func (tc *Catalog) FinishDatabaseDeletion(ctx context.Context, cutoffTime time.Time) (uint64, error) {
2210+
return tc.metaDomain.DatabaseDb(ctx).FinishDatabaseDeletion(cutoffTime)
2211+
}

go/pkg/sysdb/grpc/tenant_database_service.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,11 @@ func (s *Server) GetLastCompactionTimeForTenant(ctx context.Context, req *coordi
158158
}
159159
return res, nil
160160
}
161+
162+
func (s *Server) FinishDatabaseDeletion(ctx context.Context, req *coordinatorpb.FinishDatabaseDeletionRequest) (*coordinatorpb.FinishDatabaseDeletionResponse, error) {
163+
res, err := s.coordinator.FinishDatabaseDeletion(ctx, req)
164+
if err != nil {
165+
return nil, grpcutils.BuildInternalGrpcError(err.Error())
166+
}
167+
return res, nil
168+
}

go/pkg/sysdb/grpc/tenant_database_service_test.go

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/chroma-core/chroma/go/pkg/sysdb/metastore/db/dbcore"
1515
"github.com/chroma-core/chroma/go/pkg/sysdb/metastore/db/dbmodel"
1616
s3metastore "github.com/chroma-core/chroma/go/pkg/sysdb/metastore/s3"
17+
"github.com/chroma-core/chroma/go/pkg/types"
1718
"github.com/google/uuid"
1819
"github.com/pingcap/log"
1920
"github.com/stretchr/testify/suite"
@@ -116,7 +117,7 @@ func (suite *TenantDatabaseServiceTestSuite) TestServer_DeleteDatabase() {
116117
tenantName := "TestDeleteDatabase"
117118
databaseName := "TestDeleteDatabase"
118119
// Generate random uuid for db id
119-
databaseeId := uuid.New().String()
120+
databaseId := uuid.New().String()
120121

121122
_, err := suite.catalog.CreateTenant(context.Background(), &model.CreateTenant{
122123
Name: tenantName,
@@ -127,29 +128,61 @@ func (suite *TenantDatabaseServiceTestSuite) TestServer_DeleteDatabase() {
127128
_, err = suite.catalog.CreateDatabase(context.Background(), &model.CreateDatabase{
128129
Tenant: tenantName,
129130
Name: databaseName,
130-
ID: databaseeId,
131+
ID: databaseId,
131132
Ts: time.Now().Unix(),
132133
}, time.Now().Unix())
133134
suite.NoError(err)
134135

136+
collectionID := types.NewUniqueID()
135137
_, _, err = suite.catalog.CreateCollection(context.Background(), &model.CreateCollection{
138+
ID: collectionID,
136139
TenantID: tenantName,
137140
DatabaseName: databaseName,
138141
Name: "TestCollection",
139142
}, time.Now().Unix())
140143
suite.NoError(err)
141144

145+
timeBeforeSoftDelete := time.Now()
146+
142147
err = suite.catalog.DeleteDatabase(context.Background(), &model.DeleteDatabase{
143148
Tenant: tenantName,
144149
Name: databaseName,
145150
})
146151
suite.NoError(err)
147152

148-
// Check that associated collection was deleted
149-
var count int64
153+
// Check that associated collection was soft deleted
150154
var collections []*dbmodel.Collection
151-
suite.NoError(suite.db.Find(&collections).Count(&count).Error)
152-
suite.Equal(int64(0), count)
155+
suite.NoError(suite.db.Find(&collections).Error)
156+
suite.Equal(1, len(collections))
157+
suite.Equal(true, collections[0].IsDeleted)
158+
159+
// Database should not be eligible for hard deletion yet because it still has a (soft deleted) collection
160+
numDeleted, err := suite.catalog.FinishDatabaseDeletion(context.Background(), time.Now())
161+
suite.NoError(err)
162+
suite.Equal(uint64(0), numDeleted)
163+
164+
// Hard delete associated collection
165+
suite.NoError(err)
166+
suite.NoError(suite.catalog.DeleteCollection(context.Background(), &model.DeleteCollection{
167+
TenantID: tenantName,
168+
DatabaseName: databaseName,
169+
ID: collectionID,
170+
}, false))
171+
172+
// Database should now be eligible for hard deletion, but first verify that database is not deleted if cutoff time is prior to soft delete
173+
numDeleted, err = suite.catalog.FinishDatabaseDeletion(context.Background(), timeBeforeSoftDelete)
174+
suite.NoError(err)
175+
suite.Equal(uint64(0), numDeleted)
176+
177+
// Hard delete database
178+
numDeleted, err = suite.catalog.FinishDatabaseDeletion(context.Background(), time.Now())
179+
suite.NoError(err)
180+
suite.Equal(uint64(1), numDeleted)
181+
182+
// Verify that database is hard deleted
183+
var databases []*dbmodel.Database
184+
suite.NoError(suite.db.Debug().Where("id = ?", databaseId).Find(&databases).Error)
185+
suite.Equal(0, len(databases))
153186
}
154187

155188
func TestTenantDatabaseServiceTestSuite(t *testing.T) {

go/pkg/sysdb/metastore/db/dao/database.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dao
22

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

67
"github.com/chroma-core/chroma/go/pkg/common"
78
"github.com/chroma-core/chroma/go/pkg/sysdb/metastore/db/dbmodel"
@@ -28,21 +29,12 @@ func (s *databaseDb) DeleteByTenantIdAndName(tenantId string, databaseName strin
2829
return len(databases), err
2930
}
3031

31-
func (s *databaseDb) GetAllDatabases() ([]*dbmodel.Database, error) {
32-
var databases []*dbmodel.Database
33-
query := s.db.Table("databases")
34-
35-
if err := query.Find(&databases).Error; err != nil {
36-
return nil, err
37-
}
38-
return databases, nil
39-
}
40-
4132
func (s *databaseDb) ListDatabases(limit *int32, offset *int32, tenantID string) ([]*dbmodel.Database, error) {
4233
var databases []*dbmodel.Database
4334
query := s.db.Table("databases").
4435
Select("databases.id, databases.name, databases.tenant_id").
4536
Where("databases.tenant_id = ?", tenantID).
37+
Where("databases.is_deleted = ?", false).
4638
Order("databases.created_at ASC")
4739

4840
if limit != nil {
@@ -65,7 +57,8 @@ func (s *databaseDb) GetDatabases(tenantID string, databaseName string) ([]*dbmo
6557
query := s.db.Table("databases").
6658
Select("databases.id, databases.name, databases.tenant_id").
6759
Where("databases.name = ?", databaseName).
68-
Where("databases.tenant_id = ?", tenantID)
60+
Where("databases.tenant_id = ?", tenantID).
61+
Where("databases.is_deleted = ?", false)
6962

7063
if err := query.Find(&databases).Error; err != nil {
7164
log.Error("GetDatabases", zap.Error(err))
@@ -95,13 +88,13 @@ func (s *databaseDb) Insert(database *dbmodel.Database) error {
9588
return err
9689
}
9790

98-
func (s *databaseDb) Delete(databaseID string) error {
91+
func (s *databaseDb) SoftDelete(databaseID string) error {
9992
return s.db.Transaction(func(tx *gorm.DB) error {
100-
if err := tx.Where("id = ?", databaseID).Delete(&dbmodel.Database{}).Error; err != nil {
101-
return err
102-
}
103-
104-
if err := tx.Where("database_id = ?", databaseID).Delete(&dbmodel.Collection{}).Error; err != nil {
93+
if err := tx.Table("databases").
94+
Where("id = ?", databaseID).
95+
Update("is_deleted", true).
96+
Update("updated_at", time.Now()).
97+
Error; err != nil {
10598
return err
10699
}
107100

@@ -113,11 +106,44 @@ func (s *databaseDb) GetDatabasesByTenantID(tenantID string) ([]*dbmodel.Databas
113106
var databases []*dbmodel.Database
114107
query := s.db.Table("databases").
115108
Select("databases.id, databases.name, databases.tenant_id").
116-
Where("databases.tenant_id = ?", tenantID)
109+
Where("databases.tenant_id = ?", tenantID).
110+
Where("databases.is_deleted = ?", false)
117111

118112
if err := query.Find(&databases).Error; err != nil {
119113
log.Error("GetDatabasesByTenantID", zap.Error(err))
120114
return nil, err
121115
}
122116
return databases, nil
123117
}
118+
119+
func (s *databaseDb) FinishDatabaseDeletion(cutoffTime time.Time) (uint64, error) {
120+
numDeleted := uint64(0)
121+
122+
for {
123+
// Only hard delete databases that were soft deleted prior to the cutoff time and have no collections
124+
databasesSubQuery := s.db.
125+
Table("databases d").
126+
Select("d.id").
127+
Joins("LEFT JOIN collections c ON c.database_id = d.id").
128+
Where("d.is_deleted = ?", true).
129+
Where("d.updated_at < ?", cutoffTime).
130+
Group("d.id").
131+
Having("COUNT(c.id) = 0").
132+
Limit(1000)
133+
134+
res := s.db.Table("databases").
135+
Where("id IN (?)", databasesSubQuery).
136+
Delete(&dbmodel.Database{})
137+
if res.Error != nil {
138+
return numDeleted, res.Error
139+
}
140+
141+
numDeleted += uint64(res.RowsAffected)
142+
143+
if res.RowsAffected == 0 {
144+
break
145+
}
146+
}
147+
148+
return numDeleted, nil
149+
}

go/pkg/sysdb/metastore/db/dbmodel/database.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ func (v Database) TableName() string {
2222

2323
//go:generate mockery --name=IDatabaseDb
2424
type IDatabaseDb interface {
25-
GetAllDatabases() ([]*Database, error)
2625
GetDatabases(tenantID string, databaseName string) ([]*Database, error)
2726
ListDatabases(limit *int32, offset *int32, tenantID string) ([]*Database, error)
2827
Insert(in *Database) error
2928
DeleteAll() error
30-
Delete(databaseID string) error
29+
SoftDelete(databaseID string) error
30+
FinishDatabaseDeletion(cutoffTime time.Time) (uint64, error)
3131
}

idl/chromadb/proto/coordinator.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ message DeleteDatabaseRequest {
4646

4747
message DeleteDatabaseResponse {}
4848

49+
message FinishDatabaseDeletionRequest {
50+
google.protobuf.Timestamp cutoff_time = 1;
51+
}
52+
53+
message FinishDatabaseDeletionResponse {
54+
uint64 num_deleted = 1;
55+
}
56+
4957
message CreateTenantRequest {
5058
string name = 2; // Names are globally unique
5159
}
@@ -496,6 +504,7 @@ service SysDB {
496504
rpc GetDatabase(GetDatabaseRequest) returns (GetDatabaseResponse) {}
497505
rpc ListDatabases(ListDatabasesRequest) returns (ListDatabasesResponse) {}
498506
rpc DeleteDatabase(DeleteDatabaseRequest) returns (DeleteDatabaseResponse) {}
507+
rpc FinishDatabaseDeletion(FinishDatabaseDeletionRequest) returns (FinishDatabaseDeletionResponse) {}
499508
rpc CreateTenant(CreateTenantRequest) returns (CreateTenantResponse) {}
500509
rpc GetTenant(GetTenantRequest) returns (GetTenantResponse) {}
501510
rpc CreateSegment(CreateSegmentRequest) returns (CreateSegmentResponse) {}

rust/garbage_collector/src/garbage_collector_orchestrator_v2.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use chroma_system::{
3434
use chroma_types::chroma_proto::{CollectionVersionFile, VersionListForCollection};
3535
use chroma_types::{
3636
BatchGetCollectionSoftDeleteStatusError, CollectionUuid, DeleteCollectionError,
37+
DeleteDatabaseError,
3738
};
3839
use chrono::{DateTime, Utc};
3940
use std::collections::{HashMap, HashSet};
@@ -158,6 +159,8 @@ pub enum GarbageCollectorError {
158159
UnparsableUuid(#[from] uuid::Error),
159160
#[error("Collection deletion failed: {0}")]
160161
CollectionDeletionFailed(#[from] DeleteCollectionError),
162+
#[error("Database deletion failed: {0}")]
163+
DeleteDatabaseFailed(#[from] DeleteDatabaseError),
161164
}
162165

163166
impl ChromaError for GarbageCollectorError {
@@ -716,6 +719,19 @@ impl GarbageCollectorOrchestrator {
716719

717720
self.num_pending_tasks -= 1;
718721
if self.num_pending_tasks == 0 {
722+
let tenant = self
723+
.tenant
724+
.clone()
725+
.ok_or(GarbageCollectorError::InvariantViolation(
726+
"Expected tenant to be set".to_string(),
727+
))?;
728+
let database_name =
729+
self.database_name
730+
.clone()
731+
.ok_or(GarbageCollectorError::InvariantViolation(
732+
"Expected database to be set".to_string(),
733+
))?;
734+
719735
for collection_id in self.soft_deleted_collections_to_gc.iter() {
720736
let graph =
721737
self.graph
@@ -758,16 +774,8 @@ impl GarbageCollectorOrchestrator {
758774
if are_all_children_in_fork_tree_also_soft_deleted {
759775
self.sysdb_client
760776
.finish_collection_deletion(
761-
self.tenant.clone().ok_or(
762-
GarbageCollectorError::InvariantViolation(
763-
"Expected tenant to be set".to_string(),
764-
),
765-
)?,
766-
self.database_name.clone().ok_or(
767-
GarbageCollectorError::InvariantViolation(
768-
"Expected database to be set".to_string(),
769-
),
770-
)?,
777+
tenant.clone(),
778+
database_name.clone(),
771779
*collection_id,
772780
)
773781
.await?;

0 commit comments

Comments
 (0)