go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/service/datastore/dropped_arg_tracker.go (about)

     1  // Copyright 2020 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
    16  
    17  import (
    18  	"sort"
    19  
    20  	"go.chromium.org/luci/common/errors"
    21  )
    22  
    23  // DroppedArgTracker is used to track dropping items from Keys as well as meta
    24  // and/or PropertyMap arrays from one layer of the RawInterface to the next.
    25  //
    26  // If you're not writing a datastore backend implementation (like
    27  // "go.chromium.org/luci/gae/impl/*"), then you can ignore this type.
    28  //
    29  // For example, say your GetMulti method was passed 4 arguments, but one of them
    30  // was bad. DroppedArgTracker would allow you to "drop" the bad entry, and then
    31  // synthesize new keys/meta/values arrays excluding the bad entry. You could
    32  // then map from the new arrays back to the indexes of the original arrays.
    33  //
    34  // This DroppedArgTracker will do no allocations if you don't end up dropping
    35  // any arguments (so in the 'good' case, there are zero allocations).
    36  //
    37  // Example:
    38  //
    39  //	  Say we're given a list of arguments which look like ("_" means a bad value
    40  //	  that we drop):
    41  //
    42  //	   input: A B _ C D _ _ E
    43  //	    Idxs: 0 1 2 3 4 5 6 7
    44  //	 dropped:     2     5 6
    45  //
    46  //	DropKeys(input): A B C D E
    47  //	                 0 1 2 3 4
    48  //
    49  //	OriginalIndex(0) -> 0
    50  //	OriginalIndex(1) -> 1
    51  //	OriginalIndex(2) -> 3
    52  //	OriginalIndex(3) -> 4
    53  //	OriginalIndex(4) -> 7
    54  //
    55  // Methods on this type are NOT goroutine safe.
    56  type DroppedArgTracker []int
    57  
    58  // MarkForRemoval tracks `originalIndex` for removal when `Drop*` methods
    59  // are called.
    60  //
    61  // N is a size hint for the maximum number of entries that `dat` could have. If
    62  // `dat` has a capacity of < N, it will be allocated to N.
    63  //
    64  // If called with N == len(args) and originalIndex is always increasing, then
    65  // this will only do one allocation for the life of this DroppedArgTracker, and
    66  // each MarkForRemoval will only cost a single slice append. If called out of
    67  // order, or with a bad value of N, this will do more allocations and will do
    68  // a binary search on each call.
    69  func (dat *DroppedArgTracker) MarkForRemoval(originalIndex, N int) {
    70  	datLen := len(*dat)
    71  
    72  	if cap(*dat) < N {
    73  		newDat := make([]int, datLen, N)
    74  		copy(newDat, *dat)
    75  		*dat = newDat
    76  	}
    77  
    78  	// most uses will insert linearly, so do a quick check of the max element to
    79  	// see if originalIndex is larger and then do a simple append.
    80  	if datLen == 0 || originalIndex > (*dat)[datLen-1] {
    81  		*dat = append(*dat, originalIndex)
    82  		return
    83  	}
    84  
    85  	// Otherwise, search for the correct location and insert it
    86  	insIdx := sort.SearchInts(*dat, originalIndex)
    87  	if insIdx < datLen && (*dat)[insIdx] == originalIndex {
    88  		return
    89  	}
    90  	*dat = append(*dat, 0)
    91  	copy((*dat)[insIdx+1:], (*dat)[insIdx:])
    92  	(*dat)[insIdx] = originalIndex
    93  }
    94  
    95  // MarkNilKeys is a helper method which calls MarkForRemoval for each nil key.
    96  func (dat *DroppedArgTracker) MarkNilKeys(keys []*Key) {
    97  	for idx, k := range keys {
    98  		if k == nil {
    99  			dat.MarkForRemoval(idx, len(keys))
   100  		}
   101  	}
   102  }
   103  
   104  // MarkNilKeysMeta is a helper method which calls MarkForRemoval for each nil
   105  // key or meta.
   106  func (dat *DroppedArgTracker) MarkNilKeysMeta(keys []*Key, meta MultiMetaGetter) {
   107  	for idx, k := range keys {
   108  		if k == nil || meta[idx] == nil {
   109  			dat.MarkForRemoval(idx, len(keys))
   110  		}
   111  	}
   112  }
   113  
   114  // MarkNilKeysVals is a helper method which calls MarkForRemoval for each nil
   115  // key or value.
   116  func (dat *DroppedArgTracker) MarkNilKeysVals(keys []*Key, vals []PropertyMap) {
   117  	for idx, k := range keys {
   118  		if k == nil || vals[idx] == nil {
   119  			dat.MarkForRemoval(idx, len(keys))
   120  		}
   121  	}
   122  }
   123  
   124  // If `dat` has a positive length, this will invoke `init` once, followed by
   125  // `include` for every non-overlapping (i, j) range less than N which doesn't
   126  // include any elements indicated with MarkForRemoval.
   127  //
   128  // If `dat` contains a removed index larger than N, this panics.
   129  func (dat DroppedArgTracker) mustCompress(N int, init func(), include func(i, j int)) DroppedArgLookup {
   130  	if len(dat) == 0 || N == 0 {
   131  		return nil
   132  	}
   133  
   134  	if largestDropIdx := dat[len(dat)-1]; largestDropIdx >= N {
   135  		panic(errors.Reason(
   136  			"DroppedArgTracker has out of bound index: %d >= %d ",
   137  			largestDropIdx, N,
   138  		).Err())
   139  	}
   140  
   141  	// dal may have len < len(dat) in the event that multiple dat entries are
   142  	// contiguous (they'll be compressed into a single entry in dal).
   143  	dal := make(DroppedArgLookup, 0, len(dat))
   144  
   145  	init()
   146  	nextPotentialOriginalInclusion := 0
   147  
   148  	for numDroppedSoFar, originalIndexToDrop := range dat {
   149  		if originalIndexToDrop > nextPotentialOriginalInclusion {
   150  			include(nextPotentialOriginalInclusion, originalIndexToDrop)
   151  		}
   152  
   153  		nextPotentialOriginalInclusion = originalIndexToDrop + 1
   154  		reducedIndex := originalIndexToDrop - numDroppedSoFar
   155  
   156  		if len(dal) != 0 && dal[len(dal)-1].reducedIndex == reducedIndex {
   157  			// If the user drops multiple original indices in a row, we need to
   158  			// reflect them in a single entry in dal.
   159  			dal[len(dal)-1].originalIndex++
   160  		} else {
   161  			// otherwise, we make a new entry.
   162  			dal = append(dal, idxPair{
   163  				reducedIndex,
   164  				nextPotentialOriginalInclusion,
   165  			})
   166  		}
   167  	}
   168  	include(nextPotentialOriginalInclusion, N)
   169  
   170  	return dal
   171  }
   172  
   173  // DropKeys returns a compressed version of `keys`, dropping all elements which
   174  // were marked with MarkForRemoval.
   175  func (dat DroppedArgTracker) DropKeys(keys []*Key) ([]*Key, DroppedArgLookup) {
   176  	newKeys := keys
   177  
   178  	init := func() {
   179  		newKeys = make([]*Key, 0, len(keys)-len(dat))
   180  	}
   181  	include := func(i, j int) {
   182  		newKeys = append(newKeys, keys[i:j]...)
   183  	}
   184  
   185  	dal := dat.mustCompress(len(keys), init, include)
   186  	return newKeys, dal
   187  }
   188  
   189  // DropKeysAndMeta returns a compressed version of `keys` and `meta`, dropping
   190  // all elements which were marked with MarkForRemoval.
   191  //
   192  // `keys` and `meta` must have the same lengths.
   193  func (dat DroppedArgTracker) DropKeysAndMeta(keys []*Key, meta MultiMetaGetter) ([]*Key, MultiMetaGetter, DroppedArgLookup) {
   194  	newKeys := keys
   195  	newMeta := meta
   196  
   197  	// MultiMetaGetter is special and frequently is len 0 with non-nil keys, so we
   198  	// just keep it empty.
   199  
   200  	init := func() {
   201  		newKeys = make([]*Key, 0, len(keys)-len(dat))
   202  		if len(meta) > 0 {
   203  			newMeta = make(MultiMetaGetter, 0, len(keys)-len(dat))
   204  		}
   205  	}
   206  	include := func(i, j int) {
   207  		newKeys = append(newKeys, keys[i:j]...)
   208  		if len(meta) > 0 {
   209  			newMeta = append(newMeta, meta[i:j]...)
   210  		}
   211  	}
   212  
   213  	dal := dat.mustCompress(len(keys), init, include)
   214  	return newKeys, newMeta, dal
   215  }
   216  
   217  // DropKeysAndVals returns a compressed version of `keys` and `vals`, dropping
   218  // all elements which were marked with MarkForRemoval.
   219  //
   220  // `keys` and `vals` must have the same lengths.
   221  func (dat DroppedArgTracker) DropKeysAndVals(keys []*Key, vals []PropertyMap) ([]*Key, []PropertyMap, DroppedArgLookup) {
   222  	newKeys := keys
   223  	newVals := vals
   224  
   225  	if len(keys) != len(vals) {
   226  		panic(errors.Reason(
   227  			"DroppedArgTracker.DropKeysAndVals: mismatched lengths: %d vs %d",
   228  			len(keys), len(vals),
   229  		).Err())
   230  	}
   231  
   232  	init := func() {
   233  		newKeys = make([]*Key, 0, len(keys)-len(dat))
   234  		newVals = make([]PropertyMap, 0, len(keys)-len(dat))
   235  	}
   236  	include := func(i, j int) {
   237  		newKeys = append(newKeys, keys[i:j]...)
   238  		newVals = append(newVals, vals[i:j]...)
   239  	}
   240  
   241  	dal := dat.mustCompress(len(keys), init, include)
   242  	return newKeys, newVals, dal
   243  }
   244  
   245  type idxPair struct {
   246  	reducedIndex  int
   247  	originalIndex int
   248  }
   249  
   250  // DroppedArgLookup is returned from using a DroppedArgTracker.
   251  //
   252  // It can be used to recover the index from the original slice by providing the
   253  // reduced slice index.
   254  type DroppedArgLookup []idxPair
   255  
   256  // OriginalIndex maps from an index into the array(s) returned from MustDrop
   257  // back to the corresponding index in the original arrays.
   258  func (dal DroppedArgLookup) OriginalIndex(reducedIndex int) int {
   259  	if len(dal) == 0 {
   260  		return reducedIndex
   261  	}
   262  
   263  	// Search for the idxPair whose reducedIndex is LARGER than what we want.
   264  	dalInsertIdx := sort.Search(len(dal), func(dalIdx int) bool {
   265  		return dal[dalIdx].reducedIndex > reducedIndex
   266  	})
   267  	// If search told us that it was "0" it means that no entry in `dal` includes
   268  	// our reducedIndex.
   269  	if dalInsertIdx == 0 {
   270  		return reducedIndex
   271  	}
   272  	// Now look up the idxPair before what search returned. This will have
   273  	// a reducedIndex which is <= reducedIndex.
   274  	entry := dal[dalInsertIdx-1]
   275  	if entry.reducedIndex > reducedIndex {
   276  		return reducedIndex
   277  	}
   278  	// Finally, return the originalIndex and add the difference between our user's
   279  	// reducedIndex and the one in this entry.
   280  	return entry.originalIndex + (reducedIndex - entry.reducedIndex)
   281  }