/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.accumulo.master.state; import java.io.IOException; import java.util.Map; import java.util.Map.Entry; import org.apache.accumulo.core.Constants; import org.apache.accumulo.core.client.Connector; import org.apache.accumulo.core.client.Scanner; import org.apache.accumulo.core.client.TableNotFoundException; import org.apache.accumulo.core.data.Key; import org.apache.accumulo.core.data.Range; import org.apache.accumulo.core.data.Value; import org.apache.accumulo.core.data.impl.KeyExtent; import org.apache.accumulo.core.metadata.MetadataTable; import org.apache.accumulo.core.metadata.RootTable; import org.apache.accumulo.core.metadata.schema.MetadataSchema; import org.apache.accumulo.core.security.Authorizations; import org.apache.accumulo.core.zookeeper.ZooUtil; import org.apache.accumulo.server.cli.ClientOpts; import org.apache.accumulo.server.master.state.CurrentState; import org.apache.accumulo.server.master.state.MergeInfo; import org.apache.accumulo.server.master.state.MergeState; import org.apache.accumulo.server.master.state.MetaDataTableScanner; import org.apache.accumulo.server.master.state.TabletLocationState; import org.apache.accumulo.server.master.state.TabletLocationState.BadLocationStateException; import org.apache.accumulo.server.master.state.TabletState; import org.apache.accumulo.server.zookeeper.ZooReaderWriter; import org.apache.hadoop.io.DataInputBuffer; import org.apache.hadoop.io.Text; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MergeStats { final static private Logger log = LoggerFactory.getLogger(MergeStats.class); MergeInfo info; int hosted = 0; int unassigned = 0; int chopped = 0; int needsToBeChopped = 0; int total = 0; boolean lowerSplit = false; boolean upperSplit = false; public MergeStats(MergeInfo info) { this.info = info; if (info.getState().equals(MergeState.NONE)) return; if (info.getExtent().getEndRow() == null) upperSplit = true; if (info.getExtent().getPrevEndRow() == null) lowerSplit = true; } public MergeInfo getMergeInfo() { return info; } public void update(KeyExtent ke, TabletState state, boolean chopped, boolean hasWALs) { if (info.getState().equals(MergeState.NONE)) return; if (!upperSplit && info.getExtent().getEndRow().equals(ke.getPrevEndRow())) { log.info("Upper split found"); upperSplit = true; } if (!lowerSplit && info.getExtent().getPrevEndRow().equals(ke.getEndRow())) { log.info("Lower split found"); lowerSplit = true; } if (!info.overlaps(ke)) return; if (info.needsToBeChopped(ke)) { this.needsToBeChopped++; if (chopped) { if (state.equals(TabletState.HOSTED)) { this.chopped++; } else if (!hasWALs) { this.chopped++; } } } this.total++; if (state.equals(TabletState.HOSTED)) this.hosted++; if (state.equals(TabletState.UNASSIGNED) || state.equals(TabletState.SUSPENDED)) this.unassigned++; } public MergeState nextMergeState(Connector connector, CurrentState master) throws Exception { MergeState state = info.getState(); if (state == MergeState.NONE) return state; if (total == 0) { log.trace("failed to see any tablets for this range, ignoring " + info.getExtent()); return state; } log.info("Computing next merge state for " + info.getExtent() + " which is presently " + state + " isDelete : " + info.isDelete()); if (state == MergeState.STARTED) { state = MergeState.SPLITTING; } if (state == MergeState.SPLITTING) { log.info(hosted + " are hosted, total " + total); if (!info.isDelete() && total == 1) { log.info("Merge range is already contained in a single tablet " + info.getExtent()); state = MergeState.COMPLETE; } else if (hosted == total) { if (info.isDelete()) { if (!lowerSplit) log.info("Waiting for " + info + " lower split to occur " + info.getExtent()); else if (!upperSplit) log.info("Waiting for " + info + " upper split to occur " + info.getExtent()); else state = MergeState.WAITING_FOR_CHOPPED; } else { state = MergeState.WAITING_FOR_CHOPPED; } } else { log.info("Waiting for " + hosted + " hosted tablets to be " + total + " " + info.getExtent()); } } if (state == MergeState.WAITING_FOR_CHOPPED) { log.info(chopped + " tablets are chopped " + info.getExtent()); if (chopped == needsToBeChopped) { state = MergeState.WAITING_FOR_OFFLINE; } else { log.info("Waiting for " + chopped + " chopped tablets to be " + needsToBeChopped + " " + info.getExtent()); } } if (state == MergeState.WAITING_FOR_OFFLINE) { if (chopped != needsToBeChopped) { log.warn("Unexpected state: chopped tablets should be " + needsToBeChopped + " was " + chopped + " merge " + info.getExtent()); // Perhaps a split occurred after we chopped, but before we went offline: start over state = MergeState.WAITING_FOR_CHOPPED; } else { log.info(chopped + " tablets are chopped, " + unassigned + " are offline " + info.getExtent()); if (unassigned == total && chopped == needsToBeChopped) { if (verifyMergeConsistency(connector, master)) state = MergeState.MERGING; else log.info("Merge consistency check failed " + info.getExtent()); } else { log.info("Waiting for " + unassigned + " unassigned tablets to be " + total + " " + info.getExtent()); } } } if (state == MergeState.MERGING) { if (hosted != 0) { // Shouldn't happen log.error("Unexpected state: hosted tablets should be zero " + hosted + " merge " + info.getExtent()); state = MergeState.WAITING_FOR_OFFLINE; } if (unassigned != total) { // Shouldn't happen log.error("Unexpected state: unassigned tablets should be " + total + " was " + unassigned + " merge " + info.getExtent()); state = MergeState.WAITING_FOR_CHOPPED; } log.info(unassigned + " tablets are unassigned " + info.getExtent()); } return state; } private boolean verifyMergeConsistency(Connector connector, CurrentState master) throws TableNotFoundException, IOException { MergeStats verify = new MergeStats(info); KeyExtent extent = info.getExtent(); Scanner scanner = connector.createScanner(extent.isMeta() ? RootTable.NAME : MetadataTable.NAME, Authorizations.EMPTY); MetaDataTableScanner.configureScanner(scanner, master); Text start = extent.getPrevEndRow(); if (start == null) { start = new Text(); } String tableId = extent.getTableId(); Text first = KeyExtent.getMetadataEntry(tableId, start); Range range = new Range(first, false, null, true); scanner.setRange(range.clip(MetadataSchema.TabletsSection.getRange())); KeyExtent prevExtent = null; log.debug("Scanning range " + range); for (Entry<Key,Value> entry : scanner) { TabletLocationState tls; try { tls = MetaDataTableScanner.createTabletLocationState(entry.getKey(), entry.getValue()); } catch (BadLocationStateException e) { log.error("{}", e.getMessage(), e); return false; } log.debug("consistency check: " + tls + " walogs " + tls.walogs.size()); if (!tls.extent.getTableId().equals(tableId)) { break; } if (!tls.walogs.isEmpty() && verify.getMergeInfo().needsToBeChopped(tls.extent)) { log.debug("failing consistency: needs to be chopped" + tls.extent); return false; } if (prevExtent == null) { // this is the first tablet observed, it must be offline and its prev row must be less than the start of the merge range if (tls.extent.getPrevEndRow() != null && tls.extent.getPrevEndRow().compareTo(start) > 0) { log.debug("failing consistency: prev row is too high " + start); return false; } if (tls.getState(master.onlineTabletServers()) != TabletState.UNASSIGNED && tls.getState(master.onlineTabletServers()) != TabletState.SUSPENDED) { log.debug("failing consistency: assigned or hosted " + tls); return false; } } else if (!tls.extent.isPreviousExtent(prevExtent)) { log.debug("hole in " + MetadataTable.NAME); return false; } prevExtent = tls.extent; verify.update(tls.extent, tls.getState(master.onlineTabletServers()), tls.chopped, !tls.walogs.isEmpty()); // stop when we've seen the tablet just beyond our range if (tls.extent.getPrevEndRow() != null && extent.getEndRow() != null && tls.extent.getPrevEndRow().compareTo(extent.getEndRow()) > 0) { break; } } log.debug("chopped " + chopped + " v.chopped " + verify.chopped + " unassigned " + unassigned + " v.unassigned " + verify.unassigned + " verify.total " + verify.total); return chopped == verify.chopped && unassigned == verify.unassigned && unassigned == verify.total; } public static void main(String[] args) throws Exception { ClientOpts opts = new ClientOpts(); opts.parseArgs(MergeStats.class.getName(), args); Connector conn = opts.getConnector(); Map<String,String> tableIdMap = conn.tableOperations().tableIdMap(); for (Entry<String,String> entry : tableIdMap.entrySet()) { final String table = entry.getKey(), tableId = entry.getValue(); String path = ZooUtil.getRoot(conn.getInstance().getInstanceID()) + Constants.ZTABLES + "/" + tableId + "/merge"; MergeInfo info = new MergeInfo(); if (ZooReaderWriter.getInstance().exists(path)) { byte[] data = ZooReaderWriter.getInstance().getData(path, new Stat()); DataInputBuffer in = new DataInputBuffer(); in.reset(data, data.length); info.readFields(in); } System.out.println(String.format("%25s %10s %10s %s", table, info.getState(), info.getOperation(), info.getExtent())); } } }