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.master.procedure; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertFalse; 022import static org.junit.Assert.assertTrue; 023 024import java.io.IOException; 025import org.apache.hadoop.hbase.ConcurrentTableModificationException; 026import org.apache.hadoop.hbase.DoNotRetryIOException; 027import org.apache.hadoop.hbase.HBaseClassTestRule; 028import org.apache.hadoop.hbase.HBaseIOException; 029import org.apache.hadoop.hbase.InvalidFamilyOperationException; 030import org.apache.hadoop.hbase.TableName; 031import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; 032import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 033import org.apache.hadoop.hbase.client.CoprocessorDescriptorBuilder; 034import org.apache.hadoop.hbase.client.PerClientRandomNonceGenerator; 035import org.apache.hadoop.hbase.client.RegionInfo; 036import org.apache.hadoop.hbase.client.TableDescriptor; 037import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 038import org.apache.hadoop.hbase.constraint.ConstraintProcessor; 039import org.apache.hadoop.hbase.coprocessor.SimpleRegionObserver; 040import org.apache.hadoop.hbase.io.compress.Compression; 041import org.apache.hadoop.hbase.master.procedure.MasterProcedureTestingUtility.StepHook; 042import org.apache.hadoop.hbase.procedure2.Procedure; 043import org.apache.hadoop.hbase.procedure2.ProcedureExecutor; 044import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility; 045import org.apache.hadoop.hbase.regionserver.HRegion; 046import org.apache.hadoop.hbase.testclassification.LargeTests; 047import org.apache.hadoop.hbase.testclassification.MasterTests; 048import org.apache.hadoop.hbase.util.Bytes; 049import org.apache.hadoop.hbase.util.NonceKey; 050import org.apache.hadoop.hbase.util.TableDescriptorChecker; 051import org.junit.Assert; 052import org.junit.ClassRule; 053import org.junit.Rule; 054import org.junit.Test; 055import org.junit.experimental.categories.Category; 056import org.junit.rules.TestName; 057 058@Category({ MasterTests.class, LargeTests.class }) 059public class TestModifyTableProcedure extends TestTableDDLProcedureBase { 060 061 @ClassRule 062 public static final HBaseClassTestRule CLASS_RULE = 063 HBaseClassTestRule.forClass(TestModifyTableProcedure.class); 064 065 @Rule 066 public TestName name = new TestName(); 067 068 private static final String column_Family1 = "cf1"; 069 private static final String column_Family2 = "cf2"; 070 private static final String column_Family3 = "cf3"; 071 072 @Test 073 public void testModifyTable() throws Exception { 074 final TableName tableName = TableName.valueOf(name.getMethodName()); 075 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 076 077 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf"); 078 UTIL.getAdmin().disableTable(tableName); 079 080 // Modify the table descriptor 081 TableDescriptor htd = UTIL.getAdmin().getDescriptor(tableName); 082 083 // Test 1: Modify 1 property 084 long newMaxFileSize = htd.getMaxFileSize() * 2; 085 htd = TableDescriptorBuilder.newBuilder(htd).setMaxFileSize(newMaxFileSize) 086 .setRegionReplication(3).build(); 087 088 long procId1 = ProcedureTestingUtility.submitAndWait(procExec, 089 new ModifyTableProcedure(procExec.getEnvironment(), htd)); 090 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId1)); 091 092 TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName); 093 assertEquals(newMaxFileSize, currentHtd.getMaxFileSize()); 094 095 // Test 2: Modify multiple properties 096 boolean newReadOnlyOption = htd.isReadOnly() ? false : true; 097 long newMemStoreFlushSize = htd.getMemStoreFlushSize() * 2; 098 htd = TableDescriptorBuilder.newBuilder(htd).setReadOnly(newReadOnlyOption) 099 .setMemStoreFlushSize(newMemStoreFlushSize).build(); 100 101 long procId2 = ProcedureTestingUtility.submitAndWait(procExec, 102 new ModifyTableProcedure(procExec.getEnvironment(), htd)); 103 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId2)); 104 105 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 106 assertEquals(newReadOnlyOption, currentHtd.isReadOnly()); 107 assertEquals(newMemStoreFlushSize, currentHtd.getMemStoreFlushSize()); 108 } 109 110 @Test 111 public void testModifyTableAddCF() throws Exception { 112 final TableName tableName = TableName.valueOf(name.getMethodName()); 113 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 114 115 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf1"); 116 TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName); 117 assertEquals(1, currentHtd.getColumnFamilyNames().size()); 118 119 // Test 1: Modify the table descriptor online 120 String cf2 = "cf2"; 121 TableDescriptorBuilder tableDescriptorBuilder = 122 TableDescriptorBuilder.newBuilder(UTIL.getAdmin().getDescriptor(tableName)); 123 ColumnFamilyDescriptor columnFamilyDescriptor = 124 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf2)).build(); 125 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 126 127 long procId = ProcedureTestingUtility.submitAndWait(procExec, 128 new ModifyTableProcedure(procExec.getEnvironment(), tableDescriptorBuilder.build())); 129 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId)); 130 131 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 132 assertEquals(2, currentHtd.getColumnFamilyNames().size()); 133 assertTrue(currentHtd.hasColumnFamily(Bytes.toBytes(cf2))); 134 135 // Test 2: Modify the table descriptor offline 136 UTIL.getAdmin().disableTable(tableName); 137 ProcedureTestingUtility.waitNoProcedureRunning(procExec); 138 String cf3 = "cf3"; 139 tableDescriptorBuilder = 140 TableDescriptorBuilder.newBuilder(UTIL.getAdmin().getDescriptor(tableName)); 141 columnFamilyDescriptor = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf3)).build(); 142 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 143 144 long procId2 = ProcedureTestingUtility.submitAndWait(procExec, 145 new ModifyTableProcedure(procExec.getEnvironment(), tableDescriptorBuilder.build())); 146 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId2)); 147 148 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 149 assertTrue(currentHtd.hasColumnFamily(Bytes.toBytes(cf3))); 150 assertEquals(3, currentHtd.getColumnFamilyNames().size()); 151 } 152 153 @Test 154 public void testModifyTableDeleteCF() throws Exception { 155 final TableName tableName = TableName.valueOf(name.getMethodName()); 156 final String cf1 = "cf1"; 157 final String cf2 = "cf2"; 158 final String cf3 = "cf3"; 159 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 160 161 MasterProcedureTestingUtility.createTable(procExec, tableName, null, cf1, cf2, cf3); 162 TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName); 163 assertEquals(3, currentHtd.getColumnFamilyNames().size()); 164 165 // Test 1: Modify the table descriptor 166 TableDescriptor htd = UTIL.getAdmin().getDescriptor(tableName); 167 htd = TableDescriptorBuilder.newBuilder(htd).removeColumnFamily(Bytes.toBytes(cf2)).build(); 168 169 long procId = ProcedureTestingUtility.submitAndWait(procExec, 170 new ModifyTableProcedure(procExec.getEnvironment(), htd)); 171 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId)); 172 173 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 174 assertEquals(2, currentHtd.getColumnFamilyNames().size()); 175 assertFalse(currentHtd.hasColumnFamily(Bytes.toBytes(cf2))); 176 177 // Test 2: Modify the table descriptor offline 178 UTIL.getAdmin().disableTable(tableName); 179 ProcedureTestingUtility.waitNoProcedureRunning(procExec); 180 181 TableDescriptor htd2 = UTIL.getAdmin().getDescriptor(tableName); 182 // Disable Sanity check 183 htd2 = TableDescriptorBuilder.newBuilder(htd2).removeColumnFamily(Bytes.toBytes(cf3)) 184 .setValue(TableDescriptorChecker.TABLE_SANITY_CHECKS, Boolean.FALSE.toString()).build(); 185 186 long procId2 = ProcedureTestingUtility.submitAndWait(procExec, 187 new ModifyTableProcedure(procExec.getEnvironment(), htd2)); 188 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId2)); 189 190 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 191 assertEquals(1, currentHtd.getColumnFamilyNames().size()); 192 assertFalse(currentHtd.hasColumnFamily(Bytes.toBytes(cf3))); 193 194 // Removing the last family will fail 195 TableDescriptor htd3 = UTIL.getAdmin().getDescriptor(tableName); 196 htd3 = TableDescriptorBuilder.newBuilder(htd3).removeColumnFamily(Bytes.toBytes(cf1)).build(); 197 long procId3 = ProcedureTestingUtility.submitAndWait(procExec, 198 new ModifyTableProcedure(procExec.getEnvironment(), htd3)); 199 final Procedure<?> result = procExec.getResult(procId3); 200 assertEquals(true, result.isFailed()); 201 Throwable cause = ProcedureTestingUtility.getExceptionCause(result); 202 assertTrue("expected DoNotRetryIOException, got " + cause, 203 cause instanceof DoNotRetryIOException); 204 assertEquals(1, currentHtd.getColumnFamilyNames().size()); 205 assertTrue(currentHtd.hasColumnFamily(Bytes.toBytes(cf1))); 206 } 207 208 @Test 209 public void testRecoveryAndDoubleExecutionOffline() throws Exception { 210 final TableName tableName = TableName.valueOf(name.getMethodName()); 211 final String cf2 = "cf2"; 212 final String cf3 = "cf3"; 213 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 214 215 // create the table 216 RegionInfo[] regions = 217 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf1", cf3); 218 UTIL.getAdmin().disableTable(tableName); 219 220 ProcedureTestingUtility.waitNoProcedureRunning(procExec); 221 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true); 222 223 // Modify multiple properties of the table. 224 TableDescriptor oldDescriptor = UTIL.getAdmin().getDescriptor(tableName); 225 TableDescriptor newDescriptor = TableDescriptorBuilder.newBuilder(oldDescriptor) 226 .setCompactionEnabled(!oldDescriptor.isCompactionEnabled()) 227 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(cf2)).removeColumnFamily(Bytes.toBytes(cf3)) 228 .setRegionReplication(3).build(); 229 230 // Start the Modify procedure && kill the executor 231 long procId = 232 procExec.submitProcedure(new ModifyTableProcedure(procExec.getEnvironment(), newDescriptor)); 233 234 // Restart the executor and execute the step twice 235 MasterProcedureTestingUtility.testRecoveryAndDoubleExecution(procExec, procId); 236 237 // Validate descriptor 238 TableDescriptor currentDescriptor = UTIL.getAdmin().getDescriptor(tableName); 239 assertEquals(newDescriptor.isCompactionEnabled(), currentDescriptor.isCompactionEnabled()); 240 assertEquals(2, newDescriptor.getColumnFamilyNames().size()); 241 242 // cf2 should be added cf3 should be removed 243 MasterProcedureTestingUtility.validateTableCreation(UTIL.getHBaseCluster().getMaster(), 244 tableName, regions, false, "cf1", cf2); 245 } 246 247 @Test 248 public void testRecoveryAndDoubleExecutionOnline() throws Exception { 249 final TableName tableName = TableName.valueOf(name.getMethodName()); 250 final String cf2 = "cf2"; 251 final String cf3 = "cf3"; 252 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 253 254 // create the table 255 RegionInfo[] regions = 256 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf1", cf3); 257 258 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true); 259 260 // Modify multiple properties of the table. 261 TableDescriptorBuilder tableDescriptorBuilder = 262 TableDescriptorBuilder.newBuilder(UTIL.getAdmin().getDescriptor(tableName)); 263 ColumnFamilyDescriptor columnFamilyDescriptor = 264 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf2)).build(); 265 boolean newCompactionEnableOption = !tableDescriptorBuilder.build().isCompactionEnabled(); 266 tableDescriptorBuilder.setCompactionEnabled(newCompactionEnableOption); 267 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 268 tableDescriptorBuilder.removeColumnFamily(Bytes.toBytes(cf3)); 269 270 // Start the Modify procedure && kill the executor 271 long procId = procExec.submitProcedure( 272 new ModifyTableProcedure(procExec.getEnvironment(), tableDescriptorBuilder.build())); 273 274 // Restart the executor and execute the step twice 275 MasterProcedureTestingUtility.testRecoveryAndDoubleExecution(procExec, procId); 276 277 // Validate descriptor 278 TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName); 279 assertEquals(newCompactionEnableOption, currentHtd.isCompactionEnabled()); 280 assertEquals(2, currentHtd.getColumnFamilyNames().size()); 281 assertTrue(currentHtd.hasColumnFamily(Bytes.toBytes(cf2))); 282 assertFalse(currentHtd.hasColumnFamily(Bytes.toBytes(cf3))); 283 284 // cf2 should be added cf3 should be removed 285 MasterProcedureTestingUtility.validateTableCreation(UTIL.getHBaseCluster().getMaster(), 286 tableName, regions, "cf1", cf2); 287 } 288 289 @Test 290 public void testColumnFamilyAdditionTwiceWithNonce() throws Exception { 291 final TableName tableName = TableName.valueOf(name.getMethodName()); 292 final String cf2 = "cf2"; 293 final String cf3 = "cf3"; 294 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 295 296 // create the table 297 RegionInfo[] regions = 298 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf1", cf3); 299 300 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true); 301 // Modify multiple properties of the table. 302 TableDescriptor td = UTIL.getAdmin().getDescriptor(tableName); 303 TableDescriptor newTd = 304 TableDescriptorBuilder.newBuilder(td).setCompactionEnabled(!td.isCompactionEnabled()) 305 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(cf2)).build(); 306 307 PerClientRandomNonceGenerator nonceGenerator = PerClientRandomNonceGenerator.get(); 308 long nonceGroup = nonceGenerator.getNonceGroup(); 309 long newNonce = nonceGenerator.newNonce(); 310 NonceKey nonceKey = new NonceKey(nonceGroup, newNonce); 311 procExec.registerNonce(nonceKey); 312 313 // Start the Modify procedure && kill the executor 314 final long procId = procExec 315 .submitProcedure(new ModifyTableProcedure(procExec.getEnvironment(), newTd), nonceKey); 316 317 // Restart the executor after MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR and try to add column family 318 // as nonce are there , we should not fail 319 MasterProcedureTestingUtility.testRecoveryAndDoubleExecution(procExec, procId, new StepHook() { 320 @Override 321 public boolean execute(int step) throws IOException { 322 if (step == 3) { 323 return procId == UTIL.getHBaseCluster().getMaster().addColumn(tableName, 324 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf2)).build(), nonceGroup, 325 newNonce); 326 } 327 return true; 328 } 329 }); 330 331 // Try with different nonce, now it should fail the checks 332 try { 333 UTIL.getHBaseCluster().getMaster().addColumn(tableName, 334 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf2)).build(), nonceGroup, 335 nonceGenerator.newNonce()); 336 Assert.fail(); 337 } catch (InvalidFamilyOperationException e) { 338 } 339 340 // Validate descriptor 341 TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName); 342 assertEquals(!td.isCompactionEnabled(), currentHtd.isCompactionEnabled()); 343 assertEquals(3, currentHtd.getColumnFamilyCount()); 344 assertTrue(currentHtd.hasColumnFamily(Bytes.toBytes(cf2))); 345 assertTrue(currentHtd.hasColumnFamily(Bytes.toBytes(cf3))); 346 347 // cf2 should be added 348 MasterProcedureTestingUtility.validateTableCreation(UTIL.getHBaseCluster().getMaster(), 349 tableName, regions, "cf1", cf2, cf3); 350 } 351 352 @Test 353 public void testRollbackAndDoubleExecutionOnline() throws Exception { 354 final TableName tableName = TableName.valueOf(name.getMethodName()); 355 final String familyName = "cf2"; 356 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 357 358 // create the table 359 RegionInfo[] regions = 360 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf1"); 361 362 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true); 363 364 TableDescriptor td = UTIL.getAdmin().getDescriptor(tableName); 365 TableDescriptor newTd = 366 TableDescriptorBuilder.newBuilder(td).setCompactionEnabled(!td.isCompactionEnabled()) 367 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(familyName)).build(); 368 369 // Start the Modify procedure && kill the executor 370 long procId = 371 procExec.submitProcedure(new ModifyTableProcedure(procExec.getEnvironment(), newTd)); 372 373 int lastStep = 8; // failing before MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR 374 MasterProcedureTestingUtility.testRollbackAndDoubleExecution(procExec, procId, lastStep); 375 376 // cf2 should not be present 377 MasterProcedureTestingUtility.validateTableCreation(UTIL.getHBaseCluster().getMaster(), 378 tableName, regions, "cf1"); 379 } 380 381 @Test 382 public void testRollbackAndDoubleExecutionOffline() throws Exception { 383 final TableName tableName = TableName.valueOf(name.getMethodName()); 384 final String familyName = "cf2"; 385 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 386 387 // create the table 388 RegionInfo[] regions = 389 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf1"); 390 UTIL.getAdmin().disableTable(tableName); 391 392 ProcedureTestingUtility.waitNoProcedureRunning(procExec); 393 ProcedureTestingUtility.setKillAndToggleBeforeStoreUpdate(procExec, true); 394 395 TableDescriptor td = UTIL.getAdmin().getDescriptor(tableName); 396 TableDescriptor newTd = 397 TableDescriptorBuilder.newBuilder(td).setCompactionEnabled(!td.isCompactionEnabled()) 398 .setColumnFamily(ColumnFamilyDescriptorBuilder.of(familyName)).setRegionReplication(3) 399 .build(); 400 401 // Start the Modify procedure && kill the executor 402 long procId = 403 procExec.submitProcedure(new ModifyTableProcedure(procExec.getEnvironment(), newTd)); 404 405 // Restart the executor and rollback the step twice 406 int lastStep = 8; // failing before MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR 407 MasterProcedureTestingUtility.testRollbackAndDoubleExecution(procExec, procId, lastStep); 408 409 // cf2 should not be present 410 MasterProcedureTestingUtility.validateTableCreation(UTIL.getHBaseCluster().getMaster(), 411 tableName, regions, "cf1"); 412 } 413 414 @Test 415 public void testConcurrentAddColumnFamily() throws IOException, InterruptedException { 416 final TableName tableName = TableName.valueOf(name.getMethodName()); 417 UTIL.createTable(tableName, column_Family1); 418 419 class ConcurrentAddColumnFamily extends Thread { 420 TableName tableName = null; 421 ColumnFamilyDescriptor columnFamilyDescriptor; 422 boolean exception; 423 424 public ConcurrentAddColumnFamily(TableName tableName, 425 ColumnFamilyDescriptor columnFamilyDescriptor) { 426 this.tableName = tableName; 427 this.columnFamilyDescriptor = columnFamilyDescriptor; 428 this.exception = false; 429 } 430 431 public void run() { 432 try { 433 UTIL.getAdmin().addColumnFamily(tableName, columnFamilyDescriptor); 434 } catch (Exception e) { 435 if (e.getClass().equals(ConcurrentTableModificationException.class)) { 436 this.exception = true; 437 } 438 } 439 } 440 } 441 ColumnFamilyDescriptor columnFamilyDescriptor = 442 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(column_Family2)).build(); 443 ConcurrentAddColumnFamily t1 = new ConcurrentAddColumnFamily(tableName, columnFamilyDescriptor); 444 columnFamilyDescriptor = 445 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(column_Family3)).build(); 446 ConcurrentAddColumnFamily t2 = new ConcurrentAddColumnFamily(tableName, columnFamilyDescriptor); 447 448 t1.start(); 449 t2.start(); 450 451 t1.join(); 452 t2.join(); 453 int noOfColumnFamilies = UTIL.getAdmin().getDescriptor(tableName).getColumnFamilies().length; 454 assertTrue("Expected ConcurrentTableModificationException.", 455 ((t1.exception || t2.exception) && noOfColumnFamilies == 2) || noOfColumnFamilies == 3); 456 } 457 458 @Test 459 public void testConcurrentDeleteColumnFamily() throws IOException, InterruptedException { 460 final TableName tableName = TableName.valueOf(name.getMethodName()); 461 TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tableName); 462 ColumnFamilyDescriptor columnFamilyDescriptor = 463 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(column_Family1)).build(); 464 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 465 columnFamilyDescriptor = 466 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(column_Family2)).build(); 467 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 468 columnFamilyDescriptor = 469 ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(column_Family3)).build(); 470 tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); 471 UTIL.getAdmin().createTable(tableDescriptorBuilder.build()); 472 473 class ConcurrentCreateDeleteTable extends Thread { 474 TableName tableName = null; 475 String columnFamily = null; 476 boolean exception; 477 478 public ConcurrentCreateDeleteTable(TableName tableName, String columnFamily) { 479 this.tableName = tableName; 480 this.columnFamily = columnFamily; 481 this.exception = false; 482 } 483 484 public void run() { 485 try { 486 UTIL.getAdmin().deleteColumnFamily(tableName, columnFamily.getBytes()); 487 } catch (Exception e) { 488 if (e.getClass().equals(ConcurrentTableModificationException.class)) { 489 this.exception = true; 490 } 491 } 492 } 493 } 494 ConcurrentCreateDeleteTable t1 = new ConcurrentCreateDeleteTable(tableName, column_Family2); 495 ConcurrentCreateDeleteTable t2 = new ConcurrentCreateDeleteTable(tableName, column_Family3); 496 497 t1.start(); 498 t2.start(); 499 500 t1.join(); 501 t2.join(); 502 int noOfColumnFamilies = UTIL.getAdmin().getDescriptor(tableName).getColumnFamilies().length; 503 assertTrue("Expected ConcurrentTableModificationException.", 504 ((t1.exception || t2.exception) && noOfColumnFamilies == 2) || noOfColumnFamilies == 1); 505 } 506 507 @Test 508 public void testConcurrentModifyColumnFamily() throws IOException, InterruptedException { 509 final TableName tableName = TableName.valueOf(name.getMethodName()); 510 UTIL.createTable(tableName, column_Family1); 511 512 class ConcurrentModifyColumnFamily extends Thread { 513 TableName tableName = null; 514 ColumnFamilyDescriptor hcd = null; 515 boolean exception; 516 517 public ConcurrentModifyColumnFamily(TableName tableName, ColumnFamilyDescriptor hcd) { 518 this.tableName = tableName; 519 this.hcd = hcd; 520 this.exception = false; 521 } 522 523 public void run() { 524 try { 525 UTIL.getAdmin().modifyColumnFamily(tableName, hcd); 526 } catch (Exception e) { 527 if (e.getClass().equals(ConcurrentTableModificationException.class)) { 528 this.exception = true; 529 } 530 } 531 } 532 } 533 ColumnFamilyDescriptor modColumnFamily1 = 534 ColumnFamilyDescriptorBuilder.newBuilder(column_Family1.getBytes()).setMaxVersions(5).build(); 535 ColumnFamilyDescriptor modColumnFamily2 = 536 ColumnFamilyDescriptorBuilder.newBuilder(column_Family1.getBytes()).setMaxVersions(6).build(); 537 538 ConcurrentModifyColumnFamily t1 = new ConcurrentModifyColumnFamily(tableName, modColumnFamily1); 539 ConcurrentModifyColumnFamily t2 = new ConcurrentModifyColumnFamily(tableName, modColumnFamily2); 540 541 t1.start(); 542 t2.start(); 543 544 t1.join(); 545 t2.join(); 546 547 int maxVersions = UTIL.getAdmin().getDescriptor(tableName) 548 .getColumnFamily(column_Family1.getBytes()).getMaxVersions(); 549 assertTrue("Expected ConcurrentTableModificationException.", (t1.exception && maxVersions == 5) 550 || (t2.exception && maxVersions == 6) || !(t1.exception && t2.exception)); 551 } 552 553 @Test 554 public void testConcurrentModifyTable() throws IOException, InterruptedException { 555 final TableName tableName = TableName.valueOf(name.getMethodName()); 556 UTIL.createTable(tableName, column_Family1); 557 558 class ConcurrentModifyTable extends Thread { 559 TableName tableName = null; 560 TableDescriptor htd = null; 561 boolean exception; 562 563 public ConcurrentModifyTable(TableName tableName, TableDescriptor htd) { 564 this.tableName = tableName; 565 this.htd = htd; 566 this.exception = false; 567 } 568 569 public void run() { 570 try { 571 UTIL.getAdmin().modifyTable(htd); 572 } catch (Exception e) { 573 if (e.getClass().equals(ConcurrentTableModificationException.class)) { 574 this.exception = true; 575 } 576 } 577 } 578 } 579 TableDescriptor htd = UTIL.getAdmin().getDescriptor(tableName); 580 TableDescriptor modifiedDescriptor = 581 TableDescriptorBuilder.newBuilder(htd).setCompactionEnabled(false).build(); 582 583 ConcurrentModifyTable t1 = new ConcurrentModifyTable(tableName, modifiedDescriptor); 584 ConcurrentModifyTable t2 = new ConcurrentModifyTable(tableName, modifiedDescriptor); 585 586 t1.start(); 587 t2.start(); 588 589 t1.join(); 590 t2.join(); 591 assertFalse("Expected ConcurrentTableModificationException.", (t1.exception || t2.exception)); 592 } 593 594 @Test 595 public void testModifyWillNotReopenRegions() throws IOException { 596 final boolean reopenRegions = false; 597 final TableName tableName = TableName.valueOf(name.getMethodName()); 598 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 599 600 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf"); 601 602 // Test 1: Modify table without reopening any regions 603 TableDescriptor htd = UTIL.getAdmin().getDescriptor(tableName); 604 TableDescriptor modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd) 605 .setValue("test" + ".hbase.conf", "test.hbase.conf.value").build(); 606 long procId1 = ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 607 procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions)); 608 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId1)); 609 TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName); 610 assertEquals("test.hbase.conf.value", currentHtd.getValue("test.hbase.conf")); 611 // Regions should not aware of any changes. 612 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 613 Assert.assertNull(r.getTableDescriptor().getValue("test.hbase.conf")); 614 } 615 // Force regions to reopen 616 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 617 getMaster().getAssignmentManager().move(r.getRegionInfo()); 618 } 619 // After the regions reopen, ensure that the configuration is updated. 620 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 621 assertEquals("test.hbase.conf.value", r.getTableDescriptor().getValue("test.hbase.conf")); 622 } 623 624 // Test 2: Modifying region replication is not allowed 625 htd = UTIL.getAdmin().getDescriptor(tableName); 626 long oldRegionReplication = htd.getRegionReplication(); 627 modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd).setRegionReplication(3).build(); 628 try { 629 ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 630 procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions)); 631 Assert.fail( 632 "An exception should have been thrown while modifying region replication properties."); 633 } catch (HBaseIOException e) { 634 assertTrue(e.getMessage().contains("Can not modify")); 635 } 636 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 637 // Nothing changed 638 assertEquals(oldRegionReplication, currentHtd.getRegionReplication()); 639 640 // Test 3: Adding CFs is not allowed 641 htd = UTIL.getAdmin().getDescriptor(tableName); 642 modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd) 643 .setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder("NewCF".getBytes()).build()) 644 .build(); 645 try { 646 ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 647 procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions)); 648 Assert.fail("Should have thrown an exception while modifying CF!"); 649 } catch (HBaseIOException e) { 650 assertTrue(e.getMessage().contains("Cannot add or remove column families")); 651 } 652 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 653 Assert.assertNull(currentHtd.getColumnFamily("NewCF".getBytes())); 654 655 // Test 4: Modifying CF property is allowed 656 htd = UTIL.getAdmin().getDescriptor(tableName); 657 modifiedDescriptor = 658 TableDescriptorBuilder 659 .newBuilder(htd).modifyColumnFamily(ColumnFamilyDescriptorBuilder 660 .newBuilder("cf".getBytes()).setCompressionType(Compression.Algorithm.SNAPPY).build()) 661 .build(); 662 ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 663 procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions)); 664 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 665 Assert.assertEquals(Compression.Algorithm.NONE, 666 r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType()); 667 } 668 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 669 getMaster().getAssignmentManager().move(r.getRegionInfo()); 670 } 671 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 672 Assert.assertEquals(Compression.Algorithm.SNAPPY, 673 r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType()); 674 } 675 676 // Test 5: Modifying coprocessor is not allowed 677 htd = UTIL.getAdmin().getDescriptor(tableName); 678 modifiedDescriptor = 679 TableDescriptorBuilder.newBuilder(htd).setCoprocessor(CoprocessorDescriptorBuilder 680 .newBuilder("any.coprocessor.name").setJarPath("fake/path").build()).build(); 681 try { 682 ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 683 procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions)); 684 Assert.fail("Should have thrown an exception while modifying coprocessor!"); 685 } catch (HBaseIOException e) { 686 assertTrue(e.getMessage().contains("Can not modify Coprocessor")); 687 } 688 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 689 assertEquals(0, currentHtd.getCoprocessorDescriptors().size()); 690 691 // Test 6: Modifying is not allowed 692 htd = UTIL.getAdmin().getDescriptor(tableName); 693 modifiedDescriptor = TableDescriptorBuilder.newBuilder(htd).setRegionReplication(3).build(); 694 try { 695 ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 696 procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions)); 697 Assert.fail("Should have thrown an exception while modifying coprocessor!"); 698 } catch (HBaseIOException e) { 699 System.out.println(e.getMessage()); 700 assertTrue(e.getMessage().contains("Can not modify REGION_REPLICATION")); 701 } 702 } 703 704 @Test 705 public void testModifyTableWithCoprocessorAndColumnFamilyPropertyChange() throws IOException { 706 // HBASE-29706 - This test validates the fix for the bug where modifying only column family 707 // properties 708 // (like COMPRESSION) with REOPEN_REGIONS=false would incorrectly throw an error when 709 // coprocessors are present. The bug was caused by comparing collection hash codes 710 // instead of actual descriptor content, which failed when HashMap iteration order varied. 711 712 final boolean reopenRegions = false; 713 final TableName tableName = TableName.valueOf(name.getMethodName()); 714 final ProcedureExecutor<MasterProcedureEnv> procExec = getMasterProcedureExecutor(); 715 716 MasterProcedureTestingUtility.createTable(procExec, tableName, null, "cf"); 717 718 // Step 1: Add coprocessors to the table 719 TableDescriptor htd = UTIL.getAdmin().getDescriptor(tableName); 720 final String cp2 = ConstraintProcessor.class.getName(); 721 TableDescriptor descriptorWithCoprocessor = TableDescriptorBuilder.newBuilder(htd) 722 .setCoprocessor(CoprocessorDescriptorBuilder.newBuilder(SimpleRegionObserver.class.getName()) 723 .setPriority(100).build()) 724 .setCoprocessor(CoprocessorDescriptorBuilder.newBuilder(cp2).setPriority(200).build()) 725 .build(); 726 long procId = ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 727 procExec.getEnvironment(), descriptorWithCoprocessor, null, htd, false, true)); 728 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId)); 729 730 // Verify coprocessors were added 731 TableDescriptor currentHtd = UTIL.getAdmin().getDescriptor(tableName); 732 assertEquals(2, currentHtd.getCoprocessorDescriptors().size()); 733 assertTrue("First coprocessor should be present", 734 currentHtd.hasCoprocessor(SimpleRegionObserver.class.getName())); 735 assertTrue("Second coprocessor should be present", currentHtd.hasCoprocessor(cp2)); 736 737 // Step 2: Modify only the column family property (COMPRESSION) with REOPEN_REGIONS=false 738 // This should SUCCEED because we're not actually modifying the coprocessor, 739 // just the column family compression setting. 740 htd = UTIL.getAdmin().getDescriptor(tableName); 741 TableDescriptor modifiedDescriptor = 742 TableDescriptorBuilder 743 .newBuilder(htd).modifyColumnFamily(ColumnFamilyDescriptorBuilder 744 .newBuilder("cf".getBytes()).setCompressionType(Compression.Algorithm.SNAPPY).build()) 745 .build(); 746 747 // This should NOT throw an error - the fix ensures order-independent coprocessor comparison 748 long procId2 = ProcedureTestingUtility.submitAndWait(procExec, new ModifyTableProcedure( 749 procExec.getEnvironment(), modifiedDescriptor, null, htd, false, reopenRegions)); 750 ProcedureTestingUtility.assertProcNotFailed(procExec.getResult(procId2)); 751 752 // Verify the modification succeeded 753 currentHtd = UTIL.getAdmin().getDescriptor(tableName); 754 assertEquals("Coprocessors should still be present", 2, 755 currentHtd.getCoprocessorDescriptors().size()); 756 assertTrue("First coprocessor should still be present", 757 currentHtd.hasCoprocessor(SimpleRegionObserver.class.getName())); 758 assertTrue("Second coprocessor should still be present", currentHtd.hasCoprocessor(cp2)); 759 assertEquals("Compression should be updated in table descriptor", Compression.Algorithm.SNAPPY, 760 currentHtd.getColumnFamily("cf".getBytes()).getCompressionType()); 761 762 // Verify regions haven't picked up the change yet (since reopenRegions=false) 763 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 764 assertEquals("Regions should still have old compression", Compression.Algorithm.NONE, 765 r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType()); 766 } 767 768 // Force regions to reopen 769 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 770 getMaster().getAssignmentManager().move(r.getRegionInfo()); 771 } 772 773 // After reopen, regions should have the new compression setting 774 for (HRegion r : UTIL.getHBaseCluster().getRegions(tableName)) { 775 assertEquals("Regions should now have new compression after reopen", 776 Compression.Algorithm.SNAPPY, 777 r.getTableDescriptor().getColumnFamily("cf".getBytes()).getCompressionType()); 778 } 779 } 780}