github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/mysql/revisions.go (about) 1 package mysql 2 3 import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "time" 9 10 "github.com/authzed/spicedb/internal/datastore/revisions" 11 "github.com/authzed/spicedb/pkg/datastore" 12 ) 13 14 var ParseRevisionString = revisions.RevisionParser(revisions.TransactionID) 15 16 const ( 17 errRevision = "unable to find revision: %w" 18 errCheckRevision = "unable to check revision: %w" 19 20 // querySelectRevision will round the database's timestamp down to the nearest 21 // quantization period, and then find the first transaction after that. If there 22 // are no transactions newer than the quantization period, it just picks the latest 23 // transaction. It will also return the amount of nanoseconds until the next 24 // optimized revision would be selected server-side, for use with caching. 25 // 26 // %[1] Name of id column 27 // %[2] Relationship tuple transaction table 28 // %[3] Name of timestamp column 29 // %[4] Quantization period (in nanoseconds) 30 querySelectRevision = `SELECT COALESCE(( 31 SELECT MIN(%[1]s) 32 FROM %[2]s 33 WHERE %[3]s >= FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(UTC_TIMESTAMP(6)) * 1000000000 / %[4]d) * %[4]d / 1000000000) 34 ), ( 35 SELECT MAX(%[1]s) 36 FROM %[2]s 37 )) as revision, 38 %[4]d - CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(6)) * 1000000000 AS UNSIGNED INTEGER) %% %[4]d as validForNanos;` 39 40 // queryValidTransaction will return a single row with two values, one boolean 41 // for whether the specified transaction ID is newer than the garbage collection 42 // window, and one boolean for whether the transaction ID represents a transaction 43 // that will occur in the future. 44 // It treats the current head transaction as always valid even if it falls 45 // outside the GC window. 46 // 47 // %[1] Name of id column 48 // %[2] Relationship tuple transaction table 49 // %[3] Name of timestamp column 50 // %[4] Inverse of GC window (in seconds) 51 queryValidTransaction = ` 52 SELECT ? >= COALESCE(( 53 SELECT MIN(%[1]s) 54 FROM %[2]s 55 WHERE %[3]s >= TIMESTAMPADD(SECOND, %.6[4]f, UTC_TIMESTAMP(6)) 56 ),( 57 SELECT MAX(%[1]s) 58 FROM %[2]s 59 LIMIT 1 60 )) as fresh, ? > ( 61 SELECT MAX(%[1]s) 62 FROM %[2]s 63 ) as unknown;` 64 ) 65 66 func (mds *Datastore) optimizedRevisionFunc(ctx context.Context) (datastore.Revision, time.Duration, error) { 67 var rev uint64 68 var validForNanos time.Duration 69 if err := mds.db.QueryRowContext(ctx, mds.optimizedRevisionQuery). 70 Scan(&rev, &validForNanos); err != nil { 71 return datastore.NoRevision, 0, fmt.Errorf(errRevision, err) 72 } 73 return revisions.NewForTransactionID(rev), validForNanos, nil 74 } 75 76 func (mds *Datastore) HeadRevision(ctx context.Context) (datastore.Revision, error) { 77 // implementation deviates slightly from PSQL implementation in order to support 78 // database seeding in runtime, instead of through migrate command 79 revision, err := mds.loadRevision(ctx) 80 if err != nil { 81 return datastore.NoRevision, err 82 } 83 if revision == 0 { 84 return datastore.NoRevision, nil 85 } 86 87 return revisions.NewForTransactionID(revision), nil 88 } 89 90 func (mds *Datastore) CheckRevision(ctx context.Context, revision datastore.Revision) error { 91 if revision == datastore.NoRevision { 92 return datastore.NewInvalidRevisionErr(revision, datastore.CouldNotDetermineRevision) 93 } 94 95 rev, ok := revision.(revisions.TransactionIDRevision) 96 if !ok { 97 return fmt.Errorf("expected transaction revision, got %T", revision) 98 } 99 100 revisionTx := rev.TransactionID() 101 freshEnough, unknown, err := mds.checkValidTransaction(ctx, revisionTx) 102 if err != nil { 103 return fmt.Errorf(errCheckRevision, err) 104 } 105 106 if !freshEnough { 107 return datastore.NewInvalidRevisionErr(revision, datastore.RevisionStale) 108 } 109 if unknown { 110 return datastore.NewInvalidRevisionErr(revision, datastore.CouldNotDetermineRevision) 111 } 112 113 return nil 114 } 115 116 func (mds *Datastore) loadRevision(ctx context.Context) (uint64, error) { 117 // slightly changed to support no revisions at all, needed for runtime seeding of first transaction 118 ctx, span := tracer.Start(ctx, "loadRevision") 119 defer span.End() 120 121 query, args, err := mds.GetLastRevision.ToSql() 122 if err != nil { 123 return 0, fmt.Errorf(errRevision, err) 124 } 125 126 var revision *uint64 127 err = mds.db.QueryRowContext(ctx, query, args...).Scan(&revision) 128 if err != nil { 129 if errors.Is(err, sql.ErrNoRows) { 130 return 0, nil 131 } 132 return 0, fmt.Errorf(errRevision, err) 133 } 134 135 if revision == nil { 136 return 0, nil 137 } 138 139 return *revision, nil 140 } 141 142 func (mds *Datastore) checkValidTransaction(ctx context.Context, revisionTx uint64) (bool, bool, error) { 143 ctx, span := tracer.Start(ctx, "checkValidTransaction") 144 defer span.End() 145 146 var freshEnough, unknown sql.NullBool 147 148 err := mds.db.QueryRowContext(ctx, mds.validTransactionQuery, revisionTx, revisionTx). 149 Scan(&freshEnough, &unknown) 150 if err != nil { 151 return false, false, fmt.Errorf(errCheckRevision, err) 152 } 153 154 span.AddEvent("DB returned validTransaction checks") 155 156 return freshEnough.Bool, unknown.Bool, nil 157 } 158 159 func (mds *Datastore) createNewTransaction(ctx context.Context, tx *sql.Tx) (newTxnID uint64, err error) { 160 ctx, span := tracer.Start(ctx, "createNewTransaction") 161 defer span.End() 162 163 createQuery := mds.createTxn 164 if err != nil { 165 return 0, fmt.Errorf("createNewTransaction: %w", err) 166 } 167 168 result, err := tx.ExecContext(ctx, createQuery) 169 if err != nil { 170 return 0, fmt.Errorf("createNewTransaction: %w", err) 171 } 172 173 lastInsertID, err := result.LastInsertId() 174 if err != nil { 175 return 0, fmt.Errorf("createNewTransaction: failed to get last inserted id: %w", err) 176 } 177 178 return uint64(lastInsertID), nil 179 }