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