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