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