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}