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.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  import java.util.concurrent.atomic.AtomicBoolean;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.hadoop.hbase.DoNotRetryIOException;
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.master.MasterCoprocessorHost;
46  import org.apache.hadoop.hbase.procedure2.StateMachineProcedure;
47  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos;
48  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos.ModifyTableState;
49  import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos;
50  import org.apache.hadoop.hbase.security.User;
51  import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil;
52  
53  @InterfaceAudience.Private
54  public class ModifyTableProcedure
55      extends StateMachineProcedure<MasterProcedureEnv, ModifyTableState>
56      implements TableProcedureInterface {
57    private static final Log LOG = LogFactory.getLog(ModifyTableProcedure.class);
58  
59    private final AtomicBoolean aborted = new AtomicBoolean(false);
60  
61    private HTableDescriptor unmodifiedHTableDescriptor = null;
62    private HTableDescriptor modifiedHTableDescriptor;
63    private User user;
64    private boolean deleteColumnFamilyInModify;
65  
66    private List<HRegionInfo> regionInfoList;
67    private Boolean traceEnabled = null;
68  
69    public ModifyTableProcedure() {
70      initilize();
71    }
72  
73    public ModifyTableProcedure(final MasterProcedureEnv env, final HTableDescriptor htd) {
74      initilize();
75      this.modifiedHTableDescriptor = htd;
76      this.user = env.getRequestUser();
77      this.setOwner(this.user.getShortName());
78    }
79  
80    private void initilize() {
81      this.unmodifiedHTableDescriptor = null;
82      this.regionInfoList = null;
83      this.traceEnabled = null;
84      this.deleteColumnFamilyInModify = false;
85    }
86  
87    @Override
88    protected Flow executeFromState(final MasterProcedureEnv env, final ModifyTableState state)
89        throws InterruptedException {
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 (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.waitInitialized(this)) return false;
216     return env.getProcedureQueue().tryAcquireTableExclusiveLock(this, getTableName());
217   }
218 
219   @Override
220   protected void releaseLock(final MasterProcedureEnv env) {
221     env.getProcedureQueue().releaseTableExclusiveLock(this, getTableName());
222   }
223 
224   @Override
225   public void serializeStateData(final OutputStream stream) throws IOException {
226     super.serializeStateData(stream);
227 
228     MasterProcedureProtos.ModifyTableStateData.Builder modifyTableMsg =
229         MasterProcedureProtos.ModifyTableStateData.newBuilder()
230             .setUserInfo(MasterProcedureUtil.toProtoUserInfo(user))
231             .setModifiedTableSchema(modifiedHTableDescriptor.convert())
232             .setDeleteColumnFamilyInModify(deleteColumnFamilyInModify);
233 
234     if (unmodifiedHTableDescriptor != null) {
235       modifyTableMsg.setUnmodifiedTableSchema(unmodifiedHTableDescriptor.convert());
236     }
237 
238     modifyTableMsg.build().writeDelimitedTo(stream);
239   }
240 
241   @Override
242   public void deserializeStateData(final InputStream stream) throws IOException {
243     super.deserializeStateData(stream);
244 
245     MasterProcedureProtos.ModifyTableStateData modifyTableMsg =
246         MasterProcedureProtos.ModifyTableStateData.parseDelimitedFrom(stream);
247     user = MasterProcedureUtil.toUserInfo(modifyTableMsg.getUserInfo());
248     modifiedHTableDescriptor = HTableDescriptor.convert(modifyTableMsg.getModifiedTableSchema());
249     deleteColumnFamilyInModify = modifyTableMsg.getDeleteColumnFamilyInModify();
250 
251     if (modifyTableMsg.hasUnmodifiedTableSchema()) {
252       unmodifiedHTableDescriptor =
253           HTableDescriptor.convert(modifyTableMsg.getUnmodifiedTableSchema());
254     }
255   }
256 
257   @Override
258   public void toStringClassDetails(StringBuilder sb) {
259     sb.append(getClass().getSimpleName());
260     sb.append(" (table=");
261     sb.append(getTableName());
262     sb.append(")");
263   }
264 
265   @Override
266   public TableName getTableName() {
267     return modifiedHTableDescriptor.getTableName();
268   }
269 
270   @Override
271   public TableOperationType getTableOperationType() {
272     return TableOperationType.EDIT;
273   }
274 
275   /**
276    * Check conditions before any real action of modifying a table.
277    * @param env MasterProcedureEnv
278    * @throws IOException
279    */
280   private void prepareModify(final MasterProcedureEnv env) throws IOException {
281     // Checks whether the table exists
282     if (!MetaTableAccessor.tableExists(env.getMasterServices().getConnection(), getTableName())) {
283       throw new TableNotFoundException(getTableName());
284     }
285 
286     // check that we have at least 1 CF
287     if (modifiedHTableDescriptor.getColumnFamilies().length == 0) {
288       throw new DoNotRetryIOException("Table " + getTableName().toString() +
289         " should have at least one column family.");
290     }
291 
292     // In order to update the descriptor, we need to retrieve the old descriptor for comparison.
293     this.unmodifiedHTableDescriptor =
294         env.getMasterServices().getTableDescriptors().get(getTableName());
295 
296     if (env.getMasterServices().getAssignmentManager().getTableStateManager()
297         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
298       // We only execute this procedure with table online if online schema change config is set.
299       if (!MasterDDLOperationHelper.isOnlineSchemaChangeAllowed(env)) {
300         throw new TableNotDisabledException(getTableName());
301       }
302 
303       if (modifiedHTableDescriptor.getRegionReplication() != unmodifiedHTableDescriptor
304           .getRegionReplication()) {
305         throw new IOException("REGION_REPLICATION change is not supported for enabled tables");
306       }
307     }
308 
309     // Find out whether all column families in unmodifiedHTableDescriptor also exists in
310     // the modifiedHTableDescriptor. This is to determine whether we are safe to rollback.
311     final Set<byte[]> oldFamilies = unmodifiedHTableDescriptor.getFamiliesKeys();
312     final Set<byte[]> newFamilies = modifiedHTableDescriptor.getFamiliesKeys();
313     for (byte[] familyName : oldFamilies) {
314       if (!newFamilies.contains(familyName)) {
315         this.deleteColumnFamilyInModify = true;
316         break;
317       }
318     }
319   }
320 
321   /**
322    * Action before modifying table.
323    * @param env MasterProcedureEnv
324    * @param state the procedure state
325    * @throws IOException
326    * @throws InterruptedException
327    */
328   private void preModify(final MasterProcedureEnv env, final ModifyTableState state)
329       throws IOException, InterruptedException {
330     runCoprocessorAction(env, state);
331   }
332 
333   /**
334    * Update descriptor
335    * @param env MasterProcedureEnv
336    * @throws IOException
337    **/
338   private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException {
339     env.getMasterServices().getTableDescriptors().add(modifiedHTableDescriptor);
340   }
341 
342   /**
343    * Undo the descriptor change (for rollback)
344    * @param env MasterProcedureEnv
345    * @throws IOException
346    **/
347   private void restoreTableDescriptor(final MasterProcedureEnv env) throws IOException {
348     env.getMasterServices().getTableDescriptors().add(unmodifiedHTableDescriptor);
349 
350     // delete any new column families from the modifiedHTableDescriptor.
351     deleteFromFs(env, modifiedHTableDescriptor, unmodifiedHTableDescriptor);
352 
353     // Make sure regions are opened after table descriptor is updated.
354     reOpenAllRegionsIfTableIsOnline(env);
355   }
356 
357   /**
358    * Removes from hdfs the families that are not longer present in the new table descriptor.
359    * @param env MasterProcedureEnv
360    * @throws IOException
361    */
362   private void deleteFromFs(final MasterProcedureEnv env,
363       final HTableDescriptor oldHTableDescriptor, final HTableDescriptor newHTableDescriptor)
364       throws IOException {
365     final Set<byte[]> oldFamilies = oldHTableDescriptor.getFamiliesKeys();
366     final Set<byte[]> newFamilies = newHTableDescriptor.getFamiliesKeys();
367     for (byte[] familyName : oldFamilies) {
368       if (!newFamilies.contains(familyName)) {
369         MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(
370           env,
371           getTableName(),
372           getRegionInfoList(env),
373           familyName);
374       }
375     }
376   }
377 
378   /**
379    * update replica column families if necessary.
380    * @param env MasterProcedureEnv
381    * @throws IOException
382    */
383   private void updateReplicaColumnsIfNeeded(
384     final MasterProcedureEnv env,
385     final HTableDescriptor oldHTableDescriptor,
386     final HTableDescriptor newHTableDescriptor) throws IOException {
387     final int oldReplicaCount = oldHTableDescriptor.getRegionReplication();
388     final int newReplicaCount = newHTableDescriptor.getRegionReplication();
389 
390     if (newReplicaCount < oldReplicaCount) {
391       Set<byte[]> tableRows = new HashSet<byte[]>();
392       Connection connection = env.getMasterServices().getConnection();
393       Scan scan = MetaTableAccessor.getScanForTableName(getTableName());
394       scan.addColumn(HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER);
395 
396       try (Table metaTable = connection.getTable(TableName.META_TABLE_NAME)) {
397         ResultScanner resScanner = metaTable.getScanner(scan);
398         for (Result result : resScanner) {
399           tableRows.add(result.getRow());
400         }
401         MetaTableAccessor.removeRegionReplicasFromMeta(
402           tableRows,
403           newReplicaCount,
404           oldReplicaCount - newReplicaCount,
405           connection);
406       }
407     }
408 
409     // Setup replication for region replicas if needed
410     if (newReplicaCount > 1 && oldReplicaCount <= 1) {
411       ServerRegionReplicaUtil.setupRegionReplicaReplication(env.getMasterConfiguration());
412     }
413   }
414 
415   /**
416    * Action after modifying table.
417    * @param env MasterProcedureEnv
418    * @param state the procedure state
419    * @throws IOException
420    * @throws InterruptedException
421    */
422   private void postModify(final MasterProcedureEnv env, final ModifyTableState state)
423       throws IOException, InterruptedException {
424     runCoprocessorAction(env, state);
425   }
426 
427   /**
428    * Last action from the procedure - executed when online schema change is supported.
429    * @param env MasterProcedureEnv
430    * @throws IOException
431    */
432   private void reOpenAllRegionsIfTableIsOnline(final MasterProcedureEnv env) throws IOException {
433     // This operation only run when the table is enabled.
434     if (!env.getMasterServices().getAssignmentManager().getTableStateManager()
435         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
436       return;
437     }
438 
439     if (MasterDDLOperationHelper.reOpenAllRegions(env, getTableName(), getRegionInfoList(env))) {
440       LOG.info("Completed modify table operation on table " + getTableName());
441     } else {
442       LOG.warn("Error on reopening the regions on table " + getTableName());
443     }
444   }
445 
446   /**
447    * The procedure could be restarted from a different machine. If the variable is null, we need to
448    * retrieve it.
449    * @return traceEnabled whether the trace is enabled
450    */
451   private Boolean isTraceEnabled() {
452     if (traceEnabled == null) {
453       traceEnabled = LOG.isTraceEnabled();
454     }
455     return traceEnabled;
456   }
457 
458   /**
459    * Coprocessor Action.
460    * @param env MasterProcedureEnv
461    * @param state the procedure state
462    * @throws IOException
463    * @throws InterruptedException
464    */
465   private void runCoprocessorAction(final MasterProcedureEnv env, final ModifyTableState state)
466       throws IOException, InterruptedException {
467     final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
468     if (cpHost != null) {
469       switch (state) {
470         case MODIFY_TABLE_PRE_OPERATION:
471           cpHost.preModifyTableHandler(getTableName(), modifiedHTableDescriptor, user);
472           break;
473         case MODIFY_TABLE_POST_OPERATION:
474           cpHost.postModifyTableHandler(getTableName(), modifiedHTableDescriptor, user);
475           break;
476         default:
477           throw new UnsupportedOperationException(this + " unhandled state=" + state);
478       }
479     }
480   }
481 
482   /*
483    * Check whether we are in the state that can be rollback
484    */
485   private boolean isRollbackSupported(final ModifyTableState state) {
486     if (deleteColumnFamilyInModify) {
487       switch (state) {
488       case MODIFY_TABLE_DELETE_FS_LAYOUT:
489       case MODIFY_TABLE_POST_OPERATION:
490       case MODIFY_TABLE_REOPEN_ALL_REGIONS:
491         // It is not safe to rollback if we reach to these states.
492         return false;
493       default:
494         break;
495       }
496     }
497     return true;
498   }
499 
500   private List<HRegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException {
501     if (regionInfoList == null) {
502       regionInfoList = ProcedureSyncWait.getRegionsFromMeta(env, getTableName());
503     }
504     return regionInfoList;
505   }
506 }