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  }