mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-22 21:59:59 +00:00
## Problem We recently added a "visibility" state to layers, but nothing initializes it. Part of: - #8398 ## Summary of changes - Add a dependency on `range-set-blaze`, which is used as a fast incrementally updated alternative to KeySpace. We could also use this to replace the internals of KeySpaceRandomAccum if we wanted to. Writing a type that does this kind of "BtreeMap & merge overlapping entries" thing isn't super complicated, but no reason to write this ourselves when there's a third party impl available. - Add a function to layermap to calculate visibilities for each layer - Add a function to Timeline to call into layermap and then apply these visibilities to the Layer objects. - Invoke the calculation during startup, after image layer creations, and when removing branches. Branch removal and image layer creation are the two ways that a layer can go from Visible to Covered. - Add unit test & benchmark for the visibility calculation - Expose `pageserver_visible_physical_size` metric, which should always be <= `pageserver_remote_physical_size`. - This metric will feed into the /v1/utilization endpoint later: the visible size indicates how much space we would like to use on this pageserver for this tenant. - When `pageserver_visible_physical_size` is greater than `pageserver_resident_physical_size`, this is a sign that the tenant has long-idle branches, which result in layers that are visible in principle, but not used in practice. This does not keep visibility hints up to date in all cases: particularly, when creating a child timeline, any previously covered layers will not get marked Visible until they are accessed. Updates after image layer creation could be implemented as more of a special case, but this would require more new code: the existing depth calculation code doesn't maintain+yield the list of deltas that would be covered by an image layer. ## Performance This operation is done rarely (at startup and at timeline deletion), so needs to be efficient but not ultra-fast. There is a new `visibility` bench that measures runtime for a synthetic 100k layers case (`sequential`) and a real layer map (`real_map`) with ~26k layers. The benchmark shows runtimes of single digit milliseconds (on a ryzen 7950). This confirms that the runtime shouldn't be a problem at startup (as we already incur S3-level latencies there), but that it's slow enough that we definitely shouldn't call it more often than necessary, and it may be worthwhile to optimize further later (things like: when removing a branch, only bother scanning layers below the branchpoint) ``` visibility/sequential time: [4.5087 ms 4.5894 ms 4.6775 ms] change: [+2.0826% +3.9097% +5.8995%] (p = 0.00 < 0.05) Performance has regressed. Found 24 outliers among 100 measurements (24.00%) 2 (2.00%) high mild 22 (22.00%) high severe min: 0/1696070, max: 93/1C0887F0 visibility/real_map time: [7.0796 ms 7.0832 ms 7.0871 ms] change: [+0.3900% +0.4505% +0.5164%] (p = 0.00 < 0.05) Change within noise threshold. Found 4 outliers among 100 measurements (4.00%) 3 (3.00%) high mild 1 (1.00%) high severe min: 0/1696070, max: 93/1C0887F0 visibility/real_map_many_branches time: [4.5285 ms 4.5355 ms 4.5434 ms] change: [-1.0012% -0.8004% -0.5969%] (p = 0.00 < 0.05) Change within noise threshold. ```
638 lines
19 KiB
Rust
638 lines
19 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::ops::Range;
|
|
|
|
use tracing::info;
|
|
|
|
use crate::tenant::storage_layer::PersistentLayerDesc;
|
|
|
|
use super::layer_coverage::LayerCoverageTuple;
|
|
|
|
/// Layers in this module are identified and indexed by this data.
|
|
///
|
|
/// This is a helper struct to enable sorting layers by lsn.start.
|
|
///
|
|
/// These three values are enough to uniquely identify a layer, since
|
|
/// a layer is obligated to contain all contents within range, so two
|
|
/// deltas (or images) with the same range have identical content.
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub struct LayerKey {
|
|
// TODO I use i128 and u64 because it was easy for prototyping,
|
|
// testing, and benchmarking. If we can use the Lsn and Key
|
|
// types without overhead that would be preferable.
|
|
pub key: Range<i128>,
|
|
pub lsn: Range<u64>,
|
|
pub is_image: bool,
|
|
}
|
|
|
|
impl PartialOrd for LayerKey {
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for LayerKey {
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
// NOTE we really care about comparing by lsn.start first
|
|
self.lsn
|
|
.start
|
|
.cmp(&other.lsn.start)
|
|
.then(self.lsn.end.cmp(&other.lsn.end))
|
|
.then(self.key.start.cmp(&other.key.start))
|
|
.then(self.key.end.cmp(&other.key.end))
|
|
.then(self.is_image.cmp(&other.is_image))
|
|
}
|
|
}
|
|
|
|
impl From<&PersistentLayerDesc> for LayerKey {
|
|
fn from(layer: &PersistentLayerDesc) -> Self {
|
|
let kr = layer.get_key_range();
|
|
let lr = layer.get_lsn_range();
|
|
LayerKey {
|
|
key: kr.start.to_i128()..kr.end.to_i128(),
|
|
lsn: lr.start.0..lr.end.0,
|
|
is_image: !layer.is_incremental(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Efficiently queryable layer coverage for each LSN.
|
|
///
|
|
/// Allows answering layer map queries very efficiently,
|
|
/// but doesn't allow retroactive insertion, which is
|
|
/// sometimes necessary. See BufferedHistoricLayerCoverage.
|
|
pub struct HistoricLayerCoverage<Value> {
|
|
/// The latest state
|
|
head: LayerCoverageTuple<Value>,
|
|
|
|
/// All previous states
|
|
historic: BTreeMap<u64, LayerCoverageTuple<Value>>,
|
|
}
|
|
|
|
impl<T: Clone> Default for HistoricLayerCoverage<T> {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl<Value: Clone> HistoricLayerCoverage<Value> {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
head: LayerCoverageTuple::default(),
|
|
historic: BTreeMap::default(),
|
|
}
|
|
}
|
|
|
|
/// Add a layer
|
|
///
|
|
/// Panics if new layer has older lsn.start than an existing layer.
|
|
/// See BufferedHistoricLayerCoverage for a more general insertion method.
|
|
pub fn insert(&mut self, layer_key: LayerKey, value: Value) {
|
|
// It's only a persistent map, not a retroactive one
|
|
if let Some(last_entry) = self.historic.iter().next_back() {
|
|
let last_lsn = last_entry.0;
|
|
if layer_key.lsn.start < *last_lsn {
|
|
panic!("unexpected retroactive insert");
|
|
}
|
|
}
|
|
|
|
// Insert into data structure
|
|
let target = if layer_key.is_image {
|
|
&mut self.head.image_coverage
|
|
} else {
|
|
&mut self.head.delta_coverage
|
|
};
|
|
|
|
target.insert(layer_key.key, layer_key.lsn.clone(), value);
|
|
|
|
// Remember history. Clone is O(1)
|
|
self.historic.insert(layer_key.lsn.start, self.head.clone());
|
|
}
|
|
|
|
/// Query at a particular LSN, inclusive
|
|
pub fn get_version(&self, lsn: u64) -> Option<&LayerCoverageTuple<Value>> {
|
|
match self.historic.range(..=lsn).next_back() {
|
|
Some((_, v)) => Some(v),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
/// Remove all entries after a certain LSN (inclusive)
|
|
pub fn trim(&mut self, begin: &u64) {
|
|
self.historic.split_off(begin);
|
|
self.head = self
|
|
.historic
|
|
.iter()
|
|
.next_back()
|
|
.map(|(_, v)| v.clone())
|
|
.unwrap_or_default();
|
|
}
|
|
}
|
|
|
|
/// This is the most basic test that demonstrates intended usage.
|
|
/// All layers in this test have height 1.
|
|
#[test]
|
|
fn test_persistent_simple() {
|
|
let mut map = HistoricLayerCoverage::<String>::new();
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..5,
|
|
lsn: 100..101,
|
|
is_image: true,
|
|
},
|
|
"Layer 1".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 3..9,
|
|
lsn: 110..111,
|
|
is_image: true,
|
|
},
|
|
"Layer 2".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 5..6,
|
|
lsn: 120..121,
|
|
is_image: true,
|
|
},
|
|
"Layer 3".to_string(),
|
|
);
|
|
|
|
// After Layer 1 insertion
|
|
let version = map.get_version(105).unwrap();
|
|
assert_eq!(version.image_coverage.query(1), Some("Layer 1".to_string()));
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 1".to_string()));
|
|
|
|
// After Layer 2 insertion
|
|
let version = map.get_version(115).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 2".to_string()));
|
|
assert_eq!(version.image_coverage.query(8), Some("Layer 2".to_string()));
|
|
assert_eq!(version.image_coverage.query(11), None);
|
|
|
|
// After Layer 3 insertion
|
|
let version = map.get_version(125).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 2".to_string()));
|
|
assert_eq!(version.image_coverage.query(5), Some("Layer 3".to_string()));
|
|
assert_eq!(version.image_coverage.query(7), Some("Layer 2".to_string()));
|
|
}
|
|
|
|
/// Cover simple off-by-one edge cases
|
|
#[test]
|
|
fn test_off_by_one() {
|
|
let mut map = HistoricLayerCoverage::<String>::new();
|
|
map.insert(
|
|
LayerKey {
|
|
key: 3..5,
|
|
lsn: 100..110,
|
|
is_image: true,
|
|
},
|
|
"Layer 1".to_string(),
|
|
);
|
|
|
|
// Check different LSNs
|
|
let version = map.get_version(99);
|
|
assert!(version.is_none());
|
|
let version = map.get_version(100).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 1".to_string()));
|
|
let version = map.get_version(110).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 1".to_string()));
|
|
|
|
// Check different keys
|
|
let version = map.get_version(105).unwrap();
|
|
assert_eq!(version.image_coverage.query(2), None);
|
|
assert_eq!(version.image_coverage.query(3), Some("Layer 1".to_string()));
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 1".to_string()));
|
|
assert_eq!(version.image_coverage.query(5), None);
|
|
}
|
|
|
|
/// White-box regression test, checking for incorrect removal of node at key.end
|
|
#[test]
|
|
fn test_regression() {
|
|
let mut map = HistoricLayerCoverage::<String>::new();
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..5,
|
|
lsn: 0..5,
|
|
is_image: false,
|
|
},
|
|
"Layer 1".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..5,
|
|
lsn: 1..2,
|
|
is_image: false,
|
|
},
|
|
"Layer 2".to_string(),
|
|
);
|
|
|
|
// If an insertion operation improperly deletes the endpoint of a previous layer
|
|
// (which is more likely to happen with layers that collide on key.end), we will
|
|
// end up with an infinite layer, covering the entire keyspace. Here we assert
|
|
// that there's no layer at key 100 because we didn't insert any layer there.
|
|
let version = map.get_version(100).unwrap();
|
|
assert_eq!(version.delta_coverage.query(100), None);
|
|
}
|
|
|
|
/// Cover edge cases where layers begin or end on the same key
|
|
#[test]
|
|
fn test_key_collision() {
|
|
let mut map = HistoricLayerCoverage::<String>::new();
|
|
|
|
map.insert(
|
|
LayerKey {
|
|
key: 3..5,
|
|
lsn: 100..110,
|
|
is_image: true,
|
|
},
|
|
"Layer 10".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 5..8,
|
|
lsn: 100..110,
|
|
is_image: true,
|
|
},
|
|
"Layer 11".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 3..4,
|
|
lsn: 200..210,
|
|
is_image: true,
|
|
},
|
|
"Layer 20".to_string(),
|
|
);
|
|
|
|
// Check after layer 11
|
|
let version = map.get_version(105).unwrap();
|
|
assert_eq!(version.image_coverage.query(2), None);
|
|
assert_eq!(
|
|
version.image_coverage.query(3),
|
|
Some("Layer 10".to_string())
|
|
);
|
|
assert_eq!(
|
|
version.image_coverage.query(5),
|
|
Some("Layer 11".to_string())
|
|
);
|
|
assert_eq!(
|
|
version.image_coverage.query(7),
|
|
Some("Layer 11".to_string())
|
|
);
|
|
assert_eq!(version.image_coverage.query(8), None);
|
|
|
|
// Check after layer 20
|
|
let version = map.get_version(205).unwrap();
|
|
assert_eq!(version.image_coverage.query(2), None);
|
|
assert_eq!(
|
|
version.image_coverage.query(3),
|
|
Some("Layer 20".to_string())
|
|
);
|
|
assert_eq!(
|
|
version.image_coverage.query(5),
|
|
Some("Layer 11".to_string())
|
|
);
|
|
assert_eq!(
|
|
version.image_coverage.query(7),
|
|
Some("Layer 11".to_string())
|
|
);
|
|
assert_eq!(version.image_coverage.query(8), None);
|
|
}
|
|
|
|
/// Test when rectangles have nontrivial height and possibly overlap
|
|
#[test]
|
|
fn test_persistent_overlapping() {
|
|
let mut map = HistoricLayerCoverage::<String>::new();
|
|
|
|
// Add 3 key-disjoint layers with varying LSN ranges
|
|
map.insert(
|
|
LayerKey {
|
|
key: 1..2,
|
|
lsn: 100..200,
|
|
is_image: true,
|
|
},
|
|
"Layer 1".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 4..5,
|
|
lsn: 110..200,
|
|
is_image: true,
|
|
},
|
|
"Layer 2".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 7..8,
|
|
lsn: 120..300,
|
|
is_image: true,
|
|
},
|
|
"Layer 3".to_string(),
|
|
);
|
|
|
|
// Add wide and short layer
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..9,
|
|
lsn: 130..199,
|
|
is_image: true,
|
|
},
|
|
"Layer 4".to_string(),
|
|
);
|
|
|
|
// Add wide layer taller than some
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..9,
|
|
lsn: 140..201,
|
|
is_image: true,
|
|
},
|
|
"Layer 5".to_string(),
|
|
);
|
|
|
|
// Add wide layer taller than all
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..9,
|
|
lsn: 150..301,
|
|
is_image: true,
|
|
},
|
|
"Layer 6".to_string(),
|
|
);
|
|
|
|
// After layer 4 insertion
|
|
let version = map.get_version(135).unwrap();
|
|
assert_eq!(version.image_coverage.query(0), Some("Layer 4".to_string()));
|
|
assert_eq!(version.image_coverage.query(1), Some("Layer 1".to_string()));
|
|
assert_eq!(version.image_coverage.query(2), Some("Layer 4".to_string()));
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 2".to_string()));
|
|
assert_eq!(version.image_coverage.query(5), Some("Layer 4".to_string()));
|
|
assert_eq!(version.image_coverage.query(7), Some("Layer 3".to_string()));
|
|
assert_eq!(version.image_coverage.query(8), Some("Layer 4".to_string()));
|
|
|
|
// After layer 5 insertion
|
|
let version = map.get_version(145).unwrap();
|
|
assert_eq!(version.image_coverage.query(0), Some("Layer 5".to_string()));
|
|
assert_eq!(version.image_coverage.query(1), Some("Layer 5".to_string()));
|
|
assert_eq!(version.image_coverage.query(2), Some("Layer 5".to_string()));
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 5".to_string()));
|
|
assert_eq!(version.image_coverage.query(5), Some("Layer 5".to_string()));
|
|
assert_eq!(version.image_coverage.query(7), Some("Layer 3".to_string()));
|
|
assert_eq!(version.image_coverage.query(8), Some("Layer 5".to_string()));
|
|
|
|
// After layer 6 insertion
|
|
let version = map.get_version(155).unwrap();
|
|
assert_eq!(version.image_coverage.query(0), Some("Layer 6".to_string()));
|
|
assert_eq!(version.image_coverage.query(1), Some("Layer 6".to_string()));
|
|
assert_eq!(version.image_coverage.query(2), Some("Layer 6".to_string()));
|
|
assert_eq!(version.image_coverage.query(4), Some("Layer 6".to_string()));
|
|
assert_eq!(version.image_coverage.query(5), Some("Layer 6".to_string()));
|
|
assert_eq!(version.image_coverage.query(7), Some("Layer 6".to_string()));
|
|
assert_eq!(version.image_coverage.query(8), Some("Layer 6".to_string()));
|
|
}
|
|
|
|
/// Wrapper for HistoricLayerCoverage that allows us to hack around the lack
|
|
/// of support for retroactive insertion by rebuilding the map since the
|
|
/// change.
|
|
///
|
|
/// Why is this needed? We most often insert new layers with newer LSNs,
|
|
/// but during compaction we create layers with non-latest LSN, and during
|
|
/// GC we delete historic layers.
|
|
///
|
|
/// Even though rebuilding is an expensive (N log N) solution to the problem,
|
|
/// it's not critical since we do something equally expensive just to decide
|
|
/// whether or not to create new image layers.
|
|
/// TODO It's not expensive but it's not great to hold a layer map write lock
|
|
/// for that long.
|
|
///
|
|
/// If this becomes an actual bottleneck, one solution would be to build a
|
|
/// segment tree that holds PersistentLayerMaps. Though this would mean that
|
|
/// we take an additional log(N) performance hit for queries, which will probably
|
|
/// still be more critical.
|
|
///
|
|
/// See this for more on persistent and retroactive techniques:
|
|
/// <https://www.youtube.com/watch?v=WqCWghETNDc&t=581s>
|
|
pub struct BufferedHistoricLayerCoverage<Value> {
|
|
/// A persistent layer map that we rebuild when we need to retroactively update
|
|
historic_coverage: HistoricLayerCoverage<Value>,
|
|
|
|
/// We buffer insertion into the PersistentLayerMap to decrease the number of rebuilds.
|
|
buffer: BTreeMap<LayerKey, Option<Value>>,
|
|
|
|
/// All current layers. This is not used for search. Only to make rebuilds easier.
|
|
layers: BTreeMap<LayerKey, Value>,
|
|
}
|
|
|
|
impl<T: std::fmt::Debug> std::fmt::Debug for BufferedHistoricLayerCoverage<T> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("RetroactiveLayerMap")
|
|
.field("buffer", &self.buffer)
|
|
.field("layers", &self.layers)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl<T: Clone> Default for BufferedHistoricLayerCoverage<T> {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl<Value: Clone> BufferedHistoricLayerCoverage<Value> {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
historic_coverage: HistoricLayerCoverage::<Value>::new(),
|
|
buffer: BTreeMap::new(),
|
|
layers: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn insert(&mut self, layer_key: LayerKey, value: Value) {
|
|
self.buffer.insert(layer_key, Some(value));
|
|
}
|
|
|
|
pub fn remove(&mut self, layer_key: LayerKey) {
|
|
self.buffer.insert(layer_key, None);
|
|
}
|
|
|
|
pub fn rebuild(&mut self) {
|
|
// Find the first LSN that needs to be rebuilt
|
|
let rebuild_since: u64 = match self.buffer.iter().next() {
|
|
Some((LayerKey { lsn, .. }, _)) => lsn.start,
|
|
None => return, // No need to rebuild if buffer is empty
|
|
};
|
|
|
|
// Apply buffered updates to self.layers
|
|
let num_updates = self.buffer.len();
|
|
self.buffer.retain(|layer_key, layer| {
|
|
match layer {
|
|
Some(l) => {
|
|
self.layers.insert(layer_key.clone(), l.clone());
|
|
}
|
|
None => {
|
|
self.layers.remove(layer_key);
|
|
}
|
|
};
|
|
false
|
|
});
|
|
|
|
// Rebuild
|
|
let mut num_inserted = 0;
|
|
self.historic_coverage.trim(&rebuild_since);
|
|
for (layer_key, layer) in self.layers.range(
|
|
LayerKey {
|
|
lsn: rebuild_since..0,
|
|
key: 0..0,
|
|
is_image: false,
|
|
}..,
|
|
) {
|
|
self.historic_coverage
|
|
.insert(layer_key.clone(), layer.clone());
|
|
num_inserted += 1;
|
|
}
|
|
|
|
// TODO maybe only warn if ratio is at least 10
|
|
info!(
|
|
"Rebuilt layer map. Did {} insertions to process a batch of {} updates.",
|
|
num_inserted, num_updates,
|
|
)
|
|
}
|
|
|
|
/// Iterate all the layers
|
|
pub fn iter(&self) -> impl '_ + Iterator<Item = Value> {
|
|
// NOTE we can actually perform this without rebuilding,
|
|
// but it's not necessary for now.
|
|
if !self.buffer.is_empty() {
|
|
panic!("rebuild pls")
|
|
}
|
|
|
|
self.layers.values().cloned()
|
|
}
|
|
|
|
/// Return a reference to a queryable map, assuming all updates
|
|
/// have already been processed using self.rebuild()
|
|
pub fn get(&self) -> anyhow::Result<&HistoricLayerCoverage<Value>> {
|
|
// NOTE we error here instead of implicitly rebuilding because
|
|
// rebuilding is somewhat expensive.
|
|
// TODO maybe implicitly rebuild and log/sentry an error?
|
|
if !self.buffer.is_empty() {
|
|
anyhow::bail!("rebuild required")
|
|
}
|
|
|
|
Ok(&self.historic_coverage)
|
|
}
|
|
|
|
pub(crate) fn len(&self) -> usize {
|
|
self.layers.len()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_retroactive_regression_1() {
|
|
let mut map = BufferedHistoricLayerCoverage::new();
|
|
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..21267647932558653966460912964485513215,
|
|
lsn: 23761336..23761457,
|
|
is_image: false,
|
|
},
|
|
"sdfsdfs".to_string(),
|
|
);
|
|
|
|
map.rebuild();
|
|
|
|
let version = map.get().unwrap().get_version(23761457).unwrap();
|
|
assert_eq!(
|
|
version.delta_coverage.query(100),
|
|
Some("sdfsdfs".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_retroactive_simple() {
|
|
let mut map = BufferedHistoricLayerCoverage::new();
|
|
|
|
// Append some images in increasing LSN order
|
|
map.insert(
|
|
LayerKey {
|
|
key: 0..5,
|
|
lsn: 100..101,
|
|
is_image: true,
|
|
},
|
|
"Image 1".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 3..9,
|
|
lsn: 110..111,
|
|
is_image: true,
|
|
},
|
|
"Image 2".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 4..6,
|
|
lsn: 120..121,
|
|
is_image: true,
|
|
},
|
|
"Image 3".to_string(),
|
|
);
|
|
map.insert(
|
|
LayerKey {
|
|
key: 8..9,
|
|
lsn: 120..121,
|
|
is_image: true,
|
|
},
|
|
"Image 4".to_string(),
|
|
);
|
|
|
|
// Add a delta layer out of order
|
|
map.insert(
|
|
LayerKey {
|
|
key: 2..5,
|
|
lsn: 105..106,
|
|
is_image: false,
|
|
},
|
|
"Delta 1".to_string(),
|
|
);
|
|
|
|
// Rebuild so we can start querying
|
|
map.rebuild();
|
|
|
|
{
|
|
let map = map.get().expect("rebuilt");
|
|
|
|
let version = map.get_version(90);
|
|
assert!(version.is_none());
|
|
let version = map.get_version(102).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Image 1".to_string()));
|
|
|
|
let version = map.get_version(107).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Image 1".to_string()));
|
|
assert_eq!(version.delta_coverage.query(4), Some("Delta 1".to_string()));
|
|
|
|
let version = map.get_version(115).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Image 2".to_string()));
|
|
|
|
let version = map.get_version(125).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Image 3".to_string()));
|
|
}
|
|
|
|
// Remove Image 3
|
|
map.remove(LayerKey {
|
|
key: 4..6,
|
|
lsn: 120..121,
|
|
is_image: true,
|
|
});
|
|
map.rebuild();
|
|
|
|
{
|
|
// Check deletion worked
|
|
let map = map.get().expect("rebuilt");
|
|
let version = map.get_version(125).unwrap();
|
|
assert_eq!(version.image_coverage.query(4), Some("Image 2".to_string()));
|
|
assert_eq!(version.image_coverage.query(8), Some("Image 4".to_string()));
|
|
}
|
|
}
|