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