View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  package org.apache.hadoop.hbase.replication.regionserver;
20  
21  import static org.junit.Assert.assertEquals;
22  import static org.junit.Assert.assertFalse;
23  import static org.junit.Assert.assertNotEquals;
24  import static org.junit.Assert.assertNotNull;
25  import static org.junit.Assert.assertTrue;
26  import static org.junit.Assert.fail;
27  
28  import java.io.IOException;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.NavigableMap;
32  import java.util.NoSuchElementException;
33  import java.util.TreeMap;
34  import java.util.concurrent.PriorityBlockingQueue;
35  
36  import org.apache.hadoop.conf.Configuration;
37  import org.apache.hadoop.fs.FileSystem;
38  import org.apache.hadoop.fs.Path;
39  import org.apache.hadoop.hbase.Cell;
40  import org.apache.hadoop.hbase.HBaseTestingUtility;
41  import org.apache.hadoop.hbase.HConstants;
42  import org.apache.hadoop.hbase.HRegionInfo;
43  import org.apache.hadoop.hbase.HTableDescriptor;
44  import org.apache.hadoop.hbase.KeyValue;
45  import org.apache.hadoop.hbase.TableName;
46  import org.apache.hadoop.hbase.regionserver.MultiVersionConcurrencyControl;
47  import org.apache.hadoop.hbase.regionserver.wal.WALActionsListener;
48  import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
49  import org.apache.hadoop.hbase.replication.ReplicationQueueInfo;
50  import org.apache.hadoop.hbase.replication.WALEntryFilter;
51  import org.apache.hadoop.hbase.replication.regionserver.ReplicationSourceWALReaderThread.WALEntryBatch;
52  import org.apache.hadoop.hbase.testclassification.LargeTests;
53  import org.apache.hadoop.hbase.testclassification.ReplicationTests;
54  import org.apache.hadoop.hbase.util.Bytes;
55  import org.apache.hadoop.hbase.wal.WAL;
56  import org.apache.hadoop.hbase.wal.WAL.Entry;
57  import org.apache.hadoop.hbase.wal.WALFactory;
58  import org.apache.hadoop.hbase.wal.WALKey;
59  import org.apache.hadoop.hdfs.MiniDFSCluster;
60  import org.junit.After;
61  import org.junit.AfterClass;
62  import org.junit.Before;
63  import org.junit.BeforeClass;
64  import org.junit.Rule;
65  import org.junit.Test;
66  import org.junit.experimental.categories.Category;
67  import org.junit.rules.TestName;
68  import org.junit.runner.RunWith;
69  import org.mockito.Mockito;
70  import org.mockito.runners.MockitoJUnitRunner;
71  
72  @RunWith(MockitoJUnitRunner.class)
73  @Category({ ReplicationTests.class, LargeTests.class })
74  public class TestWALEntryStream {
75  
76    private static HBaseTestingUtility TEST_UTIL;
77    private static Configuration conf;
78    private static FileSystem fs;
79    private static MiniDFSCluster cluster;
80    private static final TableName tableName = TableName.valueOf("tablename");
81    private static final byte[] family = Bytes.toBytes("column");
82    private static final byte[] qualifier = Bytes.toBytes("qualifier");
83    private static final HRegionInfo info =
84        new HRegionInfo(tableName, HConstants.EMPTY_START_ROW, HConstants.LAST_ROW, false);
85    private static final HTableDescriptor htd = new HTableDescriptor(tableName);
86    private static NavigableMap<byte[], Integer> scopes;
87  
88    private WAL log;
89    PriorityBlockingQueue<Path> walQueue;
90    private PathWatcher pathWatcher;
91  
92    @Rule
93    public TestName tn = new TestName();
94    private final MultiVersionConcurrencyControl mvcc = new MultiVersionConcurrencyControl();
95  
96    @BeforeClass
97    public static void setUpBeforeClass() throws Exception {
98      TEST_UTIL = new HBaseTestingUtility();
99      conf = TEST_UTIL.getConfiguration();
100     TEST_UTIL.startMiniDFSCluster(3);
101 
102     cluster = TEST_UTIL.getDFSCluster();
103     fs = cluster.getFileSystem();
104     scopes = new TreeMap<byte[], Integer>(Bytes.BYTES_COMPARATOR);
105     for (byte[] fam : htd.getFamiliesKeys()) {
106       scopes.put(fam, 0);
107     }
108   }
109 
110   @AfterClass
111   public static void tearDownAfterClass() throws Exception {
112     TEST_UTIL.shutdownMiniCluster();
113   }
114 
115   @Before
116   public void setUp() throws Exception {
117     walQueue = new PriorityBlockingQueue<>();
118     List<WALActionsListener> listeners = new ArrayList<WALActionsListener>();
119     pathWatcher = new PathWatcher();
120     listeners.add(pathWatcher);
121     final WALFactory wals = new WALFactory(conf, listeners, tn.getMethodName());
122     log = wals.getWAL(info.getEncodedNameAsBytes(), info.getTable().getNamespace());
123   }
124 
125   @After
126   public void tearDown() throws Exception {
127     log.close();
128   }
129 
130   // Try out different combinations of row count and KeyValue count
131   @Test
132   public void testDifferentCounts() throws Exception {
133     int[] NB_ROWS = { 1500, 60000 };
134     int[] NB_KVS = { 1, 100 };
135     // whether compression is used
136     Boolean[] BOOL_VALS = { false, true };
137     // long lastPosition = 0;
138     for (int nbRows : NB_ROWS) {
139       for (int walEditKVs : NB_KVS) {
140         for (boolean isCompressionEnabled : BOOL_VALS) {
141           TEST_UTIL.getConfiguration().setBoolean(HConstants.ENABLE_WAL_COMPRESSION,
142             isCompressionEnabled);
143           mvcc.advanceTo(1);
144 
145           for (int i = 0; i < nbRows; i++) {
146             appendToLogPlus(walEditKVs);
147           }
148 
149           log.rollWriter();
150 
151           try (WALEntryStream entryStream =
152               new WALEntryStream(walQueue, fs, conf, new MetricsSource("1"))) {
153             int i = 0;
154             for (WAL.Entry e : entryStream) {
155               assertNotNull(e);
156               i++;
157             }
158             assertEquals(nbRows, i);
159 
160             // should've read all entries
161             assertFalse(entryStream.hasNext());
162           }
163           // reset everything for next loop
164           log.close();
165           setUp();
166         }
167       }
168     }
169   }
170 
171   /**
172    * Tests basic reading of log appends
173    */
174   @Test
175   public void testAppendsWithRolls() throws Exception {
176     appendToLog();
177 
178     long oldPos;
179     try (WALEntryStream entryStream =
180         new WALEntryStream(walQueue, fs, conf, new MetricsSource("1"))) {
181       // There's one edit in the log, read it. Reading past it needs to throw exception
182       assertTrue(entryStream.hasNext());
183       WAL.Entry entry = entryStream.next();
184       assertNotNull(entry);
185       assertFalse(entryStream.hasNext());
186       try {
187         entry = entryStream.next();
188         fail();
189       } catch (NoSuchElementException e) {
190         // expected
191       }
192       oldPos = entryStream.getPosition();
193     }
194 
195     appendToLog();
196 
197     try (WALEntryStream entryStream =
198         new WALEntryStream(walQueue, fs, conf, oldPos, new MetricsSource("1"))) {
199       // Read the newly added entry, make sure we made progress
200       WAL.Entry entry = entryStream.next();
201       assertNotEquals(oldPos, entryStream.getPosition());
202       assertNotNull(entry);
203       oldPos = entryStream.getPosition();
204     }
205 
206     // We rolled but we still should see the end of the first log and get that item
207     appendToLog();
208     log.rollWriter();
209     appendToLog();
210 
211     try (WALEntryStream entryStream =
212         new WALEntryStream(walQueue, fs, conf, oldPos, new MetricsSource("1"))) {
213       WAL.Entry entry = entryStream.next();
214       assertNotEquals(oldPos, entryStream.getPosition());
215       assertNotNull(entry);
216 
217       // next item should come from the new log
218       entry = entryStream.next();
219       assertNotEquals(oldPos, entryStream.getPosition());
220       assertNotNull(entry);
221 
222       // no more entries to read
223       assertFalse(entryStream.hasNext());
224       oldPos = entryStream.getPosition();
225     }
226   }
227 
228   /**
229    * Tests that if after a stream is opened, more entries come in and then the log is rolled, we
230    * don't mistakenly dequeue the current log thinking we're done with it
231    */
232   @Test
233   public void testLogrollWhileStreaming() throws Exception {
234     appendToLog("1");
235     appendToLog("2");// 2
236     try (WALEntryStream entryStream =
237         new WALEntryStream(walQueue, fs, conf, new MetricsSource("1"))) {
238       assertEquals("1", getRow(entryStream.next()));
239 
240       appendToLog("3"); // 3 - comes in after reader opened
241       log.rollWriter(); // log roll happening while we're reading
242       appendToLog("4"); // 4 - this append is in the rolled log
243 
244       assertEquals("2", getRow(entryStream.next()));
245       assertEquals(2, walQueue.size()); // we should not have dequeued yet since there's still an
246                                         // entry in first log
247       assertEquals("3", getRow(entryStream.next())); // if implemented improperly, this would be 4
248                                                      // and 3 would be skipped
249       assertEquals("4", getRow(entryStream.next())); // 4
250       assertEquals(1, walQueue.size()); // now we've dequeued and moved on to next log properly
251       assertFalse(entryStream.hasNext());
252     }
253   }
254 
255   /**
256    * Tests that if writes come in while we have a stream open, we shouldn't miss them
257    */
258   @Test
259   public void testNewEntriesWhileStreaming() throws Exception {
260     appendToLog("1");
261     try (WALEntryStream entryStream =
262         new WALEntryStream(walQueue, fs, conf, 0, new MetricsSource("1"))) {
263       entryStream.next(); // we've hit the end of the stream at this point
264 
265       // some new entries come in while we're streaming
266       appendToLog("2");
267       appendToLog("3");
268 
269       // don't see them
270       assertFalse(entryStream.hasNext());
271 
272       // But we do if we reset
273       entryStream.reset();
274       assertEquals("2", getRow(entryStream.next()));
275       assertEquals("3", getRow(entryStream.next()));
276       assertFalse(entryStream.hasNext());
277     }
278   }
279 
280   @Test
281   public void testResumeStreamingFromPosition() throws Exception {
282     long lastPosition = 0;
283     appendToLog("1");
284     try (WALEntryStream entryStream =
285         new WALEntryStream(walQueue, fs, conf, 0, new MetricsSource("1"))) {
286       entryStream.next(); // we've hit the end of the stream at this point
287       appendToLog("2");
288       appendToLog("3");
289       lastPosition = entryStream.getPosition();
290     }
291     // next stream should picks up where we left off
292     try (WALEntryStream entryStream =
293         new WALEntryStream(walQueue, fs, conf, lastPosition, new MetricsSource("1"))) {
294       assertEquals("2", getRow(entryStream.next()));
295       assertEquals("3", getRow(entryStream.next()));
296       assertFalse(entryStream.hasNext()); // done
297       assertEquals(1, walQueue.size());
298     }
299   }
300 
301   /**
302    * Tests that if we stop before hitting the end of a stream, we can continue where we left off
303    * using the last position
304    */
305   @Test
306   public void testPosition() throws Exception {
307     long lastPosition = 0;
308     appendEntriesToLog(3);
309     // read only one element
310     try (WALEntryStream entryStream =
311         new WALEntryStream(walQueue, fs, conf, lastPosition, new MetricsSource("1"))) {
312       entryStream.next();
313       lastPosition = entryStream.getPosition();
314     }
315     // there should still be two more entries from where we left off
316     try (WALEntryStream entryStream =
317         new WALEntryStream(walQueue, fs, conf, lastPosition, new MetricsSource("1"))) {
318       assertNotNull(entryStream.next());
319       assertNotNull(entryStream.next());
320       assertFalse(entryStream.hasNext());
321     }
322   }
323 
324 
325   @Test
326   public void testEmptyStream() throws Exception {
327     try (WALEntryStream entryStream =
328         new WALEntryStream(walQueue, fs, conf, 0, new MetricsSource("1"))) {
329       assertFalse(entryStream.hasNext());
330     }
331   }
332 
333   @Test
334   public void testReplicationSourceWALReaderThread() throws Exception {
335     appendEntriesToLog(3);
336     // get ending position
337     long position;
338     try (WALEntryStream entryStream =
339         new WALEntryStream(walQueue, fs, conf, new MetricsSource("1"))) {
340       entryStream.next();
341       entryStream.next();
342       entryStream.next();
343       position = entryStream.getPosition();
344     }
345 
346     // start up a batcher
347     ReplicationSourceManager mockSourceManager = Mockito.mock(ReplicationSourceManager.class);
348     ReplicationSourceWALReaderThread batcher = new ReplicationSourceWALReaderThread(mockSourceManager, getQueueInfo(),walQueue, 0,
349         fs, conf, getDummyFilter(), new MetricsSource("1"));
350     Path walPath = walQueue.peek();
351     batcher.start();
352     WALEntryBatch entryBatch = batcher.take();
353 
354     // should've batched up our entries
355     assertNotNull(entryBatch);
356     assertEquals(3, entryBatch.getWalEntries().size());
357     assertEquals(position, entryBatch.getLastWalPosition());
358     assertEquals(walPath, entryBatch.getLastWalPath());
359     assertEquals(3, entryBatch.getNbRowKeys());
360 
361     appendToLog("foo");
362     entryBatch = batcher.take();
363     assertEquals(1, entryBatch.getNbEntries());
364     assertEquals(getRow(entryBatch.getWalEntries().get(0)), "foo");
365   }
366 
367   private String getRow(WAL.Entry entry) {
368     Cell cell = entry.getEdit().getCells().get(0);
369     return Bytes.toString(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength());
370   }
371 
372   private void appendToLog(String key) throws IOException {
373     final long txid = log.append(htd, info,
374       new WALKey(info.getEncodedNameAsBytes(), tableName, System.currentTimeMillis(), mvcc),
375       getWALEdit(key), true);
376     log.sync(txid);
377   }
378 
379   private void appendEntriesToLog(int count) throws IOException {
380     for (int i = 0; i < count; i++) {
381       appendToLog();
382     }
383   }
384 
385   private void appendToLog() throws IOException {
386     appendToLogPlus(1);
387   }
388 
389   private void appendToLogPlus(int count) throws IOException {
390     final long txid = log.append(htd, info,
391       new WALKey(info.getEncodedNameAsBytes(), tableName, System.currentTimeMillis(), mvcc),
392       getWALEdits(count), true);
393     log.sync(txid);
394   }
395 
396   private WALEdit getWALEdits(int count) {
397     WALEdit edit = new WALEdit();
398     for (int i = 0; i < count; i++) {
399       edit.add(new KeyValue(Bytes.toBytes(System.currentTimeMillis()), family, qualifier,
400           System.currentTimeMillis(), qualifier));
401     }
402     return edit;
403   }
404 
405   private WALEdit getWALEdit(String row) {
406     WALEdit edit = new WALEdit();
407     edit.add(
408       new KeyValue(Bytes.toBytes(row), family, qualifier, System.currentTimeMillis(), qualifier));
409     return edit;
410   }
411 
412   private WALEntryFilter getDummyFilter() {
413     return new WALEntryFilter() {
414 
415       @Override
416       public Entry filter(Entry entry) {
417         return entry;
418       }
419     };
420   }
421 
422   private ReplicationQueueInfo getQueueInfo() {
423     return new ReplicationQueueInfo("1");
424   }
425 
426   class PathWatcher extends WALActionsListener.Base {
427 
428     Path currentPath;
429 
430     @Override
431     public void preLogRoll(Path oldPath, Path newPath) throws IOException {
432       walQueue.add(newPath);
433       currentPath = newPath;
434     }
435   }
436 
437 }