github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/mysql/watch.go (about) 1 package mysql 2 3 import ( 4 "context" 5 "errors" 6 "time" 7 8 "github.com/authzed/spicedb/internal/datastore/common" 9 "github.com/authzed/spicedb/internal/datastore/revisions" 10 "github.com/authzed/spicedb/pkg/datastore" 11 core "github.com/authzed/spicedb/pkg/proto/core/v1" 12 13 sq "github.com/Masterminds/squirrel" 14 ) 15 16 const ( 17 watchSleep = 100 * time.Millisecond 18 ) 19 20 // Watch notifies the caller about all changes to tuples. 21 // 22 // All events following afterRevision will be sent to the caller. 23 func (mds *Datastore) Watch(ctx context.Context, afterRevisionRaw datastore.Revision, options datastore.WatchOptions) (<-chan *datastore.RevisionChanges, <-chan error) { 24 watchBufferLength := options.WatchBufferLength 25 if watchBufferLength <= 0 { 26 watchBufferLength = mds.watchBufferLength 27 } 28 29 updates := make(chan *datastore.RevisionChanges, watchBufferLength) 30 errs := make(chan error, 1) 31 32 if options.Content&datastore.WatchSchema == datastore.WatchSchema { 33 errs <- errors.New("schema watch unsupported in MySQL") 34 return updates, errs 35 } 36 37 afterRevision, ok := afterRevisionRaw.(revisions.TransactionIDRevision) 38 if !ok { 39 errs <- datastore.NewInvalidRevisionErr(afterRevisionRaw, datastore.CouldNotDetermineRevision) 40 return updates, errs 41 } 42 43 watchBufferWriteTimeout := options.WatchBufferWriteTimeout 44 if watchBufferWriteTimeout <= 0 { 45 watchBufferWriteTimeout = mds.watchBufferWriteTimeout 46 } 47 48 sendChange := func(change *datastore.RevisionChanges) bool { 49 select { 50 case updates <- change: 51 return true 52 53 default: 54 // If we cannot immediately write, setup the timer and try again. 55 } 56 57 timer := time.NewTimer(watchBufferWriteTimeout) 58 defer timer.Stop() 59 60 select { 61 case updates <- change: 62 return true 63 64 case <-timer.C: 65 errs <- datastore.NewWatchDisconnectedErr() 66 return false 67 } 68 } 69 70 go func() { 71 defer close(updates) 72 defer close(errs) 73 74 currentTxn := afterRevision.TransactionID() 75 for { 76 var stagedUpdates []datastore.RevisionChanges 77 var err error 78 stagedUpdates, currentTxn, err = mds.loadChanges(ctx, currentTxn, options) 79 if err != nil { 80 if errors.Is(ctx.Err(), context.Canceled) { 81 errs <- datastore.NewWatchCanceledErr() 82 } else { 83 errs <- err 84 } 85 return 86 } 87 88 // Write the staged updates to the channel 89 for _, changeToWrite := range stagedUpdates { 90 changeToWrite := changeToWrite 91 if !sendChange(&changeToWrite) { 92 return 93 } 94 } 95 96 // If there were no changes, sleep a bit 97 if len(stagedUpdates) == 0 { 98 sleep := time.NewTimer(watchSleep) 99 100 select { 101 case <-sleep.C: 102 break 103 case <-ctx.Done(): 104 errs <- datastore.NewWatchCanceledErr() 105 return 106 } 107 } 108 } 109 }() 110 111 return updates, errs 112 } 113 114 func (mds *Datastore) loadChanges( 115 ctx context.Context, 116 afterRevision uint64, 117 options datastore.WatchOptions, 118 ) (changes []datastore.RevisionChanges, newRevision uint64, err error) { 119 newRevision, err = mds.loadRevision(ctx) 120 if err != nil { 121 return 122 } 123 124 if newRevision == afterRevision { 125 return 126 } 127 128 sql, args, err := mds.QueryChangedQuery.Where(sq.Or{ 129 sq.And{ 130 sq.Gt{colCreatedTxn: afterRevision}, 131 sq.LtOrEq{colCreatedTxn: newRevision}, 132 }, 133 sq.And{ 134 sq.Gt{colDeletedTxn: afterRevision}, 135 sq.LtOrEq{colDeletedTxn: newRevision}, 136 }, 137 }).ToSql() 138 if err != nil { 139 return 140 } 141 142 rows, err := mds.db.QueryContext(ctx, sql, args...) 143 if err != nil { 144 if errors.Is(err, context.Canceled) { 145 err = datastore.NewWatchCanceledErr() 146 } 147 return 148 } 149 defer common.LogOnError(ctx, rows.Close) 150 151 stagedChanges := common.NewChanges(revisions.TransactionIDKeyFunc, options.Content) 152 153 for rows.Next() { 154 nextTuple := &core.RelationTuple{ 155 ResourceAndRelation: &core.ObjectAndRelation{}, 156 Subject: &core.ObjectAndRelation{}, 157 } 158 159 var createdTxn uint64 160 var deletedTxn uint64 161 var caveatName string 162 var caveatContext caveatContextWrapper 163 err = rows.Scan( 164 &nextTuple.ResourceAndRelation.Namespace, 165 &nextTuple.ResourceAndRelation.ObjectId, 166 &nextTuple.ResourceAndRelation.Relation, 167 &nextTuple.Subject.Namespace, 168 &nextTuple.Subject.ObjectId, 169 &nextTuple.Subject.Relation, 170 &caveatName, 171 &caveatContext, 172 &createdTxn, 173 &deletedTxn, 174 ) 175 if err != nil { 176 return 177 } 178 nextTuple.Caveat, err = common.ContextualizedCaveatFrom(caveatName, caveatContext) 179 if err != nil { 180 return 181 } 182 183 if createdTxn > afterRevision && createdTxn <= newRevision { 184 if err = stagedChanges.AddRelationshipChange(ctx, revisions.NewForTransactionID(createdTxn), nextTuple, core.RelationTupleUpdate_TOUCH); err != nil { 185 return 186 } 187 } 188 189 if deletedTxn > afterRevision && deletedTxn <= newRevision { 190 if err = stagedChanges.AddRelationshipChange(ctx, revisions.NewForTransactionID(deletedTxn), nextTuple, core.RelationTupleUpdate_DELETE); err != nil { 191 return 192 } 193 } 194 } 195 if err = rows.Err(); err != nil { 196 return 197 } 198 199 changes = stagedChanges.AsRevisionChanges(revisions.TransactionIDKeyLessThanFunc) 200 return 201 }