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;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.hadoop.hbase.testclassification.LargeTests;
24  import org.apache.hadoop.hbase.HColumnDescriptor;
25  import org.apache.hadoop.hbase.HConstants;
26  import org.apache.hadoop.hbase.HTableDescriptor;
27  import org.apache.hadoop.hbase.NamespaceDescriptor;
28  import org.apache.hadoop.hbase.TableName;
29  import org.apache.hadoop.hbase.client.Admin;
30  import org.apache.hadoop.hbase.client.Connection;
31  import org.apache.hadoop.hbase.client.ConnectionFactory;
32  import org.apache.hadoop.hbase.client.Get;
33  import org.apache.hadoop.hbase.client.Put;
34  import org.apache.hadoop.hbase.client.Result;
35  import org.apache.hadoop.hbase.client.ResultScanner;
36  import org.apache.hadoop.hbase.client.Scan;
37  import org.apache.hadoop.hbase.client.Table;
38  import org.apache.hadoop.hbase.util.Bytes;
39  import org.apache.hadoop.hbase.util.JVMClusterUtil;
40  import org.junit.Before;
41  import org.junit.Test;
42  import org.junit.experimental.categories.Category;
43  
44  import static org.junit.Assert.assertArrayEquals;
45  import static org.junit.Assert.fail;
46  
47  import java.util.ArrayList;
48  import java.util.List;
49  
50  @Category(LargeTests.class)
51  public class TestReplicationDroppedTables extends TestReplicationBase {
52    private static final Log LOG = LogFactory.getLog(TestReplicationDroppedTables.class);
53  
54    /**
55     * @throws java.lang.Exception
56     */
57    @Before
58    public void setUp() throws Exception {
59      // Starting and stopping replication can make us miss new logs,
60      // rolling like this makes sure the most recent one gets added to the queue
61      for ( JVMClusterUtil.RegionServerThread r :
62          utility1.getHBaseCluster().getRegionServerThreads()) {
63        utility1.getHBaseAdmin().rollWALWriter(r.getRegionServer().getServerName());
64      }
65      int rowCount = utility1.countRows(tableName);
66      utility1.deleteTableData(tableName);
67      // truncating the table will send one Delete per row to the slave cluster
68      // in an async fashion, which is why we cannot just call deleteTableData on
69      // utility2 since late writes could make it to the slave in some way.
70      // Instead, we truncate the first table and wait for all the Deletes to
71      // make it to the slave.
72      Scan scan = new Scan();
73      int lastCount = 0;
74      for (int i = 0; i < NB_RETRIES; i++) {
75        if (i==NB_RETRIES-1) {
76          fail("Waited too much time for truncate");
77        }
78        ResultScanner scanner = htable2.getScanner(scan);
79        Result[] res = scanner.next(rowCount);
80        scanner.close();
81        if (res.length != 0) {
82          if (res.length < lastCount) {
83            i--; // Don't increment timeout if we make progress
84          }
85          lastCount = res.length;
86          LOG.info("Still got " + res.length + " rows");
87          Thread.sleep(SLEEP_TIME);
88        } else {
89          break;
90        }
91      }
92    }
93  
94    @Test(timeout = 600000)
95    public void testEditsStuckBehindDroppedTable() throws Exception {
96      // Sanity check
97      // Make sure by default edits for dropped tables stall the replication queue, even when the
98      // table(s) in question have been deleted on both ends.
99      testEditsBehindDroppedTable(false, "test_dropped");
100   }
101 
102   @Test(timeout = 600000)
103   public void testEditsDroppedWithDroppedTable() throws Exception {
104     // Make sure by default edits for dropped tables are themselves dropped when the
105     // table(s) in question have been deleted on both ends.
106     testEditsBehindDroppedTable(true, "test_dropped");
107   }
108 
109   @Test(timeout = 600000)
110   public void testEditsDroppedWithDroppedTableNS() throws Exception {
111     // also try with a namespace
112     Connection connection1 = ConnectionFactory.createConnection(conf1);
113     try (Admin admin1 = connection1.getAdmin()) {
114       admin1.createNamespace(NamespaceDescriptor.create("NS").build());
115     }
116     Connection connection2 = ConnectionFactory.createConnection(conf2);
117     try (Admin admin2 = connection2.getAdmin()) {
118       admin2.createNamespace(NamespaceDescriptor.create("NS").build());
119     }
120     testEditsBehindDroppedTable(true, "NS:test_dropped");
121   }
122 
123   private void testEditsBehindDroppedTable(boolean allowProceeding, String tName) throws Exception {
124     conf1.setBoolean(HConstants.REPLICATION_DROP_ON_DELETED_TABLE_KEY, allowProceeding);
125     conf1.setInt(HConstants.REPLICATION_SOURCE_MAXTHREADS_KEY, 1);
126 
127     // make sure we have a single region server only, so that all
128     // edits for all tables go there
129     utility1.shutdownMiniHBaseCluster();
130     utility1.startMiniHBaseCluster(1, 1);
131 
132     TableName tablename = TableName.valueOf(tName);
133     byte[] familyname = Bytes.toBytes("fam");
134     byte[] row = Bytes.toBytes("row");
135 
136     HTableDescriptor table = new HTableDescriptor(tablename);
137     HColumnDescriptor fam = new HColumnDescriptor(familyname);
138     fam.setScope(HConstants.REPLICATION_SCOPE_GLOBAL);
139     table.addFamily(fam);
140 
141     Connection connection1 = ConnectionFactory.createConnection(conf1);
142     Connection connection2 = ConnectionFactory.createConnection(conf2);
143     try (Admin admin1 = connection1.getAdmin()) {
144       admin1.createTable(table);
145     }
146     try (Admin admin2 = connection2.getAdmin()) {
147       admin2.createTable(table);
148     }
149     utility1.waitUntilAllRegionsAssigned(tablename);
150     utility2.waitUntilAllRegionsAssigned(tablename);
151 
152     Table lHtable1 = utility1.getConnection().getTable(tablename);
153 
154     // now suspend replication
155     admin.disablePeer(PEER_ID);
156 
157     // put some data (lead with 0 so the edit gets sorted before the other table's edits
158     //   in the replication batch)
159     // write a bunch of edits, making sure we fill a batch
160     byte[] rowkey = Bytes.toBytes(0+" put on table to be dropped");
161     Put put = new Put(rowkey);
162     put.addColumn(familyname, row, row);
163     lHtable1.put(put);
164 
165     rowkey = Bytes.toBytes("normal put");
166     put = new Put(rowkey);
167     put.addColumn(famName, row, row);
168     htable1.put(put);
169 
170     try (Admin admin1 = connection1.getAdmin()) {
171       admin1.disableTable(tablename);
172       admin1.deleteTable(tablename);
173     }
174     try (Admin admin2 = connection2.getAdmin()) {
175       admin2.disableTable(tablename);
176       admin2.deleteTable(tablename);
177     }
178 
179     admin.enablePeer(PEER_ID);
180     if (allowProceeding) {
181       // in this we'd expect the key to make it over
182       verifyReplicationProceeded(rowkey);
183     } else {
184       verifyReplicationStuck(rowkey);
185     }
186     // just to be safe
187     conf1.setBoolean(HConstants.REPLICATION_DROP_ON_DELETED_TABLE_KEY, false);
188   }
189 
190   @Test(timeout = 600000)
191   public void testEditsBehindDroppedTableTiming() throws Exception {
192     conf1.setBoolean(HConstants.REPLICATION_DROP_ON_DELETED_TABLE_KEY, true);
193     conf1.setInt(HConstants.REPLICATION_SOURCE_MAXTHREADS_KEY, 1);
194 
195     // make sure we have a single region server only, so that all
196     // edits for all tables go there
197     utility1.shutdownMiniHBaseCluster();
198     utility1.startMiniHBaseCluster(1, 1);
199 
200     TableName tablename = TableName.valueOf("testdroppedtimed");
201     byte[] familyname = Bytes.toBytes("fam");
202     byte[] row = Bytes.toBytes("row");
203 
204     HTableDescriptor table = new HTableDescriptor(tablename);
205     HColumnDescriptor fam = new HColumnDescriptor(familyname);
206     fam.setScope(HConstants.REPLICATION_SCOPE_GLOBAL);
207     table.addFamily(fam);
208 
209     Connection connection1 = ConnectionFactory.createConnection(conf1);
210     Connection connection2 = ConnectionFactory.createConnection(conf2);
211     try (Admin admin1 = connection1.getAdmin()) {
212       admin1.createTable(table);
213     }
214     try (Admin admin2 = connection2.getAdmin()) {
215       admin2.createTable(table);
216     }
217     utility1.waitUntilAllRegionsAssigned(tablename);
218     utility2.waitUntilAllRegionsAssigned(tablename);
219 
220     Table lHtable1 = utility1.getConnection().getTable(tablename);
221 
222     // now suspend replication
223     admin.disablePeer(PEER_ID);
224 
225     // put some data (lead with 0 so the edit gets sorted before the other table's edits
226     //   in the replication batch)
227     // write a bunch of edits, making sure we fill a batch
228     byte[] rowkey = Bytes.toBytes(0+" put on table to be dropped");
229     Put put = new Put(rowkey);
230     put.addColumn(familyname, row, row);
231     lHtable1.put(put);
232 
233     rowkey = Bytes.toBytes("normal put");
234     put = new Put(rowkey);
235     put.addColumn(famName, row, row);
236     htable1.put(put);
237 
238     try (Admin admin2 = connection2.getAdmin()) {
239       admin2.disableTable(tablename);
240       admin2.deleteTable(tablename);
241     }
242 
243     admin.enablePeer(PEER_ID);
244     // edit should still be stuck
245 
246     try (Admin admin1 = connection1.getAdmin()) {
247       // the source table still exists, replication should be stalled
248       verifyReplicationStuck(rowkey);
249 
250       admin1.disableTable(tablename);
251       // still stuck, source table still exists
252       verifyReplicationStuck(rowkey);
253 
254       admin1.deleteTable(tablename);
255       // now the source table is gone, replication should proceed, the
256       // offending edits be dropped
257       verifyReplicationProceeded(rowkey);
258     }
259     // just to be safe
260     conf1.setBoolean(HConstants.REPLICATION_DROP_ON_DELETED_TABLE_KEY, false);
261   }
262 
263   private void verifyReplicationProceeded(byte[] rowkey) throws Exception {
264     Get get = new Get(rowkey);
265     for (int i = 0; i < NB_RETRIES; i++) {
266       if (i==NB_RETRIES-1) {
267         fail("Waited too much time for put replication");
268       }
269       Result res = htable2.get(get);
270       if (res.size() == 0) {
271         LOG.info("Row not available");
272         Thread.sleep(SLEEP_TIME);
273       } else {
274         assertArrayEquals(res.getRow(), rowkey);
275         break;
276       }
277     }
278   }
279 
280   private void verifyReplicationStuck(byte[] rowkey) throws Exception {
281     Get get = new Get(rowkey);
282     for (int i = 0; i < NB_RETRIES; i++) {
283       Result res = htable2.get(get);
284       if (res.size() >= 1) {
285         fail("Edit should have been stuck behind dropped tables");
286       } else {
287         LOG.info("Row not replicated, let's wait a bit more...");
288         Thread.sleep(SLEEP_TIME);
289       }
290     }
291   }
292 }