go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/service/datastore/multicursor.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package datastore contains APIs to handle datastore queries 16 package datastore 17 18 import ( 19 "context" 20 "encoding/base64" 21 "fmt" 22 "sort" 23 24 "google.golang.org/protobuf/proto" 25 26 "go.chromium.org/luci/common/errors" 27 28 mc "go.chromium.org/luci/gae/service/datastore/internal/protos/multicursor" 29 ) 30 31 // multiCursorVersion stores the proto version for mc.Cursors 32 const multiCursorVersion = 0 33 34 const multiCursorMagic = 0xA455 35 36 // multiCursor is a custom cursor that implements String. This is returned by 37 // cursor callback from RunMulti as a cursor. 38 type multiCursor struct { 39 curs *mc.Cursors 40 } 41 42 // String returns the marshalled Cursors proto encoded in base64 43 func (c multiCursor) String() string { 44 bytes, _ := proto.Marshal(c.curs) 45 return base64.StdEncoding.EncodeToString(bytes) 46 } 47 48 // IsMultiCursor returns true if the cursor probably represents a multicursor 49 // that is returned by RunMulti. Returns false otherwise 50 // 51 // Note: There is finite chance that some other cursor can be decoded as a valid 52 // multicursor 53 func IsMultiCursor(cursor Cursor) bool { 54 return IsMultiCursorString(cursor.String()) 55 } 56 57 // IsMultiCursorString returns true if the cursor string is probably a valid 58 // representation of a multicursor that is returned by RunMulti. Returns false 59 // otherwise 60 // 61 // Note: There is finite chance that some other cursor can be decoded as a valid 62 // multicursor 63 func IsMultiCursorString(cursor string) bool { 64 cursBuf, err := base64.StdEncoding.DecodeString(cursor) 65 if err != nil { 66 // Cannot be a multicursor 67 return false 68 } 69 var curs mc.Cursors 70 err = proto.Unmarshal(cursBuf, &curs) 71 return err == nil && curs.GetMagicNumber() == multiCursorMagic 72 } 73 74 // ApplyCursors applies the cursors to the queries and returns the new list of queries. 75 // The cursor should be from RunMulti, this will not work on any other cursor. The queries 76 // should match the original list of queries that was used to generate the cursor. If 77 // the queries don't match the behavior is undefined. The order for the queries is not 78 // important as they will be sorted before use. 79 func ApplyCursors(ctx context.Context, queries []*Query, cursor Cursor) ([]*Query, error) { 80 curStr := cursor.String() 81 return ApplyCursorString(ctx, queries, curStr) 82 } 83 84 // ApplyCursorString applies the cursors represented by the string and returns the new 85 // list of queries. The cursor string should be generated from cursor returned by 86 // RunMulti, this will not work on any other cursor. The queries must match the original 87 // list of queries that was used to generate the cursor. If the queries don't match 88 // the behavior is undefined. The order of queries is not important as they will be 89 // sorted before use. 90 func ApplyCursorString(ctx context.Context, queries []*Query, cursorToken string) ([]*Query, error) { 91 cursBuf, err := base64.StdEncoding.DecodeString(cursorToken) 92 if err != nil { 93 return nil, errors.Annotate(err, "Failed to decode cursor").Err() 94 } 95 var curs mc.Cursors 96 err = proto.Unmarshal(cursBuf, &curs) 97 if err != nil { 98 return nil, err 99 } 100 if curs.GetMagicNumber() != multiCursorMagic { 101 return nil, errors.New("Cursor doesn't contain valid magic") 102 } 103 if len(queries) != len(curs.Cursors) { 104 return nil, errors.New("Length mismatch. Cannot apply this cursor to the queries") 105 } 106 if curs.Version != multiCursorVersion { 107 return nil, fmt.Errorf("Cursor version mismatch. Need %v, got %v", multiCursorVersion, curs.Version) 108 } 109 // sortedOrder will contain the sorted order for queries. This allows 110 // for updating the queries in order. 111 sortedOrder := make([]int, len(queries)) 112 for idx := range sortedOrder { 113 sortedOrder[idx] = idx 114 } 115 // Sort queries and store the order in sortedOrder 116 sort.Slice(sortedOrder, func(i, j int) bool { 117 return queries[sortedOrder[i]].Less(queries[sortedOrder[j]]) 118 }) 119 // Assign the cursors in sorted order 120 for idx, qIdx := range sortedOrder { 121 if curs.Cursors[idx] != "" { 122 cursor, err := DecodeCursor(ctx, curs.Cursors[idx]) 123 if err != nil { 124 return nil, errors.Annotate(err, "Cannot decode cursor for a query").Err() 125 } 126 queries[qIdx] = queries[qIdx].Start(cursor) 127 } 128 } 129 // Return the queries in the order recieved 130 return queries, nil 131 }