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.executor.EventType;
37  import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
38  import org.apache.hadoop.hbase.procedure2.StateMachineProcedure;
39  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
40  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos;
41  import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos.AddColumnFamilyState;
42  import org.apache.hadoop.hbase.protobuf.generated.ZooKeeperProtos;
43  import org.apache.hadoop.security.UserGroupInformation;
44  
45  /**
46   * The procedure to add a column family to an existing table.
47   */
48  @InterfaceAudience.Private
49  public class AddColumnFamilyProcedure
50      extends StateMachineProcedure<MasterProcedureEnv, AddColumnFamilyState>
51      implements TableProcedureInterface {
52    private static final Log LOG = LogFactory.getLog(AddColumnFamilyProcedure.class);
53  
54    private final AtomicBoolean aborted = new AtomicBoolean(false);
55  
56    private TableName tableName;
57    private HTableDescriptor unmodifiedHTableDescriptor;
58    private HColumnDescriptor cfDescriptor;
59    private UserGroupInformation user;
60  
61    private List<HRegionInfo> regionInfoList;
62    private Boolean traceEnabled;
63  
64    public AddColumnFamilyProcedure() {
65      this.unmodifiedHTableDescriptor = null;
66      this.regionInfoList = null;
67      this.traceEnabled = null;
68    }
69  
70    public AddColumnFamilyProcedure(
71        final MasterProcedureEnv env,
72        final TableName tableName,
73        final HColumnDescriptor cfDescriptor) throws IOException {
74      this.tableName = tableName;
75      this.cfDescriptor = cfDescriptor;
76      this.user = env.getRequestUser().getUGI();
77      this.setOwner(this.user.getShortUserName());
78      this.unmodifiedHTableDescriptor = null;
79      this.regionInfoList = null;
80      this.traceEnabled = null;
81    }
82  
83    @Override
84    protected Flow executeFromState(final MasterProcedureEnv env, final AddColumnFamilyState state) {
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 (InterruptedException|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.isInitialized()) return false;
188     return env.getProcedureQueue().tryAcquireTableWrite(
189       tableName,
190       EventType.C_M_ADD_FAMILY.toString());
191   }
192 
193   @Override
194   protected void releaseLock(final MasterProcedureEnv env) {
195     env.getProcedureQueue().releaseTableWrite(tableName);
196   }
197 
198   @Override
199   public void serializeStateData(final OutputStream stream) throws IOException {
200     super.serializeStateData(stream);
201 
202     MasterProcedureProtos.AddColumnFamilyStateData.Builder addCFMsg =
203         MasterProcedureProtos.AddColumnFamilyStateData.newBuilder()
204             .setUserInfo(MasterProcedureUtil.toProtoUserInfo(user))
205             .setTableName(ProtobufUtil.toProtoTableName(tableName))
206             .setColumnfamilySchema(cfDescriptor.convert());
207     if (unmodifiedHTableDescriptor != null) {
208       addCFMsg.setUnmodifiedTableSchema(unmodifiedHTableDescriptor.convert());
209     }
210 
211     addCFMsg.build().writeDelimitedTo(stream);
212   }
213 
214   @Override
215   public void deserializeStateData(final InputStream stream) throws IOException {
216     super.deserializeStateData(stream);
217 
218     MasterProcedureProtos.AddColumnFamilyStateData addCFMsg =
219         MasterProcedureProtos.AddColumnFamilyStateData.parseDelimitedFrom(stream);
220     user = MasterProcedureUtil.toUserInfo(addCFMsg.getUserInfo());
221     tableName = ProtobufUtil.toTableName(addCFMsg.getTableName());
222     cfDescriptor = HColumnDescriptor.convert(addCFMsg.getColumnfamilySchema());
223     if (addCFMsg.hasUnmodifiedTableSchema()) {
224       unmodifiedHTableDescriptor = HTableDescriptor.convert(addCFMsg.getUnmodifiedTableSchema());
225     }
226   }
227 
228   @Override
229   public void toStringClassDetails(StringBuilder sb) {
230     sb.append(getClass().getSimpleName());
231     sb.append(" (table=");
232     sb.append(tableName);
233     sb.append(", columnfamily=");
234     if (cfDescriptor != null) {
235       sb.append(getColumnFamilyName());
236     } else {
237       sb.append("Unknown");
238     }
239     sb.append(")");
240   }
241 
242   @Override
243   public TableName getTableName() {
244     return tableName;
245   }
246 
247   @Override
248   public TableOperationType getTableOperationType() {
249     return TableOperationType.EDIT;
250   }
251 
252   /**
253    * Action before any real action of adding column family.
254    * @param env MasterProcedureEnv
255    * @throws IOException
256    */
257   private void prepareAdd(final MasterProcedureEnv env) throws IOException {
258     // Checks whether the table is allowed to be modified.
259     MasterDDLOperationHelper.checkTableModifiable(env, tableName);
260 
261     // In order to update the descriptor, we need to retrieve the old descriptor for comparison.
262     unmodifiedHTableDescriptor = env.getMasterServices().getTableDescriptors().get(tableName);
263     if (unmodifiedHTableDescriptor == null) {
264       throw new IOException("HTableDescriptor missing for " + tableName);
265     }
266     if (unmodifiedHTableDescriptor.hasFamily(cfDescriptor.getName())) {
267       throw new InvalidFamilyOperationException("Column family '" + getColumnFamilyName()
268           + "' in table '" + tableName + "' already exists so cannot be added");
269     }
270   }
271 
272   /**
273    * Action before adding column family.
274    * @param env MasterProcedureEnv
275    * @param state the procedure state
276    * @throws IOException
277    * @throws InterruptedException
278    */
279   private void preAdd(final MasterProcedureEnv env, final AddColumnFamilyState state)
280       throws IOException, InterruptedException {
281     runCoprocessorAction(env, state);
282   }
283 
284   /**
285    * Add the column family to the file system
286    */
287   private void updateTableDescriptor(final MasterProcedureEnv env) throws IOException {
288     // Update table descriptor
289     LOG.info("AddColumn. Table = " + tableName + " HCD = " + cfDescriptor.toString());
290 
291     HTableDescriptor htd = env.getMasterServices().getTableDescriptors().get(tableName);
292 
293     if (htd.hasFamily(cfDescriptor.getName())) {
294       // It is possible to reach this situation, as we could already add the column family
295       // to table descriptor, but the master failover happens before we complete this state.
296       // We should be able to handle running this function multiple times without causing problem.
297       return;
298     }
299 
300     htd.addFamily(cfDescriptor);
301     env.getMasterServices().getTableDescriptors().add(htd);
302   }
303 
304   /**
305    * Restore the table descriptor back to pre-add
306    * @param env MasterProcedureEnv
307    * @throws IOException
308    **/
309   private void restoreTableDescriptor(final MasterProcedureEnv env) throws IOException {
310     HTableDescriptor htd = env.getMasterServices().getTableDescriptors().get(tableName);
311     if (htd.hasFamily(cfDescriptor.getName())) {
312       // Remove the column family from file system and update the table descriptor to
313       // the before-add-column-family-state
314       MasterDDLOperationHelper.deleteColumnFamilyFromFileSystem(env, tableName,
315         getRegionInfoList(env), cfDescriptor.getName(), cfDescriptor.isMobEnabled());
316 
317       env.getMasterServices().getTableDescriptors().add(unmodifiedHTableDescriptor);
318 
319       // Make sure regions are opened after table descriptor is updated.
320       reOpenAllRegionsIfTableIsOnline(env);
321     }
322   }
323 
324   /**
325    * Action after adding column family.
326    * @param env MasterProcedureEnv
327    * @param state the procedure state
328    * @throws IOException
329    * @throws InterruptedException
330    */
331   private void postAdd(final MasterProcedureEnv env, final AddColumnFamilyState state)
332       throws IOException, InterruptedException {
333     runCoprocessorAction(env, state);
334   }
335 
336   /**
337    * Last action from the procedure - executed when online schema change is supported.
338    * @param env MasterProcedureEnv
339    * @throws IOException
340    */
341   private void reOpenAllRegionsIfTableIsOnline(final MasterProcedureEnv env) throws IOException {
342     // This operation only run when the table is enabled.
343     if (!env.getMasterServices().getAssignmentManager().getTableStateManager()
344         .isTableState(getTableName(), ZooKeeperProtos.Table.State.ENABLED)) {
345       return;
346     }
347 
348     if (MasterDDLOperationHelper.reOpenAllRegions(env, getTableName(), getRegionInfoList(env))) {
349       LOG.info("Completed add column family operation on table " + getTableName());
350     } else {
351       LOG.warn("Error on reopening the regions on table " + getTableName());
352     }
353   }
354 
355   /**
356    * The procedure could be restarted from a different machine. If the variable is null, we need to
357    * retrieve it.
358    * @return traceEnabled
359    */
360   private Boolean isTraceEnabled() {
361     if (traceEnabled == null) {
362       traceEnabled = LOG.isTraceEnabled();
363     }
364     return traceEnabled;
365   }
366 
367   private String getColumnFamilyName() {
368     return cfDescriptor.getNameAsString();
369   }
370 
371   /**
372    * Coprocessor Action.
373    * @param env MasterProcedureEnv
374    * @param state the procedure state
375    * @throws IOException
376    * @throws InterruptedException
377    */
378   private void runCoprocessorAction(final MasterProcedureEnv env, final AddColumnFamilyState state)
379       throws IOException, InterruptedException {
380     final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
381     if (cpHost != null) {
382       user.doAs(new PrivilegedExceptionAction<Void>() {
383         @Override
384         public Void run() throws Exception {
385           switch (state) {
386           case ADD_COLUMN_FAMILY_PRE_OPERATION:
387             cpHost.preAddColumnHandler(tableName, cfDescriptor);
388             break;
389           case ADD_COLUMN_FAMILY_POST_OPERATION:
390             cpHost.postAddColumnHandler(tableName, cfDescriptor);
391             break;
392           default:
393             throw new UnsupportedOperationException(this + " unhandled state=" + state);
394           }
395           return null;
396         }
397       });
398     }
399   }
400 
401   private List<HRegionInfo> getRegionInfoList(final MasterProcedureEnv env) throws IOException {
402     if (regionInfoList == null) {
403       regionInfoList = ProcedureSyncWait.getRegionsFromMeta(env, getTableName());
404     }
405     return regionInfoList;
406   }
407 }