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.HConstants;
33  import org.apache.hadoop.hbase.HRegionInfo;
34  import org.apache.hadoop.hbase.HTableDescriptor;
35  import org.apache.hadoop.hbase.MetaTableAccessor;
36  import org.apache.hadoop.hbase.TableName;
37  import org.apache.hadoop.hbase.TableNotDisabledException;
38  import org.apache.hadoop.hbase.TableNotFoundException;
39  import org.apache.hadoop.hbase.classification.InterfaceAudience;
40  import org.apache.hadoop.hbase.client.Connection;
41  import org.apache.hadoop.hbase.client.Result;
42  import org.apache.hadoop.hbase.client.ResultScanner;
43  import org.apache.hadoop.hbase.client.Scan;
44  import org.apache.hadoop.hbase.client.Table;
45  import org.apache.hadoop.hbase.executor.EventType;
46  import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
47  import org.apache.hadoop.hbase.procedure2.StateMachineProcedure;
48  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos;
49  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos.ModifyTableState;
50  import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos;
51  import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil;
52  import org.apache.hadoop.security.UserGroupInformation;
53  
54  @InterfaceAudience.Private
55  public class ModifyTableProcedure
56      extends StateMachineProcedure<MasterProcedureEnv, ModifyTableState>
57      implements TableProcedureInterface {
58    private static final Log LOG = LogFactory.getLog(ModifyTableProcedure.class);
59  
60    private final AtomicBoolean aborted = new AtomicBoolean(false);
61  
62    private HTableDescriptor unmodifiedHTableDescriptor = null;
63    private HTableDescriptor modifiedHTableDescriptor;
64    private UserGroupInformation user;
65    private boolean deleteColumnFamilyInModify;
66  
67    private List<HRegionInfo> regionInfoList;
68    private Boolean traceEnabled = null;
69  
70    public ModifyTableProcedure() {
71      initilize();
72    }
73  
74    public ModifyTableProcedure(final MasterProcedureEnv env, final HTableDescriptor htd) {
75      initilize();
76      this.modifiedHTableDescriptor = htd;
77      this.user = env.getRequestUser().getUGI();
78      this.setOwner(this.user.getShortUserName());
79    }
80  
81    private void initilize() {
82      this.unmodifiedHTableDescriptor = null;
83      this.regionInfoList = null;
84      this.traceEnabled = null;
85      this.deleteColumnFamilyInModify = false;
86    }
87  
88    @Override
89    protected Flow executeFromState(final MasterProcedureEnv env, final ModifyTableState state) {
90      if (isTraceEnabled()) {
91        LOG.trace(this + " execute state=" + state);
92      }
93  
94      try {
95        switch (state) {
96        case MODIFY_TABLE_PREPARE:
97          prepareModify(env);
98          setNextState(ModifyTableState.MODIFY_TABLE_PRE_OPERATION);
99          break;
100       case MODIFY_TABLE_PRE_OPERATION:
101         preModify(env, state);
102         setNextState(ModifyTableState.MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR);
103         break;
104       case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR:
105         updateTableDescriptor(env);
106         setNextState(ModifyTableState.MODIFY_TABLE_REMOVE_REPLICA_COLUMN);
107         break;
108       case MODIFY_TABLE_REMOVE_REPLICA_COLUMN:
109         updateReplicaColumnsIfNeeded(env, unmodifiedHTableDescriptor, modifiedHTableDescriptor);
110         if (deleteColumnFamilyInModify) {
111           setNextState(ModifyTableState.MODIFY_TABLE_DELETE_FS_LAYOUT);
112         } else {
113           setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
114         }
115         break;
116       case MODIFY_TABLE_DELETE_FS_LAYOUT:
117         deleteFromFs(env, unmodifiedHTableDescriptor, modifiedHTableDescriptor);
118         setNextState(ModifyTableState.MODIFY_TABLE_POST_OPERATION);
119         break;
120       case MODIFY_TABLE_POST_OPERATION:
121         postModify(env, state);
122         setNextState(ModifyTableState.MODIFY_TABLE_REOPEN_ALL_REGIONS);
123         break;
124       case MODIFY_TABLE_REOPEN_ALL_REGIONS:
125         reOpenAllRegionsIfTableIsOnline(env);
126         return Flow.NO_MORE_STATE;
127       default:
128         throw new UnsupportedOperationException("unhandled state=" + state);
129       }
130     } catch (InterruptedException|IOException e) {
131       if (!isRollbackSupported(state)) {
132         // We reach a state that cannot be rolled back. We just need to keep retry.
133         LOG.warn("Error trying to modify table=" + getTableName() + " state=" + state, e);
134       } else {
135         LOG.error("Error trying to modify table=" + getTableName() + " state=" + state, e);
136         setFailure("master-modify-table", e);
137       }
138     }
139     return Flow.HAS_MORE_STATE;
140   }
141 
142   @Override
143   protected void rollbackState(final MasterProcedureEnv env, final ModifyTableState state)
144       throws IOException {
145     if (isTraceEnabled()) {
146       LOG.trace(this + " rollback state=" + state);
147     }
148     try {
149       switch (state) {
150       case MODIFY_TABLE_REOPEN_ALL_REGIONS:
151         break; // Nothing to undo.
152       case MODIFY_TABLE_POST_OPERATION:
153         // TODO-MAYBE: call the coprocessor event to un-modify?
154         break;
155       case MODIFY_TABLE_DELETE_FS_LAYOUT:
156         // Once we reach to this state - we could NOT rollback - as it is tricky to undelete
157         // the deleted files. We are not suppose to reach here, throw exception so that we know
158         // there is a code bug to investigate.
159         assert deleteColumnFamilyInModify;
160         throw new UnsupportedOperationException(this + " rollback of state=" + state
161             + " is unsupported.");
162       case MODIFY_TABLE_REMOVE_REPLICA_COLUMN:
163         // Undo the replica column update.
164         updateReplicaColumnsIfNeeded(env, modifiedHTableDescriptor, unmodifiedHTableDescriptor);
165         break;
166       case MODIFY_TABLE_UPDATE_TABLE_DESCRIPTOR:
167         restoreTableDescriptor(env);
168         break;
169       case MODIFY_TABLE_PRE_OPERATION:
170         // TODO-MAYBE: call the coprocessor event to un-modify?
171         break;
172       case MODIFY_TABLE_PREPARE:
173         break; // Nothing to undo.
174       default:
175         throw new UnsupportedOperationException("unhandled state=" + state);
176       }
177     } catch (IOException e) {
178       LOG.warn("Fail trying to rollback modify table=" + getTableName() + " state=" + state, e);
179       throw e;
180     }
181   }
182 
183   @Override
184   protected ModifyTableState getState(final int stateId) {
185     return ModifyTableState.valueOf(stateId);
186   }
187 
188   @Override
189   protected int getStateId(final ModifyTableState state) {
190     return state.getNumber();
191   }
192 
193   @Override
194   protected ModifyTableState getInitialState() {
195     return ModifyTableState.MODIFY_TABLE_PREPARE;
196   }
197 
198   @Override
199   protected void setNextState(final ModifyTableState state) {
200     if (aborted.get() && isRollbackSupported(state)) {
201       setAbortFailure("modify-table", "abort requested");
202     } else {
203       super.setNextState(state);
204     }
205   }
206 
207   @Override
208   public boolean abort(final MasterProcedureEnv env) {
209     aborted.set(true);
210     return true;
211   }
212 
213   @Override
214   protected boolean acquireLock(final MasterProcedureEnv env) {
215     if (!env.isInitialized()) return false;
216     return env.getProcedureQueue().tryAcquireTableWrite(
217       getTableName(),
218       EventType.C_M_MODIFY_TABLE.toString());
219   }
220 
221   @Override
222   protected void releaseLock(final MasterProcedureEnv env) {
223     env.getProcedureQueue().releaseTableWrite(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     // In order to update the descriptor, we need to retrieve the old descriptor for comparison.
289     this.unmodifiedHTableDescriptor =
290         env.getMasterServices().getTableDescriptors().get(getTableName());
291 
292     if (env.getMasterServices().getAssignmentManager().getTableStateManager()
293         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
294       // We only execute this procedure with table online if online schema change config is set.
295       if (!MasterDDLOperationHelper.isOnlineSchemaChangeAllowed(env)) {
296         throw new TableNotDisabledException(getTableName());
297       }
298 
299       if (modifiedHTableDescriptor.getRegionReplication() != unmodifiedHTableDescriptor
300           .getRegionReplication()) {
301         throw new IOException("REGION_REPLICATION change is not supported for enabled tables");
302       }
303     }
304 
305     // Find out whether all column families in unmodifiedHTableDescriptor also exists in
306     // the modifiedHTableDescriptor. This is to determine whether we are safe to rollback.
307     final Set<byte[]> oldFamilies = unmodifiedHTableDescriptor.getFamiliesKeys();
308     final Set<byte[]> newFamilies = modifiedHTableDescriptor.getFamiliesKeys();
309     for (byte[] familyName : oldFamilies) {
310       if (!newFamilies.contains(familyName)) {
311         this.deleteColumnFamilyInModify = true;
312         break;
313       }
314     }
315   }
316 
317   /**
318    * Action before modifying table.
319    * @param env MasterProcedureEnv
320    * @param state the procedure state
321    * @throws IOException
322    * @throws InterruptedException
323    */
324   private void preModify(final MasterProcedureEnv env, final ModifyTableState state)
325       throws IOException, InterruptedException {
326     runCoprocessorAction(env, state);
327   }
328 
329   /**
330    * Update descriptor
331    * @param env MasterProcedureEnv
332    * @throws IOException
333    **/
334   private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException {
335     env.getMasterServices().getTableDescriptors().add(modifiedHTableDescriptor);
336   }
337 
338   /**
339    * Undo the descriptor change (for rollback)
340    * @param env MasterProcedureEnv
341    * @throws IOException
342    **/
343   private void restoreTableDescriptor(final MasterProcedureEnv env) throws IOException {
344     env.getMasterServices().getTableDescriptors().add(unmodifiedHTableDescriptor);
345 
346     // delete any new column families from the modifiedHTableDescriptor.
347     deleteFromFs(env, modifiedHTableDescriptor, unmodifiedHTableDescriptor);
348 
349     // Make sure regions are opened after table descriptor is updated.
350     reOpenAllRegionsIfTableIsOnline(env);
351   }
352 
353   /**
354    * Removes from hdfs the families that are not longer present in the new table descriptor.
355    * @param env MasterProcedureEnv
356    * @throws IOException
357    */
358   private void deleteFromFs(final MasterProcedureEnv env,
359       final HTableDescriptor oldHTableDescriptor, final HTableDescriptor newHTableDescriptor)
360       throws IOException {
361     final Set<byte[]> oldFamilies = oldHTableDescriptor.getFamiliesKeys();
362     final Set<byte[]> newFamilies = newHTableDescriptor.getFamiliesKeys();
363     for (byte[] familyName : oldFamilies) {
364       if (!newFamilies.contains(familyName)) {
365         MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(
366           env,
367           getTableName(),
368           getRegionInfoList(env),
369           familyName);
370       }
371     }
372   }
373 
374   /**
375    * update replica column families if necessary.
376    * @param env MasterProcedureEnv
377    * @throws IOException
378    */
379   private void updateReplicaColumnsIfNeeded(
380     final MasterProcedureEnv env,
381     final HTableDescriptor oldHTableDescriptor,
382     final HTableDescriptor newHTableDescriptor) throws IOException {
383     final int oldReplicaCount = oldHTableDescriptor.getRegionReplication();
384     final int newReplicaCount = newHTableDescriptor.getRegionReplication();
385 
386     if (newReplicaCount < oldReplicaCount) {
387       Set<byte[]> tableRows = new HashSet<byte[]>();
388       Connection connection = env.getMasterServices().getConnection();
389       Scan scan = MetaTableAccessor.getScanForTableName(getTableName());
390       scan.addColumn(HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER);
391 
392       try (Table metaTable = connection.getTable(TableName.META_TABLE_NAME)) {
393         ResultScanner resScanner = metaTable.getScanner(scan);
394         for (Result result : resScanner) {
395           tableRows.add(result.getRow());
396         }
397         MetaTableAccessor.removeRegionReplicasFromMeta(
398           tableRows,
399           newReplicaCount,
400           oldReplicaCount - newReplicaCount,
401           connection);
402       }
403     }
404 
405     // Setup replication for region replicas if needed
406     if (newReplicaCount > 1 && oldReplicaCount <= 1) {
407       ServerRegionReplicaUtil.setupRegionReplicaReplication(env.getMasterConfiguration());
408     }
409   }
410 
411   /**
412    * Action after modifying table.
413    * @param env MasterProcedureEnv
414    * @param state the procedure state
415    * @throws IOException
416    * @throws InterruptedException
417    */
418   private void postModify(final MasterProcedureEnv env, final ModifyTableState state)
419       throws IOException, InterruptedException {
420     runCoprocessorAction(env, state);
421   }
422 
423   /**
424    * Last action from the procedure - executed when online schema change is supported.
425    * @param env MasterProcedureEnv
426    * @throws IOException
427    */
428   private void reOpenAllRegionsIfTableIsOnline(final MasterProcedureEnv env) throws IOException {
429     // This operation only run when the table is enabled.
430     if (!env.getMasterServices().getAssignmentManager().getTableStateManager()
431         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
432       return;
433     }
434 
435     if (MasterDDLOperationHelper.reOpenAllRegions(env, getTableName(), getRegionInfoList(env))) {
436       LOG.info("Completed modify table operation on table " + getTableName());
437     } else {
438       LOG.warn("Error on reopening the regions on table " + getTableName());
439     }
440   }
441 
442   /**
443    * The procedure could be restarted from a different machine. If the variable is null, we need to
444    * retrieve it.
445    * @return traceEnabled whether the trace is enabled
446    */
447   private Boolean isTraceEnabled() {
448     if (traceEnabled == null) {
449       traceEnabled = LOG.isTraceEnabled();
450     }
451     return traceEnabled;
452   }
453 
454   /**
455    * Coprocessor Action.
456    * @param env MasterProcedureEnv
457    * @param state the procedure state
458    * @throws IOException
459    * @throws InterruptedException
460    */
461   private void runCoprocessorAction(final MasterProcedureEnv env, final ModifyTableState state)
462       throws IOException, InterruptedException {
463     final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
464     if (cpHost != null) {
465       user.doAs(new PrivilegedExceptionAction<Void>() {
466         @Override
467         public Void run() throws Exception {
468           switch (state) {
469           case MODIFY_TABLE_PRE_OPERATION:
470             cpHost.preModifyTableHandler(getTableName(), modifiedHTableDescriptor);
471             break;
472           case MODIFY_TABLE_POST_OPERATION:
473             cpHost.postModifyTableHandler(getTableName(), modifiedHTableDescriptor);
474             break;
475           default:
476             throw new UnsupportedOperationException(this + " unhandled state=" + state);
477           }
478           return null;
479         }
480       });
481     }
482   }
483 
484   /*
485    * Check whether we are in the state that can be rollback
486    */
487   private boolean isRollbackSupported(final ModifyTableState state) {
488     if (deleteColumnFamilyInModify) {
489       switch (state) {
490       case MODIFY_TABLE_DELETE_FS_LAYOUT:
491       case MODIFY_TABLE_POST_OPERATION:
492       case MODIFY_TABLE_REOPEN_ALL_REGIONS:
493         // It is not safe to rollback if we reach to these states.
494         return false;
495       default:
496         break;
497       }
498     }
499     return true;
500   }
501 
502   private List<HRegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException {
503     if (regionInfoList == null) {
504       regionInfoList = ProcedureSyncWait.getRegionsFromMeta(env, getTableName());
505     }
506     return regionInfoList;
507   }
508 }