UNPKG

15.4 kBPlain TextView Raw
1import { PageRange } from "../generated/artifacts/models";
2import {
3 PersistencyPageRange,
4 ZERO_EXTENT_ID
5} from "../persistence/IBlobMetadataStore";
6import IPageBlobRangesManager from "./IPageBlobRangesManager";
7
8/********** Ranges Merging Strategy ************/
9//
10// Example:
11// Existing ranges: |---| |------| |---------| |-----|
12// New range : |----------------------|
13//
14// 4 existing ranges, and one new request range.
15// Above 4 existing ranges, 3 existing ranges are get impacted.
16//
17// We have 2 merging strategies:
18// #1 Merge all impacted ranges
19// : |---| |**------------------------****|
20// #2 Split first and last impacted existing ranges:
21// : |---| |**|----------------------|****|
22//
23// Question:
24// Every range has a pointer to a chunk in one persistency layer extent.
25// When implementing Get Page Ranges Diff request, we will assume 2 (sub)ranges
26// are same if they pointing to same range of an extent. (Or we can comparing the
27// persistency layer payload, but it's not efficiency.)
28//
29// For #1 strategy, Get Page Ranges Diff will return a larger range scope than
30// the actual changed. Above ranges marked as * will point to new allocated extent,
31// while in previous snapshot, they still point to old extent.
32// One potential workaround is to update these ranges in snapshot blob to pointing
33// to same new allocated extent. But this will make implementation of update ranges
34// more complex, and also makes snapshot mutable.
35// Another concern is that, the * ranges may have large footprint, which is
36// time consuming.
37//
38// For #2 strategy, there will be more and more small ranges. See the |**| and |****|.
39// But it's able . We can make the "merging" work to future GC.
40//
41// We choose #2 strategy, and leave the merging work to future GC, which will merging
42// small ranges in background.
43//
44// TODO for #2 strategy:
45// * Resize API: Should remove additional ranges
46// * Download Page Blob API after resize (shift): Should taking resize into consideration
47// * Clean Ranges API: Same strategy like update ranges
48// * Page Blob GC for Ranges Merger: Merge small ranges and extent chunks for page blob and snapshots
49// * GC for un-referred extents
50export default class PageBlobRangesManager implements IPageBlobRangesManager {
51 public mergeRange(
52 ranges: PersistencyPageRange[],
53 range: PersistencyPageRange
54 ): void {
55 const start = range.start; // Inclusive
56 const end = range.end; // Inclusive
57 const persistency = range.persistency;
58
59 const impactedScope = this.selectImpactedRanges(ranges, start, end);
60
61 // Find out first and last impacted range index
62 const impactedStartIndex = impactedScope[0];
63 const impactedEndIndex = impactedScope[1];
64 const impactedRangesCount =
65 impactedStartIndex > impactedEndIndex // No impacted ranges
66 ? 0
67 : impactedEndIndex - impactedStartIndex + 1;
68
69 // New created range for this request payload
70 const newRange: PersistencyPageRange = { start, end, persistency };
71
72 if (impactedRangesCount === 0) {
73 // If there is no existing impacted range, just insert the new range
74 ranges.splice(impactedStartIndex, 0, newRange);
75 } else {
76 // Otherwise, try to split the first and last impacted ranges
77 const firstImpactedRange = ranges[impactedStartIndex];
78 const lastImpactedRange = ranges[impactedEndIndex];
79
80 // Ranges to be inserted
81 const newRanges = [];
82
83 // If first range needs to be split, push the split range
84 if (firstImpactedRange.end >= start && firstImpactedRange.start < start) {
85 newRanges.push({
86 start: firstImpactedRange.start,
87 end: start - 1,
88 persistency: {
89 id: firstImpactedRange.persistency.id,
90 offset: firstImpactedRange.persistency.offset,
91 count: start - firstImpactedRange.start
92 }
93 });
94 }
95
96 newRanges.push(newRange);
97
98 // If last impacted range needs to be split, push the split range
99 if (end >= lastImpactedRange.start && end < lastImpactedRange.end) {
100 newRanges.push({
101 start: end + 1,
102 end: lastImpactedRange.end,
103 persistency: {
104 id: lastImpactedRange.persistency.id,
105 offset:
106 lastImpactedRange.persistency.offset +
107 (end + 1 - lastImpactedRange.start),
108 count: lastImpactedRange.end - end
109 }
110 });
111 }
112
113 ranges.splice(impactedStartIndex, impactedRangesCount, ...newRanges);
114 }
115 }
116
117 public clearRange(ranges: PersistencyPageRange[], range: PageRange): void {
118 const start = range.start; // Inclusive
119 const end = range.end; // Inclusive
120
121 // Find out existing impacted ranges list
122 const impactedScope = this.selectImpactedRanges(ranges, start, end);
123
124 // Find out first and last impacted range index
125 const impactedStartIndex = impactedScope[0];
126 const impactedEndIndex = impactedScope[1];
127 const impactedRangesCount =
128 impactedStartIndex > impactedEndIndex // No impacted ranges
129 ? 0
130 : impactedEndIndex - impactedStartIndex + 1;
131
132 if (impactedRangesCount > 0) {
133 // Try to split the first and last impacted ranges
134 const firstImpactedRange = ranges[impactedStartIndex];
135 const lastImpactedRange = ranges[impactedEndIndex];
136
137 // Ranges to be inserted
138 const newRanges = [];
139
140 // If first range needs to be split, push the split range
141 if (firstImpactedRange.end >= start && firstImpactedRange.start < start) {
142 newRanges.push({
143 start: firstImpactedRange.start,
144 end: start - 1,
145 persistency: {
146 id: firstImpactedRange.persistency.id,
147 offset: firstImpactedRange.persistency.offset,
148 count: start - firstImpactedRange.start
149 }
150 });
151 }
152
153 // If last impacted range needs to be split, push the split range
154 if (end >= lastImpactedRange.start && end < lastImpactedRange.end) {
155 newRanges.push({
156 start: end + 1,
157 end: lastImpactedRange.end,
158 persistency: {
159 id: lastImpactedRange.persistency.id,
160 offset:
161 lastImpactedRange.persistency.offset +
162 (end + 1 - lastImpactedRange.start),
163 count: lastImpactedRange.end - end
164 }
165 });
166 }
167
168 ranges.splice(impactedStartIndex, impactedRangesCount, ...newRanges);
169 }
170 }
171
172 /**
173 * This method will not modify values of parameter ranges.
174 *
175 * @param {PersistencyPageRange[]} ranges
176 * @param {PageRange} range
177 * @returns {PersistencyPageRange[]}
178 * @memberof PageBlobRangesManager
179 */
180 public cutRanges(
181 ranges: PersistencyPageRange[],
182 range: PageRange
183 ): PersistencyPageRange[] {
184 const start = range.start; // Inclusive
185 const end = range.end; // Inclusive
186
187 const impactedScope = this.selectImpactedRanges(ranges, start, end);
188
189 // Find out first and last impacted range index
190 const impactedStartIndex = impactedScope[0];
191 const impactedEndIndex = impactedScope[1];
192 const impactedRangesCount =
193 impactedStartIndex > impactedEndIndex
194 ? 0 // No impacted ranges
195 : impactedEndIndex - impactedStartIndex + 1;
196
197 let impactedRanges: PersistencyPageRange[] = [];
198
199 if (impactedRangesCount > 0) {
200 impactedRanges = ranges.slice(impactedStartIndex, impactedEndIndex + 1);
201
202 // If first range needs to be split
203 const firstImpactedRange = impactedRanges[0];
204 if (firstImpactedRange.end >= start && firstImpactedRange.start < start) {
205 impactedRanges[0] = {
206 start,
207 end: firstImpactedRange.end,
208 persistency: {
209 offset:
210 start -
211 firstImpactedRange.start +
212 firstImpactedRange.persistency.offset,
213 count:
214 firstImpactedRange.persistency.count -
215 (start - firstImpactedRange.start),
216 id: firstImpactedRange.persistency.id
217 }
218 };
219 }
220
221 const lastImpactedRange = impactedRanges[impactedRanges.length - 1];
222 // If last impacted range needs to be split
223 if (end >= lastImpactedRange.start && end < lastImpactedRange.end) {
224 impactedRanges[impactedRanges.length - 1] = {
225 start: lastImpactedRange.start,
226 end,
227 persistency: {
228 offset: lastImpactedRange.persistency.offset,
229 count:
230 lastImpactedRange.persistency.count -
231 (lastImpactedRange.end - end),
232 id: lastImpactedRange.persistency.id
233 }
234 };
235 }
236 }
237
238 return impactedRanges;
239 }
240
241 public fillZeroRanges(
242 ranges: PersistencyPageRange[],
243 range: PageRange
244 ): PersistencyPageRange[] {
245 ranges = this.cutRanges(ranges, range);
246 const filledRanges: PersistencyPageRange[] = [];
247
248 if (ranges.length === 0) {
249 return [
250 {
251 start: range.start,
252 end: range.end,
253 persistency: {
254 offset: 0,
255 count: range.end + 1 - range.start,
256 id: ZERO_EXTENT_ID
257 }
258 }
259 ];
260 }
261
262 const firstRange = ranges[0];
263 const lastRange = ranges[ranges.length - 1];
264
265 if (range.start < firstRange.start) {
266 filledRanges.push({
267 start: range.start,
268 end: firstRange.start - 1,
269 persistency: {
270 offset: 0,
271 count: firstRange.start - range.start,
272 id: ZERO_EXTENT_ID
273 }
274 });
275 }
276
277 // TODO: fill in zero ranges in the middle
278 for (let i = 0; i < ranges.length - 1; i++) {
279 const nextRange = ranges[i + 1];
280 const currentRange = ranges[i];
281
282 filledRanges.push(currentRange);
283
284 const gap = nextRange.start - 1 - currentRange.end;
285 if (gap > 0) {
286 filledRanges.push({
287 start: currentRange.end + 1,
288 end: nextRange.start - 1,
289 persistency: {
290 offset: 0,
291 count: gap,
292 id: ZERO_EXTENT_ID
293 }
294 });
295 }
296 }
297
298 filledRanges.push(ranges[ranges.length - 1]);
299
300 if (lastRange.end < range.end) {
301 filledRanges.push({
302 start: lastRange.end + 1,
303 end: range.end,
304 persistency: {
305 offset: 0,
306 count: range.end - lastRange.end,
307 id: ZERO_EXTENT_ID
308 }
309 });
310 }
311
312 return filledRanges;
313 }
314
315 /**
316 * Select overlapped ranges based on binary search.
317 *
318 * @public
319 * @param {PersistencyPageRange[]} ranges Existing ranges
320 * @param {number} start Start offset of the new range, inclusive
321 * @param {number} end End offset of the new range, inclusive
322 * @returns {[number, number]} Impacted start and end ranges indexes tuple
323 * When no impacted ranges found, will return indexes
324 * for 2 closet existing ranges
325 * [FirstLargerRangeIndex, FirstSmallerRangeIndex]
326 * @memberof PageBlobRangesManager
327 */
328 public selectImpactedRanges(
329 ranges: PersistencyPageRange[],
330 start: number,
331 end: number
332 ): [number, number] {
333 if (ranges.length === 0) {
334 return [Infinity, -1];
335 }
336
337 if (start > end || start < 0) {
338 throw new RangeError(
339 // tslint:disable-next-line:max-line-length
340 "PageBlobRangesManager:selectImpactedRanges() start must less equal than end parameter, start must larger equal than 0."
341 );
342 }
343
344 const impactedRangeStartIndex = this.locateFirstImpactedRange(
345 ranges,
346 0,
347 ranges.length,
348 start
349 );
350
351 const impactedRangesEndIndex = this.locateLastImpactedRange(
352 ranges,
353 0,
354 ranges.length,
355 end
356 );
357
358 return [impactedRangeStartIndex, impactedRangesEndIndex];
359 }
360
361 /**
362 * Locate first impacted range for a given position.
363 *
364 * @public
365 * @param {PersistencyPageRange[]} ranges
366 * @param {number} searchStart Index of start range in ranges array, inclusive
367 * @param {number} searchEnd Index of end range in ranges array, exclusive
368 * @param {number} position First range index covers or larger than position will be returned
369 * @returns {number} Index of first impacted range or Infinity for no results
370 * @memberof PageBlobHandler
371 */
372 public locateFirstImpactedRange(
373 ranges: PersistencyPageRange[],
374 searchStart: number,
375 searchEnd: number,
376 position: number
377 ): number {
378 searchStart = searchStart < 0 ? 0 : searchStart;
379 searchEnd = searchEnd > ranges.length ? searchEnd : searchEnd;
380 if (ranges.length === 0 || searchStart >= searchEnd) {
381 return Infinity;
382 }
383
384 // Only last element to check
385 if (searchStart === searchEnd - 1) {
386 return this.positionInRange(ranges[searchStart], position) ||
387 position < ranges[searchStart].start
388 ? searchStart
389 : Infinity;
390 }
391
392 // 2 or more elements left
393 const searchMid = Math.floor((searchStart + searchEnd) / 2);
394 const indexInLeft = this.locateFirstImpactedRange(
395 ranges,
396 searchStart,
397 searchMid,
398 position
399 );
400 if (indexInLeft !== Infinity) {
401 return indexInLeft;
402 }
403 if (
404 this.positionInRange(ranges[searchMid], position) ||
405 position < ranges[searchMid].start
406 ) {
407 return searchMid;
408 } else {
409 return this.locateFirstImpactedRange(
410 ranges,
411 searchMid + 1, // Remove searchMid range from searching scope
412 searchEnd,
413 position
414 );
415 }
416 }
417
418 /**
419 * Locate last impacted range for a given position.
420 *
421 * @public
422 * @param {PersistencyPageRange[]} ranges
423 * @param {number} searchStart Index of start range in ranges array, inclusive
424 * @param {number} searchEnd Index of end range in ranges array, exclusive
425 * @param {number} position Last range index covers or less than position will be returned
426 * @returns {number} Index of first impacted range or -1 for no results
427 * @memberof PageBlobHandler
428 */
429 public locateLastImpactedRange(
430 ranges: PersistencyPageRange[],
431 searchStart: number,
432 searchEnd: number,
433 position: number
434 ): number {
435 searchStart = searchStart < 0 ? 0 : searchStart;
436 searchEnd = searchEnd > ranges.length ? searchEnd : searchEnd;
437 if (ranges.length === 0 || searchStart >= searchEnd) {
438 return -1;
439 }
440
441 // Only last element to check
442 if (searchStart === searchEnd - 1) {
443 return this.positionInRange(ranges[searchStart], position) ||
444 position > ranges[searchStart].end
445 ? searchStart
446 : -1;
447 }
448
449 // 2 or more elements left
450 const searchMid = Math.floor((searchStart + searchEnd) / 2);
451 const indexInRight = this.locateLastImpactedRange(
452 ranges,
453 searchMid + 1, // Remove searchMid range from searching scope
454 searchEnd,
455 position
456 );
457 if (indexInRight > -1) {
458 return indexInRight;
459 }
460 if (
461 this.positionInRange(ranges[searchMid], position) ||
462 position > ranges[searchMid].end
463 ) {
464 return searchMid;
465 } else {
466 return this.locateLastImpactedRange(
467 ranges,
468 searchStart,
469 searchMid,
470 position
471 );
472 }
473 }
474
475 public positionInRange(
476 range: PersistencyPageRange,
477 position: number
478 ): boolean {
479 return position >= range.start && position <= range.end;
480 }
481}