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