github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/mysql/gc.go (about) 1 package mysql 2 3 import ( 4 "context" 5 "database/sql" 6 "time" 7 8 sq "github.com/Masterminds/squirrel" 9 10 "github.com/authzed/spicedb/internal/datastore/common" 11 "github.com/authzed/spicedb/internal/datastore/revisions" 12 log "github.com/authzed/spicedb/internal/logging" 13 "github.com/authzed/spicedb/pkg/datastore" 14 ) 15 16 var _ common.GarbageCollector = (*Datastore)(nil) 17 18 func (mds *Datastore) HasGCRun() bool { 19 return mds.gcHasRun.Load() 20 } 21 22 func (mds *Datastore) MarkGCCompleted() { 23 mds.gcHasRun.Store(true) 24 } 25 26 func (mds *Datastore) ResetGCCompleted() { 27 mds.gcHasRun.Store(false) 28 } 29 30 func (mds *Datastore) Now(ctx context.Context) (time.Time, error) { 31 // Retrieve the `now` time from the database. 32 nowSQL, nowArgs, err := getNow.ToSql() 33 if err != nil { 34 return time.Time{}, err 35 } 36 37 var now time.Time 38 err = mds.db.QueryRowContext(ctx, nowSQL, nowArgs...).Scan(&now) 39 if err != nil { 40 return time.Time{}, err 41 } 42 43 // This conversion should just be for convenience while debugging -- 44 // MySQL and the driver do properly timezones properly. 45 return now.UTC(), nil 46 } 47 48 // - main difference is how the PSQL driver handles null values 49 func (mds *Datastore) TxIDBefore(ctx context.Context, before time.Time) (datastore.Revision, error) { 50 // Find the highest transaction ID before the GC window. 51 query, args, err := mds.GetLastRevision.Where(sq.Lt{colTimestamp: before}).ToSql() 52 if err != nil { 53 return datastore.NoRevision, err 54 } 55 56 var value sql.NullInt64 57 err = mds.db.QueryRowContext(ctx, query, args...).Scan(&value) 58 if err != nil { 59 return datastore.NoRevision, err 60 } 61 62 if !value.Valid { 63 log.Ctx(ctx).Debug().Time("before", before).Msg("no stale transactions found in the datastore") 64 return datastore.NoRevision, nil 65 } 66 67 return revisions.NewForTransactionID(uint64(value.Int64)), nil 68 } 69 70 // - implementation misses metrics 71 func (mds *Datastore) DeleteBeforeTx( 72 ctx context.Context, 73 txID datastore.Revision, 74 ) (removed common.DeletionCounts, err error) { 75 // Delete any relationship rows with deleted_transaction <= the transaction ID. 76 removed.Relationships, err = mds.batchDelete(ctx, mds.driver.RelationTuple(), sq.LtOrEq{colDeletedTxn: txID}) 77 if err != nil { 78 return 79 } 80 81 // Delete all transaction rows with ID < the transaction ID. 82 // 83 // We don't delete the transaction itself to ensure there is always at least 84 // one transaction present. 85 removed.Transactions, err = mds.batchDelete(ctx, mds.driver.RelationTupleTransaction(), sq.Lt{colID: txID}) 86 if err != nil { 87 return 88 } 89 90 // Delete any namespace rows with deleted_transaction <= the transaction ID. 91 removed.Namespaces, err = mds.batchDelete(ctx, mds.driver.Namespace(), sq.LtOrEq{colDeletedTxn: txID}) 92 return 93 } 94 95 // - query was reworked to make it compatible with Vitess 96 // - API differences with PSQL driver 97 func (mds *Datastore) batchDelete(ctx context.Context, tableName string, filter sqlFilter) (int64, error) { 98 query, args, err := sb.Delete(tableName).Where(filter).Limit(batchDeleteSize).ToSql() 99 if err != nil { 100 return -1, err 101 } 102 103 var deletedCount int64 104 for { 105 cr, err := mds.db.ExecContext(ctx, query, args...) 106 if err != nil { 107 return deletedCount, err 108 } 109 110 rowsDeleted, err := cr.RowsAffected() 111 if err != nil { 112 return deletedCount, err 113 } 114 deletedCount += rowsDeleted 115 if rowsDeleted < batchDeleteSize { 116 break 117 } 118 } 119 120 return deletedCount, nil 121 }