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}