github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/lookupsubjects_reducers.go (about)

     1  package graph
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/authzed/spicedb/internal/datasets"
     8  	"github.com/authzed/spicedb/internal/dispatch"
     9  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    10  	"github.com/authzed/spicedb/pkg/tuple"
    11  )
    12  
    13  // lookupSubjectsUnion defines a reducer for union operations, where all the results from each stream
    14  // for each branch are unioned together, filtered, limited and then published.
    15  type lookupSubjectsUnion struct {
    16  	parentStream dispatch.LookupSubjectsStream
    17  	collectors   map[int]*dispatch.CollectingDispatchStream[*v1.DispatchLookupSubjectsResponse]
    18  	ci           cursorInformation
    19  }
    20  
    21  func newLookupSubjectsUnion(parentStream dispatch.LookupSubjectsStream, ci cursorInformation) *lookupSubjectsUnion {
    22  	return &lookupSubjectsUnion{
    23  		parentStream: parentStream,
    24  		collectors:   map[int]*dispatch.CollectingDispatchStream[*v1.DispatchLookupSubjectsResponse]{},
    25  		ci:           ci,
    26  	}
    27  }
    28  
    29  func (lsu *lookupSubjectsUnion) ForIndex(ctx context.Context, setOperationIndex int) dispatch.LookupSubjectsStream {
    30  	collector := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx)
    31  	lsu.collectors[setOperationIndex] = collector
    32  	return collector
    33  }
    34  
    35  func (lsu *lookupSubjectsUnion) CompletedChildOperations() error {
    36  	foundSubjects := datasets.NewSubjectSetByResourceID()
    37  	metadata := emptyMetadata
    38  
    39  	for index := 0; index < len(lsu.collectors); index++ {
    40  		collector, ok := lsu.collectors[index]
    41  		if !ok {
    42  			return fmt.Errorf("missing collector for index %d", index)
    43  		}
    44  
    45  		for _, result := range collector.Results() {
    46  			metadata = combineResponseMetadata(metadata, result.Metadata)
    47  			if err := foundSubjects.UnionWith(result.FoundSubjectsByResourceId); err != nil {
    48  				return fmt.Errorf("failed to UnionWith under lookupSubjectsUnion: %w", err)
    49  			}
    50  		}
    51  	}
    52  
    53  	if foundSubjects.IsEmpty() {
    54  		return nil
    55  	}
    56  
    57  	// Since we've collected results from multiple branches, some which may be past the end of the overall limit,
    58  	// do a cursor-based filtering here to ensure we only return the limit.
    59  	resp, done, err := createFilteredAndLimitedResponse(lsu.ci, foundSubjects.AsMap(), metadata)
    60  	defer done()
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	if resp == nil {
    66  		return nil
    67  	}
    68  
    69  	return lsu.parentStream.Publish(resp)
    70  }
    71  
    72  // branchRunInformation is information passed to a RunUntilSpanned handler.
    73  type branchRunInformation struct {
    74  	ci cursorInformation
    75  }
    76  
    77  // dependentBranchReducerReloopLimit is the limit of results for each iteration of the dependent branch LookupSubject redispatches.
    78  const dependentBranchReducerReloopLimit = 1000
    79  
    80  // dependentBranchReducer is the implementation reducer for any rewrite operations whose branches depend upon one another
    81  // (intersection and exclusion).
    82  type dependentBranchReducer struct {
    83  	// parentStream is the stream to which results will be published, after reduction.
    84  	parentStream dispatch.LookupSubjectsStream
    85  
    86  	// collectors are a map from branch index to the associated collector of stream results.
    87  	collectors map[int]*dispatch.CollectingDispatchStream[*v1.DispatchLookupSubjectsResponse]
    88  
    89  	// parentCi is the cursor information from the parent call.
    90  	parentCi cursorInformation
    91  
    92  	// combinationHandler is the function invoked to "combine" the results from different branches, such as performing
    93  	// intersection or exclusion.
    94  	combinationHandler func(fs datasets.SubjectSetByResourceID, other datasets.SubjectSetByResourceID) error
    95  
    96  	// firstBranchCi is the *current* cursor for the first branch; this value is updated during iteration as the reducer is
    97  	// re-run.
    98  	firstBranchCi cursorInformation
    99  }
   100  
   101  // ForIndex returns the stream to which results should be published for the branch with the given index. Must not be called
   102  // in parallel.
   103  func (dbr *dependentBranchReducer) ForIndex(ctx context.Context, setOperationIndex int) dispatch.LookupSubjectsStream {
   104  	collector := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx)
   105  	dbr.collectors[setOperationIndex] = collector
   106  	return collector
   107  }
   108  
   109  // RunUntilSpanned runs the branch (with the given index) until all necessary results have been collected. For the first branch,
   110  // this is just a direct invocation. For all other branches, the handler will be reinvoked until all results have been collected
   111  // *or* the last subject ID found is >= the last subject ID found by the first branch, ensuring that all other branches have
   112  // "spanned" the subjects of the first branch. This is necessary because an intersection or exclusion must operate over the same
   113  // set of subject IDs.
   114  func (dbr *dependentBranchReducer) RunUntilSpanned(ctx context.Context, index int, handler func(ctx context.Context, current branchRunInformation) error) error {
   115  	// If invoking the run for the first branch, use the current first branch cursor.
   116  	if index == 0 {
   117  		return handler(ctx, branchRunInformation{ci: dbr.firstBranchCi.withClonedLimits()})
   118  	}
   119  
   120  	// Otherwise, run the branch until it has either exhausted all results OR the last result returned matches the last result previously
   121  	// returned by the first branch. This is to ensure that the other branches encompass the entire "span" of results from the first branch,
   122  	// which is necessary for intersection or exclusion (e.g. dependent branches).
   123  	firstBranchTerminalSubjectID, err := finalSubjectIDForResults(dbr.firstBranchCi, dbr.collectors[0].Results())
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	// If there are no concrete subject IDs found, then simply invoke the handler with the first branch's cursor/limit to
   129  	// return the wildcard; all other results will be superflouous.
   130  	if firstBranchTerminalSubjectID == "" {
   131  		return handler(ctx, branchRunInformation{ci: dbr.firstBranchCi})
   132  	}
   133  
   134  	// Otherwise, run the handler until its returned results is empty OR its cursor is >= the terminal subject ID.
   135  	startingCursor := dbr.firstBranchCi.currentCursor
   136  	previousResultCount := 0
   137  	for {
   138  		limits := newLimitTracker(dependentBranchReducerReloopLimit)
   139  		ci, err := newCursorInformation(startingCursor, limits, lsDispatchVersion)
   140  		if err != nil {
   141  			return err
   142  		}
   143  
   144  		// Invoke the handler with a modified limits and a cursor starting at the previous call.
   145  		if err := handler(ctx, branchRunInformation{
   146  			ci: ci,
   147  		}); err != nil {
   148  			return err
   149  		}
   150  
   151  		// Check for any new results found. If none, then we're done.
   152  		updatedResults := dbr.collectors[index].Results()
   153  		if len(updatedResults) == previousResultCount {
   154  			return nil
   155  		}
   156  
   157  		// Otherwise, grab the terminal subject ID to create the next cursor.
   158  		previousResultCount = len(updatedResults)
   159  		terminalSubjectID, err := finalSubjectIDForResults(dbr.parentCi, updatedResults)
   160  		if err != nil {
   161  			return nil
   162  		}
   163  
   164  		// If the cursor is now the wildcard, then we know that all concrete results have been consumed.
   165  		if terminalSubjectID == tuple.PublicWildcard {
   166  			return nil
   167  		}
   168  
   169  		// If the terminal subject in the results collector is now at or beyond that of the first branch, then
   170  		// we've spanned the entire results set necessary to perform the intersection or exclusion.
   171  		if firstBranchTerminalSubjectID != tuple.PublicWildcard && terminalSubjectID >= firstBranchTerminalSubjectID {
   172  			return nil
   173  		}
   174  
   175  		startingCursor = updatedResults[len(updatedResults)-1].AfterResponseCursor
   176  	}
   177  }
   178  
   179  // CompletedDependentChildOperations is invoked once all branches have been run to perform combination and publish any
   180  // valid subject IDs. This also moves the first branch's cursor forward.
   181  //
   182  // Returns the number of results from the first branch, and/or any error. The number of results is used to determine whether
   183  // the first branch has been exhausted.
   184  func (dbr *dependentBranchReducer) CompletedDependentChildOperations() (int, error) {
   185  	firstBranchCount := -1
   186  
   187  	// Update the first branch cursor for moving forward. This ensures that each iteration of the first branch for
   188  	// RunUntilSpanned is moving forward.
   189  	firstBranchTerminalSubjectID, err := finalSubjectIDForResults(dbr.parentCi, dbr.collectors[0].Results())
   190  	if err != nil {
   191  		return firstBranchCount, err
   192  	}
   193  
   194  	existingFirstBranchCI := dbr.firstBranchCi
   195  	if firstBranchTerminalSubjectID != "" {
   196  		updatedCI, err := dbr.firstBranchCi.withOutgoingSection(firstBranchTerminalSubjectID)
   197  		if err != nil {
   198  			return -1, err
   199  		}
   200  
   201  		updatedCursor := updatedCI.responsePartialCursor()
   202  		fbci, err := newCursorInformation(updatedCursor, dbr.firstBranchCi.limits, lsDispatchVersion)
   203  		if err != nil {
   204  			return firstBranchCount, err
   205  		}
   206  
   207  		dbr.firstBranchCi = fbci
   208  	}
   209  
   210  	// Run the combiner over the results.
   211  	var foundSubjects datasets.SubjectSetByResourceID
   212  	metadata := emptyMetadata
   213  
   214  	for index := 0; index < len(dbr.collectors); index++ {
   215  		collector, ok := dbr.collectors[index]
   216  		if !ok {
   217  			return firstBranchCount, fmt.Errorf("missing collector for index %d", index)
   218  		}
   219  
   220  		results := datasets.NewSubjectSetByResourceID()
   221  		for _, result := range collector.Results() {
   222  			metadata = combineResponseMetadata(metadata, result.Metadata)
   223  			if err := results.UnionWith(result.FoundSubjectsByResourceId); err != nil {
   224  				return firstBranchCount, fmt.Errorf("failed to UnionWith: %w", err)
   225  			}
   226  		}
   227  
   228  		if index == 0 {
   229  			foundSubjects = results
   230  			firstBranchCount = results.ConcreteSubjectCount()
   231  		} else {
   232  			err := dbr.combinationHandler(foundSubjects, results)
   233  			if err != nil {
   234  				return firstBranchCount, err
   235  			}
   236  
   237  			if foundSubjects.IsEmpty() {
   238  				return firstBranchCount, nil
   239  			}
   240  		}
   241  	}
   242  
   243  	// Apply the limits to the found results.
   244  	resp, done, err := createFilteredAndLimitedResponse(existingFirstBranchCI, foundSubjects.AsMap(), metadata)
   245  	defer done()
   246  	if err != nil {
   247  		return firstBranchCount, err
   248  	}
   249  
   250  	if resp == nil {
   251  		return firstBranchCount, nil
   252  	}
   253  
   254  	return firstBranchCount, dbr.parentStream.Publish(resp)
   255  }
   256  
   257  func newLookupSubjectsIntersection(parentStream dispatch.LookupSubjectsStream, ci cursorInformation) *dependentBranchReducer {
   258  	return &dependentBranchReducer{
   259  		parentStream: parentStream,
   260  		collectors:   map[int]*dispatch.CollectingDispatchStream[*v1.DispatchLookupSubjectsResponse]{},
   261  		parentCi:     ci,
   262  		combinationHandler: func(fs datasets.SubjectSetByResourceID, other datasets.SubjectSetByResourceID) error {
   263  			return fs.IntersectionDifference(other)
   264  		},
   265  		firstBranchCi: ci,
   266  	}
   267  }
   268  
   269  func newLookupSubjectsExclusion(parentStream dispatch.LookupSubjectsStream, ci cursorInformation) *dependentBranchReducer {
   270  	return &dependentBranchReducer{
   271  		parentStream: parentStream,
   272  		collectors:   map[int]*dispatch.CollectingDispatchStream[*v1.DispatchLookupSubjectsResponse]{},
   273  		parentCi:     ci,
   274  		combinationHandler: func(fs datasets.SubjectSetByResourceID, other datasets.SubjectSetByResourceID) error {
   275  			fs.SubtractAll(other)
   276  			return nil
   277  		},
   278  		firstBranchCi: ci,
   279  	}
   280  }