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