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 }