blob: e2221d035c379f3547cd5695a6f0bc63d9ec532b [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.paimon.sst;
import org.apache.paimon.fs.ByteArraySeekableStream;
import org.apache.paimon.fs.Path;
import org.apache.paimon.io.cache.CacheKey;
import org.apache.paimon.io.cache.CacheManager;
import org.apache.paimon.memory.MemorySegment;
import org.apache.paimon.memory.MemorySlice;
import org.apache.paimon.options.MemorySize;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
/** Tests for shared block ownership and file invalidation. */
class BlockCacheTest {
@ParameterizedTest
@ValueSource(booleans = {false, true})
@Timeout(30)
void testConcurrentLoadAfterFastLookupMissIsReused(boolean slice) throws Exception {
Path path = new Path("shared-file");
CountDownLatch missed = new CountDownLatch(1);
CountDownLatch resume = new CountDownLatch(1);
AtomicBoolean pause = new AtomicBoolean(true);
AtomicInteger loads = new AtomicInteger();
ExecutorService executor = Executors.newSingleThreadExecutor();
try (CacheManager manager =
new CacheManager(MemorySize.ofKibiBytes(64), 0) {
@Override
public MemorySlice getPageSliceIfPresent(CacheKey key) {
MemorySlice cached = super.getPageSliceIfPresent(key);
if (cached == null && pause.compareAndSet(true, false)) {
missed.countDown();
try {
if (!resume.await(10, TimeUnit.SECONDS)) {
throw new AssertionError("Timed out after cache miss");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
return cached;
}
};
BlockCache first = new BlockCache(path, input(), manager);
BlockCache second = new BlockCache(path, input(), manager)) {
Future<MemorySlice> pending = executor.submit(() -> read(first, slice, loads));
try {
assertThat(missed.await(10, TimeUnit.SECONDS)).isTrue();
MemorySlice loaded = read(second, slice, loads);
resume.countDown();
MemorySlice reused = pending.get(10, TimeUnit.SECONDS);
assertThat(reused.copyBytes()).containsExactly(1, 2, 3, 4);
assertThat(reused.segment()).isSameAs(loaded.segment());
assertThat(loads).hasValue(1);
} finally {
resume.countDown();
}
} finally {
executor.shutdownNow();
}
}
@Test
void testSharedReaderReloadsInvalidatedBlock() throws Exception {
Path path = new Path("shared-file");
AtomicInteger loads = new AtomicInteger();
try (CacheManager manager = new CacheManager(MemorySize.ofKibiBytes(64), 0);
BlockCache first = new BlockCache(path, input(), manager);
BlockCache second = new BlockCache(path, input(), manager)) {
MemorySegment original =
first.getBlock(
0,
4,
bytes -> {
loads.incrementAndGet();
return bytes;
},
false);
assertThat(
second.getBlock(
0,
4,
bytes -> {
loads.incrementAndGet();
return bytes;
},
false))
.isSameAs(original);
manager.invalidPage(CacheKey.forPosition(path, 0, 4, false));
MemorySegment reloaded =
second.getBlock(
0,
4,
bytes -> {
loads.incrementAndGet();
return bytes;
},
false);
assertThat(reloaded.getHeapMemory()).containsExactly(1, 2, 3, 4);
assertThat(loads).hasValue(2);
}
}
@Test
void testCloseInvalidatesAllReadersPagesForFile() throws Exception {
Path path = new Path("shared-file");
try (CacheManager manager = new CacheManager(MemorySize.ofKibiBytes(64), 0.5);
BlockCache first = new BlockCache(path, input(), manager);
BlockCache second = new BlockCache(path, input(), manager);
BlockCache other = new BlockCache(new Path("other-file"), input(), manager)) {
first.getBlock(0, 4, bytes -> bytes, false);
second.getBlock(4, 4, bytes -> bytes, true);
MemorySegment otherBlock = other.getBlock(0, 4, bytes -> bytes, false);
first.close();
assertThat(manager.contains(CacheKey.forPosition(path, 0, 4, false))).isFalse();
assertThat(manager.contains(CacheKey.forPosition(path, 4, 4, true))).isFalse();
assertThat(
other.getBlock(
0,
4,
bytes -> {
throw new AssertionError("Unexpected reload");
},
false))
.isSameAs(otherBlock);
}
}
private static MemorySlice read(BlockCache cache, boolean slice, AtomicInteger loads) {
if (slice) {
return cache.getBlockSlice(
0,
4,
bytes -> {
loads.incrementAndGet();
return MemorySlice.wrap(bytes);
},
false);
}
return MemorySlice.wrap(
cache.getBlock(
0,
4,
bytes -> {
loads.incrementAndGet();
return bytes;
},
false));
}
private static ByteArraySeekableStream input() {
return new ByteArraySeekableStream(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});
}
}