github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/test/test_series_iterator.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package test 22 23 import ( 24 "fmt" 25 "sort" 26 "time" 27 28 "github.com/m3db/m3/src/dbnode/encoding" 29 "github.com/m3db/m3/src/dbnode/encoding/m3tsz" 30 "github.com/m3db/m3/src/dbnode/namespace" 31 "github.com/m3db/m3/src/dbnode/ts" 32 "github.com/m3db/m3/src/dbnode/x/xio" 33 "github.com/m3db/m3/src/query/models" 34 "github.com/m3db/m3/src/x/checked" 35 "github.com/m3db/m3/src/x/ident" 36 xtime "github.com/m3db/m3/src/x/time" 37 ) 38 39 var ( 40 // SeriesNamespace is the expected namespace for the generated series 41 SeriesNamespace string 42 // TestTags is the expected tags for the generated series 43 TestTags map[string]string 44 // BlockSize is the expected block size for the generated series 45 BlockSize time.Duration 46 // Start is the expected start time for the first block in the generated series 47 Start xtime.UnixNano 48 // SeriesStart is the expected start time for the generated series 49 SeriesStart xtime.UnixNano 50 // Middle is the expected end for the first block, and start of the second block 51 Middle xtime.UnixNano 52 // End is the expected end time for the generated series 53 End xtime.UnixNano 54 55 testIterAlloc func(r xio.Reader64, d namespace.SchemaDescr) encoding.ReaderIterator 56 ) 57 58 func init() { 59 SeriesNamespace = "namespace" 60 61 TestTags = map[string]string{"foo": "bar", "baz": "qux"} 62 63 BlockSize = time.Hour / 2 64 65 Start = xtime.Now().Truncate(time.Hour) 66 SeriesStart = Start.Add(2 * time.Minute) 67 Middle = Start.Add(BlockSize) 68 End = Middle.Add(BlockSize) 69 70 testIterAlloc = m3tsz.DefaultReaderIteratorAllocFn(encoding.NewOptions()) 71 } 72 73 // Builds a MultiReaderIterator representing a single replica 74 // with two segments, one merged with values from 1->30, and 75 // one which is unmerged with 2 segments from 101->130 76 // with one of the unmerged containing even points, other containing odd 77 func buildReplica() (encoding.MultiReaderIterator, error) { 78 // Build a merged BlockReader 79 encoder := m3tsz.NewEncoder(Start, checked.NewBytes(nil, nil), true, encoding.NewOptions()) 80 i := 0 81 for at := time.Duration(0); at < BlockSize; at += time.Minute { 82 i++ 83 datapoint := ts.Datapoint{TimestampNanos: Start.Add(at), Value: float64(i)} 84 err := encoder.Encode(datapoint, xtime.Second, nil) 85 if err != nil { 86 return nil, err 87 } 88 } 89 segment := encoder.Discard() 90 mergedReader := xio.BlockReader{ 91 SegmentReader: xio.NewSegmentReader(segment), 92 Start: Start, 93 BlockSize: BlockSize, 94 } 95 96 // Build two unmerged BlockReaders 97 i = 100 98 encoder = m3tsz.NewEncoder(Middle, checked.NewBytes(nil, nil), true, encoding.NewOptions()) 99 encoderTwo := m3tsz.NewEncoder(Middle, checked.NewBytes(nil, nil), true, encoding.NewOptions()) 100 useFirstEncoder := true 101 102 for at := time.Duration(0); at < BlockSize; at += time.Minute { 103 i++ 104 datapoint := ts.Datapoint{TimestampNanos: Middle.Add(at), Value: float64(i)} 105 var err error 106 if useFirstEncoder { 107 err = encoder.Encode(datapoint, xtime.Second, nil) 108 } else { 109 err = encoderTwo.Encode(datapoint, xtime.Second, nil) 110 } 111 if err != nil { 112 return nil, err 113 } 114 useFirstEncoder = !useFirstEncoder 115 } 116 117 segment = encoder.Discard() 118 segmentTwo := encoderTwo.Discard() 119 unmergedReaders := []xio.BlockReader{ 120 { 121 SegmentReader: xio.NewSegmentReader(segment), 122 Start: Middle, 123 BlockSize: BlockSize, 124 }, 125 { 126 SegmentReader: xio.NewSegmentReader(segmentTwo), 127 Start: Middle, 128 BlockSize: BlockSize, 129 }, 130 } 131 132 multiReader := encoding.NewMultiReaderIterator(testIterAlloc, nil) 133 sliceOfSlicesIter := xio.NewReaderSliceOfSlicesFromBlockReadersIterator([][]xio.BlockReader{ 134 {mergedReader}, 135 unmergedReaders, 136 }) 137 multiReader.ResetSliceOfSlices(sliceOfSlicesIter, nil) 138 return multiReader, nil 139 } 140 141 type sortableTags []ident.Tag 142 143 func (a sortableTags) Len() int { return len(a) } 144 func (a sortableTags) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 145 func (a sortableTags) Less(i, j int) bool { return a[i].Name.String() < a[j].Name.String() } 146 147 // BuildTestSeriesIterator creates a sample SeriesIterator 148 // This series iterator has two identical replicas. 149 // Each replica has two blocks. 150 // The first block in each replica is merged and has values 1->30 151 // The values 1 and 2 appear before the SeriesIterator start time, and are not expected 152 // to appear when reading through the iterator 153 // The second block is unmerged; when it was merged, it has values 101 -> 130 154 // from two readers, one with even values and other with odd values 155 // Expected data points for reading through the iterator: [3..30,101..130], 58 in total 156 // SeriesIterator ID is given, namespace is 'namespace' 157 // Tags are "foo": "bar" and "baz": "qux" 158 func BuildTestSeriesIterator(id string) (encoding.SeriesIterator, error) { 159 replicaOne, err := buildReplica() 160 if err != nil { 161 return nil, err 162 } 163 replicaTwo, err := buildReplica() 164 if err != nil { 165 return nil, err 166 } 167 168 sortTags := make(sortableTags, 0, len(TestTags)) 169 for name, value := range TestTags { 170 sortTags = append(sortTags, ident.StringTag(name, value)) 171 } 172 173 sort.Sort(sortTags) 174 tags := ident.Tags{} 175 for _, t := range sortTags { 176 tags.Append(t) 177 } 178 179 return encoding.NewSeriesIterator( 180 encoding.SeriesIteratorOptions{ 181 ID: ident.StringID(id), 182 Namespace: ident.StringID(SeriesNamespace), 183 Tags: ident.NewTagsIterator(tags), 184 StartInclusive: SeriesStart, 185 EndExclusive: End, 186 Replicas: []encoding.MultiReaderIterator{ 187 replicaOne, 188 replicaTwo, 189 }, 190 }, nil), nil 191 } 192 193 // Datapoint is a datapoint with a value and an offset for building a custom iterator 194 type Datapoint struct { 195 Value float64 196 Offset time.Duration 197 } 198 199 // BuildCustomIterator builds a custom iterator with bounds 200 func BuildCustomIterator( 201 dps [][]Datapoint, 202 testTags map[string]string, 203 seriesID, seriesNamespace string, 204 start xtime.UnixNano, 205 blockSize, stepSize time.Duration, 206 ) (encoding.SeriesIterator, models.Bounds, error) { 207 // Build a merged BlockReader 208 readers := make([][]xio.BlockReader, 0, len(dps)) 209 currentStart := start 210 for _, datapoints := range dps { 211 encoder := m3tsz.NewEncoder(currentStart, checked.NewBytes(nil, nil), true, encoding.NewOptions()) 212 // NB: empty datapoints should skip this block reader but still increase time 213 if len(datapoints) > 0 { 214 for _, dp := range datapoints { 215 offset := dp.Offset 216 if offset > blockSize { 217 return nil, models.Bounds{}, 218 fmt.Errorf("custom series iterator offset is larger than blockSize") 219 } 220 221 if offset < 0 { 222 return nil, models.Bounds{}, 223 fmt.Errorf("custom series iterator offset is negative") 224 } 225 226 tsDp := ts.Datapoint{ 227 Value: dp.Value, 228 TimestampNanos: currentStart.Add(offset), 229 } 230 231 err := encoder.Encode(tsDp, xtime.Second, nil) 232 if err != nil { 233 return nil, models.Bounds{}, err 234 } 235 } 236 237 segment := encoder.Discard() 238 readers = append(readers, []xio.BlockReader{{ 239 SegmentReader: xio.NewSegmentReader(segment), 240 Start: currentStart, 241 BlockSize: blockSize, 242 }}) 243 } 244 245 currentStart = currentStart.Add(blockSize) 246 } 247 248 multiReader := encoding.NewMultiReaderIterator(testIterAlloc, nil) 249 sliceOfSlicesIter := xio.NewReaderSliceOfSlicesFromBlockReadersIterator(readers) 250 multiReader.ResetSliceOfSlices(sliceOfSlicesIter, nil) 251 252 tags := ident.Tags{} 253 for name, value := range testTags { 254 tags.Append(ident.StringTag(name, value)) 255 } 256 257 return encoding.NewSeriesIterator( 258 encoding.SeriesIteratorOptions{ 259 ID: ident.StringID(seriesID), 260 Namespace: ident.StringID(seriesNamespace), 261 Tags: ident.NewTagsIterator(tags), 262 StartInclusive: start, 263 EndExclusive: currentStart.Add(blockSize), 264 Replicas: []encoding.MultiReaderIterator{ 265 multiReader, 266 }, 267 }, nil), 268 models.Bounds{ 269 Start: start, 270 Duration: blockSize * time.Duration(len(dps)), 271 StepSize: stepSize, 272 }, 273 nil 274 }