View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase.master.procedure;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.security.PrivilegedExceptionAction;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Set;
28  import java.util.concurrent.atomic.AtomicBoolean;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.hadoop.hbase.DoNotRetryIOException;
33  import org.apache.hadoop.hbase.HConstants;
34  import org.apache.hadoop.hbase.HRegionInfo;
35  import org.apache.hadoop.hbase.HTableDescriptor;
36  import org.apache.hadoop.hbase.MetaTableAccessor;
37  import org.apache.hadoop.hbase.TableName;
38  import org.apache.hadoop.hbase.TableNotDisabledException;
39  import org.apache.hadoop.hbase.TableNotFoundException;
40  import org.apache.hadoop.hbase.classification.InterfaceAudience;
41  import org.apache.hadoop.hbase.client.Connection;
42  import org.apache.hadoop.hbase.client.Result;
43  import org.apache.hadoop.hbase.client.ResultScanner;
44  import org.apache.hadoop.hbase.client.Scan;
45  import org.apache.hadoop.hbase.client.Table;
46  import org.apache.hadoop.hbase.executor.EventType;
47  import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
48  import org.apache.hadoop.hbase.procedure2.StateMachineProcedure;
49  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos;
50  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos.ModifyTableState;
51  import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos;
52  import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil;
53  import org.apache.hadoop.security.UserGroupInformation;
54  
55  @InterfaceAudience.Private
56  public class ModifyTableProcedure
57      extends StateMachineProcedure<MasterProcedureEnv, ModifyTableState>
58      implements TableProcedureInterface {
59    private static final Log LOG = LogFactory.getLog(ModifyTableProcedure.class);
60  
61    private final AtomicBoolean aborted = new AtomicBoolean(false);
62  
63    private HTableDescriptor unmodifiedHTableDescriptor = null;
64    private HTableDescriptor modifiedHTableDescriptor;
65    private UserGroupInformation user;
66    private boolean deleteColumnFamilyInModify;
67  
68    private List<HRegionInfo> regionInfoList;
69    private Boolean traceEnabled = null;
70  
71    public ModifyTableProcedure() {
72      initilize();
73    }
74  
75    public ModifyTableProcedure(
76      final MasterProcedureEnv env,
77      final HTableDescriptor htd) throws IOException {
78      initilize();
79      this.modifiedHTableDescriptor = htd;
80      this.user = env.getRequestUser().getUGI();
81      this.setOwner(this.user.getShortUserName());
82    }
83  
84    private void initilize() {
85      this.unmodifiedHTableDescriptor = null;
86      this.regionInfoList = null;
87      this.traceEnabled = null;
88      this.deleteColumnFamilyInModify = false;
89    }
90  
91    @Override
92    protected Flow executeFromState(final MasterProcedureEnv env, final ModifyTableState state)
93        throws InterruptedException {
94      if (isTraceEnabled()) {
95        LOG.trace(this + " execute state=" + state);
96      }
97  
98      try {
99        switch (state) {
100       case MODIFY_TABLE_PREPARE:
101         prepareModify(env);
102         setNextState(ModifyTableState.MODIFY_TABLE_PRE_OPERATION);
103         break;
104       case MODIFY_TABLE_PRE_OPERATION:
105         preModify(env, state);
106         setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR);
107         break;
108       case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR:
109         updateTableDescriptor(env);
110         setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN);
111         break;
112       case MODIFY_TABLE_REMOVE_REPLICA_COLUMN:
113         updateReplicaColumnsIfNeeded(env, unmodifiedHTableDescriptor, modifiedHTableDescriptor);
114         if (deleteColumnFamilyInModify) {
115           setNextState(ModifyTableState.MODIFY_TABLE_DELETE_FS_LAYOUT);
116         } else {
117           setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
118         }
119         break;
120       case MODIFY_TABLE_DELETE_FS_LAYOUT:
121         deleteFromFs(env, unmodifiedHTableDescriptor, modifiedHTableDescriptor);
122         setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
123         break;
124       case MODIFY_TABLE_POST_OPERATION:
125         postModify(env, state);
126         setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS);
127         break;
128       case MODIFY_TABLE_REOPEN_ALL_REGIONS:
129         reOpenAllRegionsIfTableIsOnline(env);
130         return Flow.NO_MORE_STATE;
131       default:
132         throw new UnsupportedOperationException("unhandled state=" + state);
133       }
134     } catch (IOException e) {
135       if (!isRollbackSupported(state)) {
136         // We reach a state that cannot be rolled back. We just need to keep retry.
137         LOG.warn("Error trying to modify table=" + getTableName() + " state=" + state, e);
138       } else {
139         LOG.error("Error trying to modify table=" + getTableName() + " state=" + state, e);
140         setFailure("master-modify-table", e);
141       }
142     }
143     return Flow.HAS_MORE_STATE;
144   }
145 
146   @Override
147   protected void rollbackState(final MasterProcedureEnv env, final ModifyTableState state)
148       throws IOException {
149     if (isTraceEnabled()) {
150       LOG.trace(this + " rollback state=" + state);
151     }
152     try {
153       switch (state) {
154       case MODIFY_TABLE_REOPEN_ALL_REGIONS:
155         break; // Nothing to undo.
156       case MODIFY_TABLE_POST_OPERATION:
157         // TODO-MAYBE: call the coprocessor event to un-modify?
158         break;
159       case MODIFY_TABLE_DELETE_FS_LAYOUT:
160         // Once we reach to this state - we could NOT rollback - as it is tricky to undelete
161         // the deleted files. We are not suppose to reach here, throw exception so that we know
162         // there is a code bug to investigate.
163         assert deleteColumnFamilyInModify;
164         throw new UnsupportedOperationException(this + " rollback of state=" + state
165             + " is unsupported.");
166       case MODIFY_TABLE_REMOVE_REPLICA_COLUMN:
167         // Undo the replica column update.
168         updateReplicaColumnsIfNeeded(env, modifiedHTableDescriptor, unmodifiedHTableDescriptor);
169         break;
170       case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR:
171         restoreTableDescriptor(env);
172         break;
173       case MODIFY_TABLE_PRE_OPERATION:
174         // TODO-MAYBE: call the coprocessor event to un-modify?
175         break;
176       case MODIFY_TABLE_PREPARE:
177         break; // Nothing to undo.
178       default:
179         throw new UnsupportedOperationException("unhandled state=" + state);
180       }
181     } catch (IOException e) {
182       LOG.warn("Fail trying to rollback modify table=" + getTableName() + " state=" + state, e);
183       throw e;
184     }
185   }
186 
187   @Override
188   protected ModifyTableState getState(final int stateId) {
189     return ModifyTableState.valueOf(stateId);
190   }
191 
192   @Override
193   protected int getStateId(final ModifyTableState state) {
194     return state.getNumber();
195   }
196 
197   @Override
198   protected ModifyTableState getInitialState() {
199     return ModifyTableState.MODIFY_TABLE_PREPARE;
200   }
201 
202   @Override
203   protected void setNextState(final ModifyTableState state) {
204     if (aborted.get() && isRollbackSupported(state)) {
205       setAbortFailure("modify-table", "abort requested");
206     } else {
207       super.setNextState(state);
208     }
209   }
210 
211   @Override
212   public boolean abort(final MasterProcedureEnv env) {
213     aborted.set(true);
214     return true;
215   }
216 
217   @Override
218   protected boolean acquireLock(final MasterProcedureEnv env) {
219     if (env.waitInitialized(this)) return false;
220     return env.getProcedureQueue().tryAcquireTableExclusiveLock(this, getTableName());
221   }
222 
223   @Override
224   protected void releaseLock(final MasterProcedureEnv env) {
225     env.getProcedureQueue().releaseTableExclusiveLock(this, getTableName());
226   }
227 
228   @Override
229   public void serializeStateData(final OutputStream stream) throws IOException {
230     super.serializeStateData(stream);
231 
232     MasterProcedureProtos.ModifyTableStateData.Builder modifyTableMsg =
233         MasterProcedureProtos.ModifyTableStateData.newBuilder()
234             .setUserInfo(MasterProcedureUtil.toProtoUserInfo(user))
235             .setModifiedTableSchema(modifiedHTableDescriptor.convert())
236             .setDeleteColumnFamilyInModify(deleteColumnFamilyInModify);
237 
238     if (unmodifiedHTableDescriptor != null) {
239       modifyTableMsg.setUnmodifiedTableSchema(unmodifiedHTableDescriptor.convert());
240     }
241 
242     modifyTableMsg.build().writeDelimitedTo(stream);
243   }
244 
245   @Override
246   public void deserializeStateData(final InputStream stream) throws IOException {
247     super.deserializeStateData(stream);
248 
249     MasterProcedureProtos.ModifyTableStateData modifyTableMsg =
250         MasterProcedureProtos.ModifyTableStateData.parseDelimitedFrom(stream);
251     user = MasterProcedureUtil.toUserInfo(modifyTableMsg.getUserInfo());
252     modifiedHTableDescriptor = HTableDescriptor.convert(modifyTableMsg.getModifiedTableSchema());
253     deleteColumnFamilyInModify = modifyTableMsg.getDeleteColumnFamilyInModify();
254 
255     if (modifyTableMsg.hasUnmodifiedTableSchema()) {
256       unmodifiedHTableDescriptor =
257           HTableDescriptor.convert(modifyTableMsg.getUnmodifiedTableSchema());
258     }
259   }
260 
261   @Override
262   public void toStringClassDetails(StringBuilder sb) {
263     sb.append(getClass().getSimpleName());
264     sb.append(" (table=");
265     sb.append(getTableName());
266     sb.append(")");
267   }
268 
269   @Override
270   public TableName getTableName() {
271     return modifiedHTableDescriptor.getTableName();
272   }
273 
274   @Override
275   public TableOperationType getTableOperationType() {
276     return TableOperationType.EDIT;
277   }
278 
279   /**
280    * Check conditions before any real action of modifying a table.
281    * @param env MasterProcedureEnv
282    * @throws IOException
283    */
284   private void prepareModify(final MasterProcedureEnv env) throws IOException {
285     // Checks whether the table exists
286     if (!MetaTableAccessor.tableExists(env.getMasterServices().getConnection(), getTableName())) {
287       throw new TableNotFoundException(getTableName());
288     }
289 
290     // check that we have at least 1 CF
291     if (modifiedHTableDescriptor.getColumnFamilies().length == 0) {
292       throw new DoNotRetryIOException("Table " + getTableName().toString() +
293         " should have at least one column family.");
294     }
295 
296     // In order to update the descriptor, we need to retrieve the old descriptor for comparison.
297     this.unmodifiedHTableDescriptor =
298         env.getMasterServices().getTableDescriptors().get(getTableName());
299 
300     if (env.getMasterServices().getAssignmentManager().getTableStateManager()
301         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
302       // We only execute this procedure with table online if online schema change config is set.
303       if (!MasterDDLOperationHelper.isOnlineSchemaChangeAllowed(env)) {
304         throw new TableNotDisabledException(getTableName());
305       }
306 
307       if (modifiedHTableDescriptor.getRegionReplication() != unmodifiedHTableDescriptor
308           .getRegionReplication()) {
309         throw new IOException("REGION_REPLICATION change is not supported for enabled tables");
310       }
311     }
312 
313     // Find out whether all column families in unmodifiedHTableDescriptor also exists in
314     // the modifiedHTableDescriptor. This is to determine whether we are safe to rollback.
315     final Set<byte[]> oldFamilies = unmodifiedHTableDescriptor.getFamiliesKeys();
316     final Set<byte[]> newFamilies = modifiedHTableDescriptor.getFamiliesKeys();
317     for (byte[] familyName : oldFamilies) {
318       if (!newFamilies.contains(familyName)) {
319         this.deleteColumnFamilyInModify = true;
320         break;
321       }
322     }
323   }
324 
325   /**
326    * Action before modifying table.
327    * @param env MasterProcedureEnv
328    * @param state the procedure state
329    * @throws IOException
330    * @throws InterruptedException
331    */
332   private void preModify(final MasterProcedureEnv env, final ModifyTableState state)
333       throws IOException, InterruptedException {
334     runCoprocessorAction(env, state);
335   }
336 
337   /**
338    * Update descriptor
339    * @param env MasterProcedureEnv
340    * @throws IOException
341    **/
342   private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException {
343     env.getMasterServices().getTableDescriptors().add(modifiedHTableDescriptor);
344   }
345 
346   /**
347    * Undo the descriptor change (for rollback)
348    * @param env MasterProcedureEnv
349    * @throws IOException
350    **/
351   private void restoreTableDescriptor(final MasterProcedureEnv env) throws IOException {
352     env.getMasterServices().getTableDescriptors().add(unmodifiedHTableDescriptor);
353 
354     // delete any new column families from the modifiedHTableDescriptor.
355     deleteFromFs(env, modifiedHTableDescriptor, unmodifiedHTableDescriptor);
356 
357     // Make sure regions are opened after table descriptor is updated.
358     reOpenAllRegionsIfTableIsOnline(env);
359   }
360 
361   /**
362    * Removes from hdfs the families that are not longer present in the new table descriptor.
363    * @param env MasterProcedureEnv
364    * @throws IOException
365    */
366   private void deleteFromFs(final MasterProcedureEnv env,
367       final HTableDescriptor oldHTableDescriptor, final HTableDescriptor newHTableDescriptor)
368       throws IOException {
369     final Set<byte[]> oldFamilies = oldHTableDescriptor.getFamiliesKeys();
370     final Set<byte[]> newFamilies = newHTableDescriptor.getFamiliesKeys();
371     for (byte[] familyName : oldFamilies) {
372       if (!newFamilies.contains(familyName)) {
373         MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(
374           env,
375           getTableName(),
376           getRegionInfoList(env),
377           familyName,
378           oldHTableDescriptor.getFamily(familyName).isMobEnabled());
379       }
380     }
381   }
382 
383   /**
384    * update replica column families if necessary.
385    * @param env MasterProcedureEnv
386    * @throws IOException
387    */
388   private void updateReplicaColumnsIfNeeded(
389     final MasterProcedureEnv env,
390     final HTableDescriptor oldHTableDescriptor,
391     final HTableDescriptor newHTableDescriptor) throws IOException {
392     final int oldReplicaCount = oldHTableDescriptor.getRegionReplication();
393     final int newReplicaCount = newHTableDescriptor.getRegionReplication();
394 
395     if (newReplicaCount < oldReplicaCount) {
396       Set<byte[]> tableRows = new HashSet<byte[]>();
397       Connection connection = env.getMasterServices().getConnection();
398       Scan scan = MetaTableAccessor.getScanForTableName(getTableName());
399       scan.addColumn(HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER);
400 
401       try (Table metaTable = connection.getTable(TableName.META_TABLE_NAME)) {
402         ResultScanner resScanner = metaTable.getScanner(scan);
403         for (Result result : resScanner) {
404           tableRows.add(result.getRow());
405         }
406         MetaTableAccessor.removeRegionReplicasFromMeta(
407           tableRows,
408           newReplicaCount,
409           oldReplicaCount - newReplicaCount,
410           connection);
411       }
412     }
413 
414     // Setup replication for region replicas if needed
415     if (newReplicaCount > 1 && oldReplicaCount <= 1) {
416       ServerRegionReplicaUtil.setupRegionReplicaReplication(env.getMasterConfiguration());
417     }
418   }
419 
420   /**
421    * Action after modifying table.
422    * @param env MasterProcedureEnv
423    * @param state the procedure state
424    * @throws IOException
425    * @throws InterruptedException
426    */
427   private void postModify(final MasterProcedureEnv env, final ModifyTableState state)
428       throws IOException, InterruptedException {
429     runCoprocessorAction(env, state);
430   }
431 
432   /**
433    * Last action from the procedure - executed when online schema change is supported.
434    * @param env MasterProcedureEnv
435    * @throws IOException
436    */
437   private void reOpenAllRegionsIfTableIsOnline(final MasterProcedureEnv env) throws IOException {
438     // This operation only run when the table is enabled.
439     if (!env.getMasterServices().getAssignmentManager().getTableStateManager()
440         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
441       return;
442     }
443 
444     if (MasterDDLOperationHelper.reOpenAllRegions(env, getTableName(), getRegionInfoList(env))) {
445       LOG.info("Completed modify table operation on table " + getTableName());
446     } else {
447       LOG.warn("Error on reopening the regions on table " + getTableName());
448     }
449   }
450 
451   /**
452    * The procedure could be restarted from a different machine. If the variable is null, we need to
453    * retrieve it.
454    * @return traceEnabled whether the trace is enabled
455    */
456   private Boolean isTraceEnabled() {
457     if (traceEnabled == null) {
458       traceEnabled = LOG.isTraceEnabled();
459     }
460     return traceEnabled;
461   }
462 
463   /**
464    * Coprocessor Action.
465    * @param env MasterProcedureEnv
466    * @param state the procedure state
467    * @throws IOException
468    * @throws InterruptedException
469    */
470   private void runCoprocessorAction(final MasterProcedureEnv env, final ModifyTableState state)
471       throws IOException, InterruptedException {
472     final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
473     if (cpHost != null) {
474       user.doAs(new PrivilegedExceptionAction<Void>() {
475         @Override
476         public Void run() throws Exception {
477           switch (state) {
478           case MODIFY_TABLE_PRE_OPERATION:
479             cpHost.preModifyTableHandler(getTableName(), modifiedHTableDescriptor);
480             break;
481           case MODIFY_TABLE_POST_OPERATION:
482             cpHost.postModifyTableHandler(getTableName(), modifiedHTableDescriptor);
483             break;
484           default:
485             throw new UnsupportedOperationException(this + " unhandled state=" + state);
486           }
487           return null;
488         }
489       });
490     }
491   }
492 
493   /*
494    * Check whether we are in the state that can be rollback
495    */
496   private boolean isRollbackSupported(final ModifyTableState state) {
497     if (deleteColumnFamilyInModify) {
498       switch (state) {
499       case MODIFY_TABLE_DELETE_FS_LAYOUT:
500       case MODIFY_TABLE_POST_OPERATION:
501       case MODIFY_TABLE_REOPEN_ALL_REGIONS:
502         // It is not safe to rollback if we reach to these states.
503         return false;
504       default:
505         break;
506       }
507     }
508     return true;
509   }
510 
511   private List<HRegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException {
512     if (regionInfoList == null) {
513       regionInfoList = ProcedureSyncWait.getRegionsFromMeta(env, getTableName());
514     }
515     return regionInfoList;
516   }
517 }