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