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 }