001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.io.hfile.bucket;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertFalse;
022import static org.junit.Assert.assertNotEquals;
023import static org.junit.Assert.assertNotNull;
024import static org.junit.Assert.assertNull;
025import static org.junit.Assert.assertTrue;
026
027import java.util.ArrayList;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.CountDownLatch;
033import java.util.concurrent.ExecutorService;
034import java.util.concurrent.Executors;
035import java.util.concurrent.TimeUnit;
036import java.util.concurrent.atomic.AtomicInteger;
037import org.apache.hadoop.hbase.HBaseClassTestRule;
038import org.apache.hadoop.hbase.testclassification.SmallTests;
039import org.junit.Before;
040import org.junit.ClassRule;
041import org.junit.Test;
042import org.junit.experimental.categories.Category;
043
044/**
045 * Tests for {@link FilePathStringPool}
046 */
047@Category({ SmallTests.class })
048public class TestFilePathStringPool {
049
050  @ClassRule
051  public static final HBaseClassTestRule CLASS_RULE =
052    HBaseClassTestRule.forClass(TestFilePathStringPool.class);
053
054  private FilePathStringPool pool;
055
056  @Before
057  public void setUp() {
058    pool = FilePathStringPool.getInstance();
059    pool.clear();
060  }
061
062  @Test
063  public void testSingletonPattern() {
064    FilePathStringPool instance1 = FilePathStringPool.getInstance();
065    FilePathStringPool instance2 = FilePathStringPool.getInstance();
066    assertNotNull(instance1);
067    assertNotNull(instance2);
068    assertEquals(instance1, instance2);
069  }
070
071  @Test
072  public void testBasicEncodeDecodeRoundTrip() {
073    String testString = "/hbase/data/default/test-table/region1/cf1/file1.hfile";
074    int id = pool.encode(testString);
075    String decoded = pool.decode(id);
076    assertEquals(testString, decoded);
077  }
078
079  @Test
080  public void testEncodeReturnsSameIdForSameString() {
081    String testString = "/hbase/data/file.hfile";
082    int id1 = pool.encode(testString);
083    int id2 = pool.encode(testString);
084    assertEquals(id1, id2);
085    assertEquals(1, pool.size());
086  }
087
088  @Test
089  public void testEncodeDifferentStringsGetDifferentIds() {
090    String string1 = "/path/to/file1.hfile";
091    String string2 = "/path/to/file2.hfile";
092    int id1 = pool.encode(string1);
093    int id2 = pool.encode(string2);
094    assertNotEquals(id1, id2);
095    assertEquals(2, pool.size());
096  }
097
098  @Test(expected = IllegalArgumentException.class)
099  public void testEncodeNullStringThrowsException() {
100    pool.encode(null);
101  }
102
103  @Test
104  public void testDecodeNonExistentIdReturnsNull() {
105    String decoded = pool.decode(999999);
106    assertNull(decoded);
107  }
108
109  @Test
110  public void testContainsWithId() {
111    String testString = "/hbase/file.hfile";
112    int id = pool.encode(testString);
113    assertTrue(pool.contains(id));
114    assertFalse(pool.contains(id + 1));
115  }
116
117  @Test
118  public void testContainsWithString() {
119    String testString = "/hbase/file.hfile";
120    pool.encode(testString);
121    assertTrue(pool.contains(testString));
122    assertFalse(pool.contains("/hbase/other-file.hfile"));
123  }
124
125  @Test
126  public void testRemoveExistingString() {
127    String testString = "/hbase/file.hfile";
128    int id = pool.encode(testString);
129    assertEquals(1, pool.size());
130    assertTrue(pool.contains(testString));
131    boolean removed = pool.remove(testString);
132    assertTrue(removed);
133    assertEquals(0, pool.size());
134    assertFalse(pool.contains(testString));
135    assertFalse(pool.contains(id));
136    assertNull(pool.decode(id));
137  }
138
139  @Test
140  public void testRemoveNonExistentStringReturnsFalse() {
141    boolean removed = pool.remove("/non/existent/file.hfile");
142    assertFalse(removed);
143  }
144
145  @Test
146  public void testRemoveNullStringReturnsFalse() {
147    boolean removed = pool.remove(null);
148    assertFalse(removed);
149  }
150
151  @Test
152  public void testClear() {
153    pool.encode("/file1.hfile");
154    pool.encode("/file2.hfile");
155    pool.encode("/file3.hfile");
156    assertEquals(3, pool.size());
157    pool.clear();
158    assertEquals(0, pool.size());
159  }
160
161  @Test
162  public void testSizeTracking() {
163    assertEquals(0, pool.size());
164    pool.encode("/file1.hfile");
165    assertEquals(1, pool.size());
166    pool.encode("/file2.hfile");
167    assertEquals(2, pool.size());
168    // Encoding same string should not increase size
169    pool.encode("/file1.hfile");
170    assertEquals(2, pool.size());
171    pool.remove("/file1.hfile");
172    assertEquals(1, pool.size());
173    pool.clear();
174    assertEquals(0, pool.size());
175  }
176
177  @Test
178  public void testGetPoolStats() {
179    String stats = pool.getPoolStats();
180    assertEquals("No strings encoded", stats);
181    pool.encode("/hbase/data/table1/file1.hfile");
182    pool.encode("/hbase/data/table2/file2.hfile");
183    stats = pool.getPoolStats();
184    assertNotNull(stats);
185    assertTrue(stats.contains("2 unique strings"));
186    assertTrue(stats.contains("avg length:"));
187  }
188
189  @Test
190  public void testConcurrentEncoding() throws InterruptedException {
191    int numThreads = 10;
192    int stringsPerThread = 100;
193    ExecutorService executor = Executors.newFixedThreadPool(numThreads);
194    CountDownLatch doneLatch = new CountDownLatch(numThreads);
195    ConcurrentHashMap<String, Integer> results = new ConcurrentHashMap<>();
196    AtomicInteger errorCount = new AtomicInteger(0);
197
198    for (int t = 0; t < numThreads; t++) {
199      final int threadId = t;
200      executor.submit(() -> {
201        try {
202          for (int i = 0; i < stringsPerThread; i++) {
203            String string = "/thread" + threadId + "/file" + i + ".hfile";
204            int id = pool.encode(string);
205            results.put(string, id);
206          }
207        } catch (Exception e) {
208          errorCount.incrementAndGet();
209        } finally {
210          doneLatch.countDown();
211        }
212      });
213    }
214
215    assertTrue(doneLatch.await(30, TimeUnit.SECONDS));
216    executor.shutdown();
217
218    assertEquals(0, errorCount.get());
219    assertEquals(numThreads * stringsPerThread, pool.size());
220    assertEquals(numThreads * stringsPerThread, results.size());
221
222    // Verify all strings can be decoded correctly
223    for (Map.Entry<String, Integer> entry : results.entrySet()) {
224      String decoded = pool.decode(entry.getValue());
225      assertEquals(entry.getKey(), decoded);
226    }
227  }
228
229  @Test
230  public void testConcurrentEncodingSameStrings() throws InterruptedException {
231    int numThreads = 20;
232    String sharedString = "/shared/file.hfile";
233    ExecutorService executor = Executors.newFixedThreadPool(numThreads);
234    CountDownLatch doneLatch = new CountDownLatch(numThreads);
235    Set<Integer> ids = ConcurrentHashMap.newKeySet();
236    AtomicInteger errorCount = new AtomicInteger(0);
237
238    for (int i = 0; i < numThreads; i++) {
239      executor.submit(() -> {
240        try {
241          int id = pool.encode(sharedString);
242          ids.add(id);
243        } catch (Exception e) {
244          errorCount.incrementAndGet();
245        } finally {
246          doneLatch.countDown();
247        }
248      });
249    }
250
251    doneLatch.await(10, TimeUnit.SECONDS);
252    executor.shutdown();
253
254    assertEquals(0, errorCount.get());
255    // All threads should get the same ID
256    assertEquals(1, ids.size());
257    assertEquals(1, pool.size());
258  }
259
260  @Test
261  public void testConcurrentRemoval() throws InterruptedException {
262    // Pre-populate with strings
263    List<String> strings = new ArrayList<>();
264    for (int i = 0; i < 100; i++) {
265      String string = "/file" + i + ".hfile";
266      strings.add(string);
267      pool.encode(string);
268    }
269    assertEquals(100, pool.size());
270
271    int numThreads = 10;
272    ExecutorService executor = Executors.newFixedThreadPool(numThreads);
273    CountDownLatch doneLatch = new CountDownLatch(numThreads);
274    AtomicInteger successfulRemovals = new AtomicInteger(0);
275
276    for (int t = 0; t < numThreads; t++) {
277      final int threadId = t;
278      executor.submit(() -> {
279        try {
280          for (int i = threadId * 10; i < (threadId + 1) * 10; i++) {
281            if (pool.remove(strings.get(i))) {
282              successfulRemovals.incrementAndGet();
283            }
284          }
285        } catch (Exception e) {
286          // Ignore
287        } finally {
288          doneLatch.countDown();
289        }
290      });
291    }
292
293    doneLatch.await(10, TimeUnit.SECONDS);
294    executor.shutdown();
295
296    assertEquals(100, successfulRemovals.get());
297    assertEquals(0, pool.size());
298  }
299
300  @Test
301  public void testBidirectionalMappingConsistency() {
302    // Verify that both mappings stay consistent
303    List<String> strings = new ArrayList<>();
304    List<Integer> ids = new ArrayList<>();
305
306    for (int i = 0; i < 50; i++) {
307      String string = "/region" + (i % 5) + "/file" + i + ".hfile";
308      strings.add(string);
309      ids.add(pool.encode(string));
310    }
311
312    // Verify forward mapping (string -> id)
313    for (int i = 0; i < strings.size(); i++) {
314      int expectedId = ids.get(i);
315      int actualId = pool.encode(strings.get(i));
316      assertEquals(expectedId, actualId);
317    }
318
319    // Verify reverse mapping (id -> string)
320    for (int i = 0; i < ids.size(); i++) {
321      String expectedString = strings.get(i);
322      String actualString = pool.decode(ids.get(i));
323      assertEquals(expectedString, actualString);
324    }
325  }
326}