Skip to content

Commit f72932d

Browse files
authored
HBASE-30025 Wire TieredExclusiveTopology as CombinedBlockCache-compatible service (#8501)
Signed-off-by: Tak Lon (Stephen) Wu <taklwu@apache.org>
1 parent b8cb97f commit f72932d

3 files changed

Lines changed: 426 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.io.hfile.cache;
19+
20+
import java.util.Objects;
21+
import org.apache.hadoop.hbase.io.hfile.BlockCache;
22+
import org.apache.yetus.audience.InterfaceAudience;
23+
24+
/**
25+
* Factory helpers for topology-backed {@link CacheAccessService} instances.
26+
* <p>
27+
* These helpers are intended for transitional wiring while existing cache implementations still
28+
* expose the legacy {@link BlockCache} API. The supplied block caches are adapted to
29+
* {@link CacheEngine} using {@link BlockCacheBackedCacheEngine}, assembled into a
30+
* {@link TieredExclusiveTopology}, and exposed through {@link TopologyBackedCacheAccessService}.
31+
* </p>
32+
* <p>
33+
* This class does not change production cache wiring by itself. It only provides a reusable
34+
* construction path for tests and later migration steps that need a CombinedBlockCache-compatible
35+
* topology-backed service.
36+
* </p>
37+
*/
38+
@InterfaceAudience.Private
39+
public final class TopologyBackedCacheAccessServices {
40+
41+
private TopologyBackedCacheAccessServices() {
42+
}
43+
44+
/**
45+
* Creates a topology-backed cache access service from existing L1 and L2 block caches.
46+
* <p>
47+
* The resulting service uses {@link TieredExclusiveTopology}, which models the current
48+
* CombinedBlockCache-compatible L1/L2 behavior where promotion can move a block from one tier to
49+
* another.
50+
* </p>
51+
* @param name human-readable topology/service name
52+
* @param l1 L1 block cache
53+
* @param l2 L2 block cache
54+
* @param policy placement and admission policy
55+
* @return topology-backed cache access service
56+
*/
57+
public static TopologyBackedCacheAccessService fromTieredExclusiveBlockCaches(String name,
58+
BlockCache l1, BlockCache l2, CachePlacementAdmissionPolicy policy) {
59+
Objects.requireNonNull(name, "name must not be null");
60+
Objects.requireNonNull(l1, "l1 must not be null");
61+
Objects.requireNonNull(l2, "l2 must not be null");
62+
Objects.requireNonNull(policy, "policy must not be null");
63+
64+
CacheEngine l1Engine = CacheEngines.fromBlockCache(l1);
65+
CacheEngine l2Engine = CacheEngines.fromBlockCache(l2);
66+
CacheTopology topology = new TieredExclusiveTopology(name, l1Engine, l2Engine);
67+
return new TopologyBackedCacheAccessService(topology, policy);
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.io.hfile.cache;
19+
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertNull;
22+
import static org.junit.jupiter.api.Assertions.assertSame;
23+
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.ArgumentMatchers.anyBoolean;
25+
import static org.mockito.ArgumentMatchers.eq;
26+
import static org.mockito.Mockito.mock;
27+
import static org.mockito.Mockito.mockingDetails;
28+
import static org.mockito.Mockito.never;
29+
import static org.mockito.Mockito.verify;
30+
import static org.mockito.Mockito.when;
31+
32+
import java.util.Arrays;
33+
import org.apache.hadoop.hbase.io.hfile.BlockCache;
34+
import org.apache.hadoop.hbase.io.hfile.BlockCacheKey;
35+
import org.apache.hadoop.hbase.io.hfile.Cacheable;
36+
import org.apache.hadoop.hbase.testclassification.IOTests;
37+
import org.apache.hadoop.hbase.testclassification.SmallTests;
38+
import org.junit.jupiter.api.Tag;
39+
import org.junit.jupiter.api.Test;
40+
import org.mockito.invocation.Invocation;
41+
42+
@Tag(IOTests.TAG)
43+
@Tag(SmallTests.TAG)
44+
public class TestCombinedBlockCacheCompatibleTopologyBackedCacheAccessService {
45+
46+
@Test
47+
void testL1HitReturnsBlockWithoutCheckingL2() {
48+
BlockCache l1 = mock(BlockCache.class);
49+
BlockCache l2 = mock(BlockCache.class);
50+
BlockCacheKey key = new BlockCacheKey("file", 1L);
51+
Cacheable block = mock(Cacheable.class);
52+
53+
when(l1.getBlock(key, true, false, true)).thenReturn(block);
54+
55+
TopologyBackedCacheAccessService service = service(l1, l2, noPromotionPolicy());
56+
57+
assertSame(block, service.getBlock(key, requestContext()));
58+
59+
verify(l1).getBlock(key, true, false, true);
60+
verify(l2, never()).getBlock(any(), anyBoolean(), anyBoolean(), anyBoolean());
61+
}
62+
63+
@Test
64+
void testL2HitReturnsBlock() {
65+
BlockCache l1 = mock(BlockCache.class);
66+
BlockCache l2 = mock(BlockCache.class);
67+
BlockCacheKey key = new BlockCacheKey("file", 1L);
68+
Cacheable block = mock(Cacheable.class);
69+
70+
when(l1.getBlock(key, true, false, true)).thenReturn(null);
71+
when(l2.getBlock(key, true, false, true)).thenReturn(block);
72+
73+
TopologyBackedCacheAccessService service = service(l1, l2, noPromotionPolicy());
74+
75+
assertSame(block, service.getBlock(key, requestContext()));
76+
77+
verify(l1).getBlock(key, true, false, true);
78+
verify(l2).getBlock(key, true, false, true);
79+
}
80+
81+
@Test
82+
void testMissReturnsNull() {
83+
BlockCache l1 = mock(BlockCache.class);
84+
BlockCache l2 = mock(BlockCache.class);
85+
BlockCacheKey key = new BlockCacheKey("file", 1L);
86+
87+
when(l1.getBlock(key, true, false, true)).thenReturn(null);
88+
when(l2.getBlock(key, true, false, true)).thenReturn(null);
89+
90+
TopologyBackedCacheAccessService service = service(l1, l2, noPromotionPolicy());
91+
92+
assertNull(service.getBlock(key, requestContext()));
93+
94+
verify(l1).getBlock(key, true, false, true);
95+
verify(l2).getBlock(key, true, false, true);
96+
}
97+
98+
@Test
99+
void testL2HitWithPromotionMovesBlockToL1() {
100+
BlockCache l1 = mock(BlockCache.class);
101+
BlockCache l2 = mock(BlockCache.class);
102+
BlockCacheKey key = new BlockCacheKey("file", 1L);
103+
Cacheable block = mock(Cacheable.class);
104+
105+
when(l1.getBlock(key, true, false, true)).thenReturn(null);
106+
when(l2.getBlock(key, true, false, true)).thenReturn(block);
107+
108+
TopologyBackedCacheAccessService service = service(l1, l2, promoteL2HitToL1Policy());
109+
110+
assertSame(block, service.getBlock(key, requestContext()));
111+
112+
verify(l1).getBlock(key, true, false, true);
113+
verify(l2).getBlock(key, true, false, true);
114+
assertCachedExactlyOnce(l1, key, block);
115+
verify(l2).evictBlock(key);
116+
assertNotCached(l2);
117+
}
118+
119+
@Test
120+
void testRejectedBlockIsNotCached() {
121+
BlockCache l1 = mock(BlockCache.class);
122+
BlockCache l2 = mock(BlockCache.class);
123+
BlockCacheKey key = new BlockCacheKey("file", 1L);
124+
Cacheable block = mock(Cacheable.class);
125+
126+
TopologyBackedCacheAccessService service = service(l1, l2, rejectPolicy());
127+
128+
service.cacheBlock(key, block, writeContext());
129+
130+
verify(l1, never()).cacheBlock(any(), any());
131+
verify(l1, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
132+
verify(l2, never()).cacheBlock(any(), any());
133+
verify(l2, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
134+
}
135+
136+
@Test
137+
void testCacheBlockToL1() {
138+
BlockCache l1 = mock(BlockCache.class);
139+
BlockCache l2 = mock(BlockCache.class);
140+
BlockCacheKey key = new BlockCacheKey("file", 1L);
141+
Cacheable block = mock(Cacheable.class);
142+
143+
TopologyBackedCacheAccessService service = service(l1, l2, admitToTiersPolicy(CacheTier.L1));
144+
145+
service.cacheBlock(key, block, writeContext());
146+
147+
verify(l1).cacheBlock(key, block, false, false);
148+
verify(l2, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
149+
}
150+
151+
@Test
152+
void testCacheBlockToL2() {
153+
BlockCache l1 = mock(BlockCache.class);
154+
BlockCache l2 = mock(BlockCache.class);
155+
BlockCacheKey key = new BlockCacheKey("file", 1L);
156+
Cacheable block = mock(Cacheable.class);
157+
158+
TopologyBackedCacheAccessService service = service(l1, l2, admitToTiersPolicy(CacheTier.L2));
159+
160+
service.cacheBlock(key, block, writeContext());
161+
162+
verify(l2).cacheBlock(key, block, false, false);
163+
verify(l1, never()).cacheBlock(any(), any(), anyBoolean(), anyBoolean());
164+
}
165+
166+
@Test
167+
void testCacheBlockToBothTiers() {
168+
BlockCache l1 = mock(BlockCache.class);
169+
BlockCache l2 = mock(BlockCache.class);
170+
BlockCacheKey key = new BlockCacheKey("file", 1L);
171+
Cacheable block = mock(Cacheable.class);
172+
173+
TopologyBackedCacheAccessService service =
174+
service(l1, l2, admitToTiersPolicy(CacheTier.L1, CacheTier.L2));
175+
176+
service.cacheBlock(key, block, writeContext());
177+
178+
verify(l1).cacheBlock(key, block, false, false);
179+
verify(l2).cacheBlock(key, block, false, false);
180+
}
181+
182+
@Test
183+
void testEvictBlockEvictsFromBothTiers() {
184+
BlockCache l1 = mock(BlockCache.class);
185+
BlockCache l2 = mock(BlockCache.class);
186+
BlockCacheKey key = new BlockCacheKey("file", 1L);
187+
188+
TopologyBackedCacheAccessService service = service(l1, l2, noPromotionPolicy());
189+
190+
service.evictBlock(key);
191+
192+
verify(l1).evictBlock(key);
193+
verify(l2).evictBlock(key);
194+
}
195+
196+
@Test
197+
void testShutdownShutsDownBothTiers() {
198+
BlockCache l1 = mock(BlockCache.class);
199+
BlockCache l2 = mock(BlockCache.class);
200+
201+
TopologyBackedCacheAccessService service = service(l1, l2, noPromotionPolicy());
202+
203+
service.shutdown();
204+
205+
verify(l1).shutdown();
206+
verify(l2).shutdown();
207+
}
208+
209+
private static TopologyBackedCacheAccessService service(BlockCache l1, BlockCache l2,
210+
CachePlacementAdmissionPolicy policy) {
211+
return TopologyBackedCacheAccessServices.fromTieredExclusiveBlockCaches("combined", l1, l2,
212+
policy);
213+
}
214+
215+
private static CacheRequestContext requestContext() {
216+
return CacheRequestContext.newBuilder().withCaching(true).withRepeat(false)
217+
.withUpdateCacheMetrics(true).build();
218+
}
219+
220+
private static CacheWriteContext writeContext() {
221+
return CacheWriteContext.newBuilder().withInMemory(false).withWaitWhenCache(false).build();
222+
}
223+
224+
private static CachePlacementAdmissionPolicy noPromotionPolicy() {
225+
CachePlacementAdmissionPolicy policy = mock(CachePlacementAdmissionPolicy.class);
226+
when(policy.shouldPromote(any(), any(), any(), any(), any()))
227+
.thenReturn(PromotionDecision.none());
228+
return policy;
229+
}
230+
231+
private static CachePlacementAdmissionPolicy promoteL2HitToL1Policy() {
232+
CachePlacementAdmissionPolicy policy = mock(CachePlacementAdmissionPolicy.class);
233+
when(policy.shouldPromote(any(), any(), eq(CacheTier.L1), any(), any()))
234+
.thenReturn(PromotionDecision.none());
235+
when(policy.shouldPromote(any(), any(), eq(CacheTier.L2), any(), any()))
236+
.thenReturn(PromotionDecision.promoteTo(CacheTier.L1, false));
237+
return policy;
238+
}
239+
240+
private static CachePlacementAdmissionPolicy admitToTiersPolicy(CacheTier... tiers) {
241+
CachePlacementAdmissionPolicy policy = mock(CachePlacementAdmissionPolicy.class);
242+
when(policy.shouldAdmit(any(), any(), any(), any(), any()))
243+
.thenReturn(AdmissionDecision.admit());
244+
when(policy.selectTier(any(), any(), any(), any()))
245+
.thenReturn(TierDecision.multiple(Arrays.asList(tiers)));
246+
return policy;
247+
}
248+
249+
private static CachePlacementAdmissionPolicy rejectPolicy() {
250+
CachePlacementAdmissionPolicy policy = mock(CachePlacementAdmissionPolicy.class);
251+
when(policy.shouldAdmit(any(), any(), any(), any(), any()))
252+
.thenReturn(AdmissionDecision.reject("test rejection"));
253+
return policy;
254+
}
255+
256+
private static void assertCachedExactlyOnce(BlockCache cache, BlockCacheKey key,
257+
Cacheable block) {
258+
long cacheBlockCalls = mockingDetails(cache).getInvocations().stream()
259+
.filter(invocation -> isCacheBlockInvocation(invocation, key, block)).count();
260+
261+
assertEquals(1, cacheBlockCalls);
262+
}
263+
264+
private static void assertNotCached(BlockCache cache) {
265+
long cacheBlockCalls = mockingDetails(cache).getInvocations().stream()
266+
.filter(invocation -> "cacheBlock".equals(invocation.getMethod().getName())).count();
267+
268+
assertEquals(0, cacheBlockCalls);
269+
}
270+
271+
private static boolean isCacheBlockInvocation(Invocation invocation, BlockCacheKey key,
272+
Cacheable block) {
273+
if (!"cacheBlock".equals(invocation.getMethod().getName())) {
274+
return false;
275+
}
276+
277+
Object[] arguments = invocation.getArguments();
278+
return arguments.length >= 2 && arguments[0] == key && arguments[1] == block;
279+
}
280+
}

0 commit comments

Comments
 (0)