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  }