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.List;
26  import java.util.concurrent.atomic.AtomicBoolean;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.apache.hadoop.hbase.HRegionInfo;
31  import org.apache.hadoop.hbase.HTableDescriptor;
32  import org.apache.hadoop.hbase.InvalidFamilyOperationException;
33  import org.apache.hadoop.hbase.TableName;
34  import org.apache.hadoop.hbase.classification.InterfaceAudience;
35  import org.apache.hadoop.hbase.executor.EventType;
36  import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
37  import org.apache.hadoop.hbase.procedure2.StateMachineProcedure;
38  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
39  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos;
40  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos.DeleteColumnFamilyState;
41  import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos;
42  import org.apache.hadoop.hbase.util.ByteStringer;
43  import org.apache.hadoop.hbase.util.Bytes;
44  import org.apache.hadoop.security.UserGroupInformation;
45  
46  /**
47   * The procedure to delete a column family from an existing table.
48   */
49  @InterfaceAudience.Private
50  public class DeleteColumnFamilyProcedure
51      extends StateMachineProcedure<MasterProcedureEnv, DeleteColumnFamilyState>
52      implements TableProcedureInterface {
53    private static final Log LOG = LogFactory.getLog(DeleteColumnFamilyProcedure.class);
54  
55    private final AtomicBoolean aborted = new AtomicBoolean(false);
56  
57    private HTableDescriptor unmodifiedHTableDescriptor;
58    private TableName tableName;
59    private byte [] familyName;
60    private boolean hasMob;
61    private UserGroupInformation user;
62  
63    private List<HRegionInfo> regionInfoList;
64    private Boolean traceEnabled;
65  
66    public DeleteColumnFamilyProcedure() {
67      this.unmodifiedHTableDescriptor = null;
68      this.regionInfoList = null;
69      this.traceEnabled = null;
70    }
71  
72    public DeleteColumnFamilyProcedure(
73        final MasterProcedureEnv env,
74        final TableName tableName,
75        final byte[] familyName) throws IOException {
76      this.tableName = tableName;
77      this.familyName = familyName;
78      this.user = env.getRequestUser().getUGI();
79      this.setOwner(this.user.getShortUserName());
80      this.unmodifiedHTableDescriptor = null;
81      this.regionInfoList = null;
82      this.traceEnabled = null;
83    }
84  
85    @Override
86    protected Flow executeFromState(final MasterProcedureEnv env, DeleteColumnFamilyState state) {
87      if (isTraceEnabled()) {
88        LOG.trace(this + " execute state=" + state);
89      }
90  
91      try {
92        switch (state) {
93        case DELETE_COLUMN_FAMILY_PREPARE:
94          prepareDelete(env);
95          setNextState(DeleteColumnFamilyState.DELETE_COLUMN_FAMILY_PRE_OPERATION);
96          break;
97        case DELETE_COLUMN_FAMILY_PRE_OPERATION:
98          preDelete(env, state);
99          setNextState(DeleteColumnFamilyState.DELETE_COLUMN_FAMILY_UPDATE_TABLE_DESCRIPTOR);
100         break;
101       case DELETE_COLUMN_FAMILY_UPDATE_TABLE_DESCRIPTOR:
102         updateTableDescriptor(env);
103         setNextState(DeleteColumnFamilyState.DELETE_COLUMN_FAMILY_DELETE_FS_LAYOUT);
104         break;
105       case DELETE_COLUMN_FAMILY_DELETE_FS_LAYOUT:
106         deleteFromFs(env);
107         setNextState(DeleteColumnFamilyState.DELETE_COLUMN_FAMILY_POST_OPERATION);
108         break;
109       case DELETE_COLUMN_FAMILY_POST_OPERATION:
110         postDelete(env, state);
111         setNextState(DeleteColumnFamilyState.DELETE_COLUMN_FAMILY_REOPEN_ALL_REGIONS);
112         break;
113       case DELETE_COLUMN_FAMILY_REOPEN_ALL_REGIONS:
114         reOpenAllRegionsIfTableIsOnline(env);
115         return Flow.NO_MORE_STATE;
116       default:
117         throw new UnsupportedOperationException(this + " unhandled state=" + state);
118       }
119     } catch (InterruptedException|IOException e) {
120       if (!isRollbackSupported(state)) {
121         // We reach a state that cannot be rolled back. We just need to keep retry.
122         LOG.warn("Error trying to delete the column family " + getColumnFamilyName()
123           + " from table " + tableName + "(in state=" + state + ")", e);
124       } else {
125         LOG.error("Error trying to delete the column family " + getColumnFamilyName()
126           + " from table " + tableName + "(in state=" + state + ")", e);
127         setFailure("master-delete-column-family", e);
128       }
129     }
130     return Flow.HAS_MORE_STATE;
131   }
132 
133   @Override
134   protected void rollbackState(final MasterProcedureEnv env, final DeleteColumnFamilyState state)
135       throws IOException {
136     if (isTraceEnabled()) {
137       LOG.trace(this + " rollback state=" + state);
138     }
139     try {
140       switch (state) {
141       case DELETE_COLUMN_FAMILY_REOPEN_ALL_REGIONS:
142         break; // Nothing to undo.
143       case DELETE_COLUMN_FAMILY_POST_OPERATION:
144         // TODO-MAYBE: call the coprocessor event to undo?
145         break;
146       case DELETE_COLUMN_FAMILY_DELETE_FS_LAYOUT:
147         // Once we reach to this state - we could NOT rollback - as it is tricky to undelete
148         // the deleted files. We are not suppose to reach here, throw exception so that we know
149         // there is a code bug to investigate.
150         throw new UnsupportedOperationException(this + " rollback of state=" + state
151             + " is unsupported.");
152       case DELETE_COLUMN_FAMILY_UPDATE_TABLE_DESCRIPTOR:
153         restoreTableDescriptor(env);
154         break;
155       case DELETE_COLUMN_FAMILY_PRE_OPERATION:
156         // TODO-MAYBE: call the coprocessor event to undo?
157         break;
158       case DELETE_COLUMN_FAMILY_PREPARE:
159         break; // nothing to do
160       default:
161         throw new UnsupportedOperationException(this + " unhandled state=" + state);
162       }
163     } catch (IOException e) {
164       // This will be retried. Unless there is a bug in the code,
165       // this should be just a "temporary error" (e.g. network down)
166       LOG.warn("Failed rollback attempt step " + state + " for deleting the column family"
167           + getColumnFamilyName() + " to the table " + tableName, e);
168       throw e;
169     }
170   }
171 
172   @Override
173   protected DeleteColumnFamilyState getState(final int stateId) {
174     return DeleteColumnFamilyState.valueOf(stateId);
175   }
176 
177   @Override
178   protected int getStateId(final DeleteColumnFamilyState state) {
179     return state.getNumber();
180   }
181 
182   @Override
183   protected DeleteColumnFamilyState getInitialState() {
184     return DeleteColumnFamilyState.DELETE_COLUMN_FAMILY_PREPARE;
185   }
186 
187   @Override
188   protected void setNextState(DeleteColumnFamilyState state) {
189     if (aborted.get() && isRollbackSupported(state)) {
190       setAbortFailure("delete-columnfamily", "abort requested");
191     } else {
192       super.setNextState(state);
193     }
194   }
195 
196   @Override
197   public boolean abort(final MasterProcedureEnv env) {
198     aborted.set(true);
199     return true;
200   }
201 
202   @Override
203   protected boolean acquireLock(final MasterProcedureEnv env) {
204     if (!env.isInitialized()) return false;
205     return env.getProcedureQueue().tryAcquireTableWrite(
206       tableName,
207       EventType.C_M_DELETE_FAMILY.toString());
208   }
209 
210   @Override
211   protected void releaseLock(final MasterProcedureEnv env) {
212     env.getProcedureQueue().releaseTableWrite(tableName);
213   }
214 
215   @Override
216   public void serializeStateData(final OutputStream stream) throws IOException {
217     super.serializeStateData(stream);
218 
219     MasterProcedureProtos.DeleteColumnFamilyStateData.Builder deleteCFMsg =
220         MasterProcedureProtos.DeleteColumnFamilyStateData.newBuilder()
221             .setUserInfo(MasterProcedureUtil.toProtoUserInfo(user))
222             .setTableName(ProtobufUtil.toProtoTableName(tableName))
223             .setColumnfamilyName(ByteStringer.wrap(familyName));
224     if (unmodifiedHTableDescriptor != null) {
225       deleteCFMsg.setUnmodifiedTableSchema(unmodifiedHTableDescriptor.convert());
226     }
227 
228     deleteCFMsg.build().writeDelimitedTo(stream);
229   }
230 
231   @Override
232   public void deserializeStateData(final InputStream stream) throws IOException {
233     super.deserializeStateData(stream);
234     MasterProcedureProtos.DeleteColumnFamilyStateData deleteCFMsg =
235         MasterProcedureProtos.DeleteColumnFamilyStateData.parseDelimitedFrom(stream);
236     user = MasterProcedureUtil.toUserInfo(deleteCFMsg.getUserInfo());
237     tableName = ProtobufUtil.toTableName(deleteCFMsg.getTableName());
238     familyName = deleteCFMsg.getColumnfamilyName().toByteArray();
239 
240     if (deleteCFMsg.hasUnmodifiedTableSchema()) {
241       unmodifiedHTableDescriptor = HTableDescriptor.convert(deleteCFMsg.getUnmodifiedTableSchema());
242     }
243   }
244 
245   @Override
246   public void toStringClassDetails(StringBuilder sb) {
247     sb.append(getClass().getSimpleName());
248     sb.append(" (table=");
249     sb.append(tableName);
250     sb.append(", columnfamily=");
251     if (familyName != null) {
252       sb.append(getColumnFamilyName());
253     } else {
254       sb.append("Unknown");
255     }
256     sb.append(")");
257   }
258 
259   @Override
260   public TableName getTableName() {
261     return tableName;
262   }
263 
264   @Override
265   public TableOperationType getTableOperationType() {
266     return TableOperationType.EDIT;
267   }
268 
269   /**
270    * Action before any real action of deleting column family.
271    * @param env MasterProcedureEnv
272    * @throws IOException
273    */
274   private void prepareDelete(final MasterProcedureEnv env) throws IOException {
275     // Checks whether the table is allowed to be modified.
276     MasterDDLOperationHelper.checkTableModifiable(env, tableName);
277 
278     // In order to update the descriptor, we need to retrieve the old descriptor for comparison.
279     unmodifiedHTableDescriptor = env.getMasterServices().getTableDescriptors().get(tableName);
280     if (unmodifiedHTableDescriptor == null) {
281       throw new IOException("HTableDescriptor missing for " + tableName);
282     }
283     if (!unmodifiedHTableDescriptor.hasFamily(familyName)) {
284       throw new InvalidFamilyOperationException("Family '" + getColumnFamilyName()
285           + "' does not exist, so it cannot be deleted");
286     }
287 
288     if (unmodifiedHTableDescriptor.getColumnFamilies().length == 1) {
289       throw new InvalidFamilyOperationException("Family '" + getColumnFamilyName()
290         + "' is the only column family in the table, so it cannot be deleted");
291     }
292 
293     // whether mob family
294     hasMob = unmodifiedHTableDescriptor.getFamily(familyName).isMobEnabled();
295   }
296 
297   /**
298    * Action before deleting column family.
299    * @param env MasterProcedureEnv
300    * @param state the procedure state
301    * @throws IOException
302    * @throws InterruptedException
303    */
304   private void preDelete(final MasterProcedureEnv env, final DeleteColumnFamilyState state)
305       throws IOException, InterruptedException {
306     runCoprocessorAction(env, state);
307   }
308 
309   /**
310    * Remove the column family from the file system and update the table descriptor
311    */
312   private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException {
313     // Update table descriptor
314     LOG.info("DeleteColumn. Table = " + tableName + " family = " + getColumnFamilyName());
315 
316     HTableDescriptor htd = env.getMasterServices().getTableDescriptors().get(tableName);
317 
318     if (!htd.hasFamily(familyName)) {
319       // It is possible to reach this situation, as we could already delete the column family
320       // from table descriptor, but the master failover happens before we complete this state.
321       // We should be able to handle running this function multiple times without causing problem.
322       return;
323     }
324 
325     htd.removeFamily(familyName);
326     env.getMasterServices().getTableDescriptors().add(htd);
327   }
328 
329   /**
330    * Restore back to the old descriptor
331    * @param env MasterProcedureEnv
332    * @throws IOException
333    **/
334   private void restoreTableDescriptor(final MasterProcedureEnv env) throws IOException {
335     env.getMasterServices().getTableDescriptors().add(unmodifiedHTableDescriptor);
336 
337     // Make sure regions are opened after table descriptor is updated.
338     reOpenAllRegionsIfTableIsOnline(env);
339   }
340 
341   /**
342    * Remove the column family from the file system
343    **/
344   private void deleteFromFs(final MasterProcedureEnv env) throws IOException {
345     MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(env, tableName,
346       getRegionInfoList(env), familyName, hasMob);
347   }
348 
349   /**
350    * Action after deleting column family.
351    * @param env MasterProcedureEnv
352    * @param state the procedure state
353    * @throws IOException
354    * @throws InterruptedException
355    */
356   private void postDelete(final MasterProcedureEnv env, final DeleteColumnFamilyState state)
357       throws IOException, InterruptedException {
358     runCoprocessorAction(env, state);
359   }
360 
361   /**
362    * Last action from the procedure - executed when online schema change is supported.
363    * @param env MasterProcedureEnv
364    * @throws IOException
365    */
366   private void reOpenAllRegionsIfTableIsOnline(final MasterProcedureEnv env) throws IOException {
367     // This operation only run when the table is enabled.
368     if (!env.getMasterServices().getAssignmentManager().getTableStateManager()
369         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
370       return;
371     }
372 
373     if (MasterDDLOperationHelper.reOpenAllRegions(env, getTableName(), getRegionInfoList(env))) {
374       LOG.info("Completed delete column family operation on table " + getTableName());
375     } else {
376       LOG.warn("Error on reopening the regions on table " + getTableName());
377     }
378   }
379 
380   /**
381    * The procedure could be restarted from a different machine. If the variable is null, we need to
382    * retrieve it.
383    * @return traceEnabled
384    */
385   private Boolean isTraceEnabled() {
386     if (traceEnabled == null) {
387       traceEnabled = LOG.isTraceEnabled();
388     }
389     return traceEnabled;
390   }
391 
392   private String getColumnFamilyName() {
393     return Bytes.toString(familyName);
394   }
395 
396   /**
397    * Coprocessor Action.
398    * @param env MasterProcedureEnv
399    * @param state the procedure state
400    * @throws IOException
401    * @throws InterruptedException
402    */
403   private void runCoprocessorAction(final MasterProcedureEnv env,
404       final DeleteColumnFamilyState state) throws IOException, InterruptedException {
405     final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
406     if (cpHost != null) {
407       user.doAs(new PrivilegedExceptionAction<Void>() {
408         @Override
409         public Void run() throws Exception {
410           switch (state) {
411           case DELETE_COLUMN_FAMILY_PRE_OPERATION:
412             cpHost.preDeleteColumnHandler(tableName, familyName);
413             break;
414           case DELETE_COLUMN_FAMILY_POST_OPERATION:
415             cpHost.postDeleteColumnHandler(tableName, familyName);
416             break;
417           default:
418             throw new UnsupportedOperationException(this + " unhandled state=" + state);
419           }
420           return null;
421         }
422       });
423     }
424   }
425 
426   /*
427    * Check whether we are in the state that can be rollback
428    */
429   private boolean isRollbackSupported(final DeleteColumnFamilyState state) {
430     switch (state) {
431     case DELETE_COLUMN_FAMILY_REOPEN_ALL_REGIONS:
432     case DELETE_COLUMN_FAMILY_POST_OPERATION:
433     case DELETE_COLUMN_FAMILY_DELETE_FS_LAYOUT:
434         // It is not safe to rollback if we reach to these states.
435         return false;
436       default:
437         break;
438     }
439     return true;
440   }
441 
442   private List<HRegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException {
443     if (regionInfoList == null) {
444       regionInfoList = ProcedureSyncWait.getRegionsFromMeta(env, getTableName());
445     }
446     return regionInfoList;
447   }
448 }