001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.hbase.master.procedure;
020
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.fs.FileSystem;
030import org.apache.hadoop.fs.Path;
031import org.apache.hadoop.hbase.MetaTableAccessor;
032import org.apache.hadoop.hbase.TableExistsException;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.client.RegionInfo;
035import org.apache.hadoop.hbase.client.TableDescriptor;
036import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
037import org.apache.hadoop.hbase.errorhandling.ForeignException;
038import org.apache.hadoop.hbase.errorhandling.ForeignExceptionDispatcher;
039import org.apache.hadoop.hbase.master.MasterCoprocessorHost;
040import org.apache.hadoop.hbase.master.MasterFileSystem;
041import org.apache.hadoop.hbase.master.MetricsSnapshot;
042import org.apache.hadoop.hbase.master.procedure.CreateTableProcedure.CreateHdfsRegions;
043import org.apache.hadoop.hbase.mob.MobUtils;
044import org.apache.hadoop.hbase.monitoring.MonitoredTask;
045import org.apache.hadoop.hbase.monitoring.TaskMonitor;
046import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer;
047import org.apache.hadoop.hbase.snapshot.ClientSnapshotDescriptionUtils;
048import org.apache.hadoop.hbase.snapshot.RestoreSnapshotException;
049import org.apache.hadoop.hbase.snapshot.RestoreSnapshotHelper;
050import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils;
051import org.apache.hadoop.hbase.snapshot.SnapshotManifest;
052import org.apache.hadoop.hbase.util.FSTableDescriptors;
053import org.apache.hadoop.hbase.util.FSUtils;
054import org.apache.hadoop.hbase.util.Pair;
055import org.apache.yetus.audience.InterfaceAudience;
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
059import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
060import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
061import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos;
062import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.CloneSnapshotState;
063import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos.SnapshotDescription;
064
065@InterfaceAudience.Private
066public class CloneSnapshotProcedure
067    extends AbstractStateMachineTableProcedure<CloneSnapshotState> {
068  private static final Logger LOG = LoggerFactory.getLogger(CloneSnapshotProcedure.class);
069
070  private TableDescriptor tableDescriptor;
071  private SnapshotDescription snapshot;
072  private boolean restoreAcl;
073  private List<RegionInfo> newRegions = null;
074  private Map<String, Pair<String, String> > parentsToChildrenPairMap = new HashMap<>();
075
076  // Monitor
077  private MonitoredTask monitorStatus = null;
078
079  private Boolean traceEnabled = null;
080
081  /**
082   * Constructor (for failover)
083   */
084  public CloneSnapshotProcedure() {
085  }
086
087  public CloneSnapshotProcedure(final MasterProcedureEnv env,
088      final TableDescriptor tableDescriptor, final SnapshotDescription snapshot) {
089    this(env, tableDescriptor, snapshot, false);
090  }
091
092  /**
093   * Constructor
094   * @param env MasterProcedureEnv
095   * @param tableDescriptor the table to operate on
096   * @param snapshot snapshot to clone from
097   */
098  public CloneSnapshotProcedure(final MasterProcedureEnv env,
099      final TableDescriptor tableDescriptor, final SnapshotDescription snapshot,
100      final boolean restoreAcl) {
101    super(env);
102    this.tableDescriptor = tableDescriptor;
103    this.snapshot = snapshot;
104    this.restoreAcl = restoreAcl;
105
106    getMonitorStatus();
107  }
108
109  /**
110   * Set up monitor status if it is not created.
111   */
112  private MonitoredTask getMonitorStatus() {
113    if (monitorStatus == null) {
114      monitorStatus = TaskMonitor.get().createStatus("Cloning  snapshot '" + snapshot.getName() +
115        "' to table " + getTableName());
116    }
117    return monitorStatus;
118  }
119
120  private void restoreSnapshotAcl(MasterProcedureEnv env) throws IOException {
121    Configuration conf = env.getMasterServices().getConfiguration();
122    if (restoreAcl && snapshot.hasUsersAndPermissions() && snapshot.getUsersAndPermissions() != null
123        && SnapshotDescriptionUtils.isSecurityAvailable(conf)) {
124      RestoreSnapshotHelper.restoreSnapshotAcl(snapshot, tableDescriptor.getTableName(), conf);
125    }
126  }
127
128  @Override
129  protected Flow executeFromState(final MasterProcedureEnv env, final CloneSnapshotState state)
130      throws InterruptedException {
131    if (isTraceEnabled()) {
132      LOG.trace(this + " execute state=" + state);
133    }
134    try {
135      switch (state) {
136        case CLONE_SNAPSHOT_PRE_OPERATION:
137          // Verify if we can clone the table
138          prepareClone(env);
139
140          preCloneSnapshot(env);
141          setNextState(CloneSnapshotState.CLONE_SNAPSHOT_WRITE_FS_LAYOUT);
142          break;
143        case CLONE_SNAPSHOT_WRITE_FS_LAYOUT:
144          newRegions = createFilesystemLayout(env, tableDescriptor, newRegions);
145          setNextState(CloneSnapshotState.CLONE_SNAPSHOT_ADD_TO_META);
146          break;
147        case CLONE_SNAPSHOT_ADD_TO_META:
148          addRegionsToMeta(env);
149          setNextState(CloneSnapshotState.CLONE_SNAPSHOT_ASSIGN_REGIONS);
150          break;
151        case CLONE_SNAPSHOT_ASSIGN_REGIONS:
152          CreateTableProcedure.setEnablingState(env, getTableName());
153          addChildProcedure(env.getAssignmentManager().createRoundRobinAssignProcedures(newRegions));
154          setNextState(CloneSnapshotState.CLONE_SNAPSHOT_UPDATE_DESC_CACHE);
155          break;
156        case CLONE_SNAPSHOT_UPDATE_DESC_CACHE:
157          CreateTableProcedure.setEnabledState(env, getTableName());
158          CreateTableProcedure.updateTableDescCache(env, getTableName());
159          setNextState(CloneSnapshotState.CLONE_SNAPHOST_RESTORE_ACL);
160          break;
161        case CLONE_SNAPHOST_RESTORE_ACL:
162          restoreSnapshotAcl(env);
163          setNextState(CloneSnapshotState.CLONE_SNAPSHOT_POST_OPERATION);
164          break;
165        case CLONE_SNAPSHOT_POST_OPERATION:
166          postCloneSnapshot(env);
167
168          MetricsSnapshot metricsSnapshot = new MetricsSnapshot();
169          metricsSnapshot.addSnapshotClone(
170            getMonitorStatus().getCompletionTimestamp() - getMonitorStatus().getStartTime());
171          getMonitorStatus().markComplete("Clone snapshot '"+ snapshot.getName() +"' completed!");
172          return Flow.NO_MORE_STATE;
173        default:
174          throw new UnsupportedOperationException("unhandled state=" + state);
175      }
176    } catch (IOException e) {
177      if (isRollbackSupported(state)) {
178        setFailure("master-clone-snapshot", e);
179      } else {
180        LOG.warn("Retriable error trying to clone snapshot=" + snapshot.getName() +
181          " to table=" + getTableName() + " state=" + state, e);
182      }
183    }
184    return Flow.HAS_MORE_STATE;
185  }
186
187  @Override
188  protected void rollbackState(final MasterProcedureEnv env, final CloneSnapshotState state)
189      throws IOException {
190    if (state == CloneSnapshotState.CLONE_SNAPSHOT_PRE_OPERATION) {
191      DeleteTableProcedure.deleteTableStates(env, getTableName());
192      // TODO-MAYBE: call the deleteTable coprocessor event?
193      return;
194    }
195
196    // The procedure doesn't have a rollback. The execution will succeed, at some point.
197    throw new UnsupportedOperationException("unhandled state=" + state);
198  }
199
200  @Override
201  protected boolean isRollbackSupported(final CloneSnapshotState state) {
202    switch (state) {
203      case CLONE_SNAPSHOT_PRE_OPERATION:
204        return true;
205      default:
206        return false;
207    }
208  }
209
210  @Override
211  protected CloneSnapshotState getState(final int stateId) {
212    return CloneSnapshotState.valueOf(stateId);
213  }
214
215  @Override
216  protected int getStateId(final CloneSnapshotState state) {
217    return state.getNumber();
218  }
219
220  @Override
221  protected CloneSnapshotState getInitialState() {
222    return CloneSnapshotState.CLONE_SNAPSHOT_PRE_OPERATION;
223  }
224
225  @Override
226  public TableName getTableName() {
227    return tableDescriptor.getTableName();
228  }
229
230  @Override
231  public TableOperationType getTableOperationType() {
232    return TableOperationType.CREATE; // Clone is creating a table
233  }
234
235  @Override
236  public void toStringClassDetails(StringBuilder sb) {
237    sb.append(getClass().getSimpleName());
238    sb.append(" (table=");
239    sb.append(getTableName());
240    sb.append(" snapshot=");
241    sb.append(snapshot);
242    sb.append(")");
243  }
244
245  @Override
246  protected void serializeStateData(ProcedureStateSerializer serializer)
247      throws IOException {
248    super.serializeStateData(serializer);
249
250    MasterProcedureProtos.CloneSnapshotStateData.Builder cloneSnapshotMsg =
251      MasterProcedureProtos.CloneSnapshotStateData.newBuilder()
252        .setUserInfo(MasterProcedureUtil.toProtoUserInfo(getUser()))
253        .setSnapshot(this.snapshot)
254        .setTableSchema(ProtobufUtil.toTableSchema(tableDescriptor));
255    if (newRegions != null) {
256      for (RegionInfo hri: newRegions) {
257        cloneSnapshotMsg.addRegionInfo(ProtobufUtil.toRegionInfo(hri));
258      }
259    }
260    if (!parentsToChildrenPairMap.isEmpty()) {
261      final Iterator<Map.Entry<String, Pair<String, String>>> it =
262        parentsToChildrenPairMap.entrySet().iterator();
263      while (it.hasNext()) {
264        final Map.Entry<String, Pair<String, String>> entry = it.next();
265
266        MasterProcedureProtos.RestoreParentToChildRegionsPair.Builder parentToChildrenPair =
267          MasterProcedureProtos.RestoreParentToChildRegionsPair.newBuilder()
268          .setParentRegionName(entry.getKey())
269          .setChild1RegionName(entry.getValue().getFirst())
270          .setChild2RegionName(entry.getValue().getSecond());
271        cloneSnapshotMsg.addParentToChildRegionsPairList(parentToChildrenPair);
272      }
273    }
274    serializer.serialize(cloneSnapshotMsg.build());
275  }
276
277  @Override
278  protected void deserializeStateData(ProcedureStateSerializer serializer)
279      throws IOException {
280    super.deserializeStateData(serializer);
281
282    MasterProcedureProtos.CloneSnapshotStateData cloneSnapshotMsg =
283        serializer.deserialize(MasterProcedureProtos.CloneSnapshotStateData.class);
284    setUser(MasterProcedureUtil.toUserInfo(cloneSnapshotMsg.getUserInfo()));
285    snapshot = cloneSnapshotMsg.getSnapshot();
286    tableDescriptor = ProtobufUtil.toTableDescriptor(cloneSnapshotMsg.getTableSchema());
287    if (cloneSnapshotMsg.getRegionInfoCount() == 0) {
288      newRegions = null;
289    } else {
290      newRegions = new ArrayList<>(cloneSnapshotMsg.getRegionInfoCount());
291      for (HBaseProtos.RegionInfo hri: cloneSnapshotMsg.getRegionInfoList()) {
292        newRegions.add(ProtobufUtil.toRegionInfo(hri));
293      }
294    }
295    if (cloneSnapshotMsg.getParentToChildRegionsPairListCount() > 0) {
296      parentsToChildrenPairMap = new HashMap<>();
297      for (MasterProcedureProtos.RestoreParentToChildRegionsPair parentToChildrenPair:
298        cloneSnapshotMsg.getParentToChildRegionsPairListList()) {
299        parentsToChildrenPairMap.put(
300          parentToChildrenPair.getParentRegionName(),
301          new Pair<>(
302            parentToChildrenPair.getChild1RegionName(),
303            parentToChildrenPair.getChild2RegionName()));
304      }
305    }
306    // Make sure that the monitor status is set up
307    getMonitorStatus();
308  }
309
310  /**
311   * Action before any real action of cloning from snapshot.
312   * @param env MasterProcedureEnv
313   * @throws IOException
314   */
315  private void prepareClone(final MasterProcedureEnv env) throws IOException {
316    final TableName tableName = getTableName();
317    if (MetaTableAccessor.tableExists(env.getMasterServices().getConnection(), tableName)) {
318      throw new TableExistsException(getTableName());
319    }
320  }
321
322  /**
323   * Action before cloning from snapshot.
324   * @param env MasterProcedureEnv
325   * @throws IOException
326   * @throws InterruptedException
327   */
328  private void preCloneSnapshot(final MasterProcedureEnv env)
329      throws IOException, InterruptedException {
330    if (!getTableName().isSystemTable()) {
331      // Check and update namespace quota
332      final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
333
334      SnapshotManifest manifest = SnapshotManifest.open(
335        env.getMasterConfiguration(),
336        mfs.getFileSystem(),
337        SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshot, mfs.getRootDir()),
338        snapshot);
339
340      ProcedureSyncWait.getMasterQuotaManager(env)
341        .checkNamespaceTableAndRegionQuota(getTableName(), manifest.getRegionManifestsMap().size());
342    }
343
344    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
345    if (cpHost != null) {
346      cpHost.preCreateTableAction(tableDescriptor, null, getUser());
347    }
348  }
349
350  /**
351   * Action after cloning from snapshot.
352   * @param env MasterProcedureEnv
353   * @throws IOException
354   * @throws InterruptedException
355   */
356  private void postCloneSnapshot(final MasterProcedureEnv env)
357      throws IOException, InterruptedException {
358    final MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost();
359    if (cpHost != null) {
360      final RegionInfo[] regions = (newRegions == null) ? null :
361        newRegions.toArray(new RegionInfo[newRegions.size()]);
362      cpHost.postCompletedCreateTableAction(tableDescriptor, regions, getUser());
363    }
364  }
365
366  /**
367   * Create regions in file system.
368   * @param env MasterProcedureEnv
369   * @throws IOException
370   */
371  private List<RegionInfo> createFilesystemLayout(
372    final MasterProcedureEnv env,
373    final TableDescriptor tableDescriptor,
374    final List<RegionInfo> newRegions) throws IOException {
375    return createFsLayout(env, tableDescriptor, newRegions, new CreateHdfsRegions() {
376      @Override
377      public List<RegionInfo> createHdfsRegions(
378        final MasterProcedureEnv env,
379        final Path tableRootDir, final TableName tableName,
380        final List<RegionInfo> newRegions) throws IOException {
381
382        final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
383        final FileSystem fs = mfs.getFileSystem();
384        final Path rootDir = mfs.getRootDir();
385        final Configuration conf = env.getMasterConfiguration();
386        final ForeignExceptionDispatcher monitorException = new ForeignExceptionDispatcher();
387
388        getMonitorStatus().setStatus("Clone snapshot - creating regions for table: " + tableName);
389
390        try {
391          // 1. Execute the on-disk Clone
392          Path snapshotDir = SnapshotDescriptionUtils.getCompletedSnapshotDir(snapshot, rootDir);
393          SnapshotManifest manifest = SnapshotManifest.open(conf, fs, snapshotDir, snapshot);
394          RestoreSnapshotHelper restoreHelper = new RestoreSnapshotHelper(
395            conf, fs, manifest, tableDescriptor, tableRootDir, monitorException, monitorStatus);
396          RestoreSnapshotHelper.RestoreMetaChanges metaChanges = restoreHelper.restoreHdfsRegions();
397
398          // Clone operation should not have stuff to restore or remove
399          Preconditions.checkArgument(
400            !metaChanges.hasRegionsToRestore(), "A clone should not have regions to restore");
401          Preconditions.checkArgument(
402            !metaChanges.hasRegionsToRemove(), "A clone should not have regions to remove");
403
404          // At this point the clone is complete. Next step is enabling the table.
405          String msg =
406            "Clone snapshot="+ snapshot.getName() +" on table=" + tableName + " completed!";
407          LOG.info(msg);
408          monitorStatus.setStatus(msg + " Waiting for table to be enabled...");
409
410          // 2. Let the next step to add the regions to meta
411          return metaChanges.getRegionsToAdd();
412        } catch (Exception e) {
413          String msg = "clone snapshot=" + ClientSnapshotDescriptionUtils.toString(snapshot) +
414            " failed because " + e.getMessage();
415          LOG.error(msg, e);
416          IOException rse = new RestoreSnapshotException(msg, e,
417              ProtobufUtil.createSnapshotDesc(snapshot));
418
419          // these handlers aren't futures so we need to register the error here.
420          monitorException.receive(new ForeignException("Master CloneSnapshotProcedure", rse));
421          throw rse;
422        }
423      }
424    });
425  }
426
427  /**
428   * Create region layout in file system.
429   * @param env MasterProcedureEnv
430   * @throws IOException
431   */
432  private List<RegionInfo> createFsLayout(
433    final MasterProcedureEnv env,
434    final TableDescriptor tableDescriptor,
435    List<RegionInfo> newRegions,
436    final CreateHdfsRegions hdfsRegionHandler) throws IOException {
437    final MasterFileSystem mfs = env.getMasterServices().getMasterFileSystem();
438    final Path tempdir = mfs.getTempDir();
439
440    // 1. Create Table Descriptor
441    // using a copy of descriptor, table will be created enabling first
442    final Path tempTableDir = FSUtils.getTableDir(tempdir, tableDescriptor.getTableName());
443    ((FSTableDescriptors)(env.getMasterServices().getTableDescriptors()))
444      .createTableDescriptorForTableDirectory(tempTableDir,
445              TableDescriptorBuilder.newBuilder(tableDescriptor).build(), false);
446
447    // 2. Create Regions
448    newRegions = hdfsRegionHandler.createHdfsRegions(
449      env, tempdir, tableDescriptor.getTableName(), newRegions);
450
451    // 3. Move Table temp directory to the hbase root location
452    CreateTableProcedure.moveTempDirectoryToHBaseRoot(env, tableDescriptor, tempTableDir);
453    // Move Table temp mob directory to the hbase root location
454    Path tempMobTableDir = MobUtils.getMobTableDir(tempdir, tableDescriptor.getTableName());
455    if (mfs.getFileSystem().exists(tempMobTableDir)) {
456      moveTempMobDirectoryToHBaseRoot(mfs, tableDescriptor, tempMobTableDir);
457    }
458    return newRegions;
459  }
460
461  /**
462   * Move table temp mob directory to the hbase root location
463   * @param mfs The master file system
464   * @param tableDescriptor The table to operate on
465   * @param tempMobTableDir The temp mob directory of table
466   * @throws IOException If failed to move temp mob dir to hbase root dir
467   */
468  private void moveTempMobDirectoryToHBaseRoot(final MasterFileSystem mfs,
469      final TableDescriptor tableDescriptor, final Path tempMobTableDir) throws IOException {
470    FileSystem fs = mfs.getFileSystem();
471    final Path tableMobDir =
472        MobUtils.getMobTableDir(mfs.getRootDir(), tableDescriptor.getTableName());
473    if (!fs.delete(tableMobDir, true) && fs.exists(tableMobDir)) {
474      throw new IOException("Couldn't delete mob table " + tableMobDir);
475    }
476    if (!fs.exists(tableMobDir.getParent())) {
477      fs.mkdirs(tableMobDir.getParent());
478    }
479    if (!fs.rename(tempMobTableDir, tableMobDir)) {
480      throw new IOException("Unable to move mob table from temp=" + tempMobTableDir
481          + " to hbase root=" + tableMobDir);
482    }
483  }
484
485  /**
486   * Add regions to hbase:meta table.
487   * @param env MasterProcedureEnv
488   * @throws IOException
489   */
490  private void addRegionsToMeta(final MasterProcedureEnv env) throws IOException {
491    newRegions = CreateTableProcedure.addTableToMeta(env, tableDescriptor, newRegions);
492
493    // TODO: parentsToChildrenPairMap is always empty, which makes updateMetaParentRegions()
494    // a no-op. This part seems unnecessary. Figure out. - Appy 12/21/17
495    RestoreSnapshotHelper.RestoreMetaChanges metaChanges =
496        new RestoreSnapshotHelper.RestoreMetaChanges(
497                tableDescriptor, parentsToChildrenPairMap);
498    metaChanges.updateMetaParentRegions(env.getMasterServices().getConnection(), newRegions);
499  }
500
501  /**
502   * The procedure could be restarted from a different machine. If the variable is null, we need to
503   * retrieve it.
504   * @return traceEnabled
505   */
506  private Boolean isTraceEnabled() {
507    if (traceEnabled == null) {
508      traceEnabled = LOG.isTraceEnabled();
509    }
510    return traceEnabled;
511  }
512}