Author: nspiegelberg
Date: Sat Sep  3 02:55:29 2011
New Revision: 1164797

URL: http://svn.apache.org/viewvc?rev=1164797&view=rev
Log:
HBASE-1730 Online schema changes for HBase

Added:
    hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/BulkReOpen.java
    hbase/trunk/src/main/ruby/shell/commands/alter_async.rb
      - copied, changed from r1164762, 
hbase/trunk/src/main/ruby/shell/commands/alter.rb
    hbase/trunk/src/main/ruby/shell/commands/alter_status.rb
Modified:
    hbase/trunk/CHANGES.txt
    hbase/trunk/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java
    hbase/trunk/src/main/java/org/apache/hadoop/hbase/executor/EventHandler.java
    hbase/trunk/src/main/java/org/apache/hadoop/hbase/ipc/HMasterInterface.java
    
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java
    hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
    
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/ClosedRegionHandler.java
    
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableAddFamilyHandler.java
    
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableEventHandler.java
    hbase/trunk/src/main/ruby/hbase/admin.rb
    hbase/trunk/src/main/ruby/shell.rb
    hbase/trunk/src/main/ruby/shell/commands/alter.rb
    hbase/trunk/src/test/java/org/apache/hadoop/hbase/client/TestAdmin.java

Modified: hbase/trunk/CHANGES.txt
URL: 
http://svn.apache.org/viewvc/hbase/trunk/CHANGES.txt?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/CHANGES.txt (original)
+++ hbase/trunk/CHANGES.txt Sat Sep  3 02:55:29 2011
@@ -438,6 +438,7 @@ Release 0.91.0 - Unreleased
                (Lars Hofhansl)
    HBASE-4257  Limit the number of regions in transitions displayed on
                master webpage. (todd)
+   HBASE-1730  Online Schema Changes
 
   TASKS
    HBASE-3559  Move report of split to master OFF the heartbeat channel

Modified: 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java 
(original)
+++ hbase/trunk/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java 
Sat Sep  3 02:55:29 2011
@@ -847,6 +847,28 @@ public class HBaseAdmin implements Abort
   }
 
   /**
+   * Get the status of alter command - indicates how many regions have received
+   * the updated schema Asynchronous operation.
+   *
+   * @param tableName
+   *          name of the table to get the status of
+   * @return Pair indicating the number of regions updated Pair.getFirst() is 
the
+   *         regions that are yet to be updated Pair.getSecond() is the total 
number
+   *         of regions of the table
+   * @throws IOException
+   *           if a remote or network exception occurs
+   */
+  public Pair<Integer, Integer> getAlterStatus(final byte[] tableName)
+  throws IOException {
+    HTableDescriptor.isLegalTableName(tableName);
+    try {
+      return getMaster().getAlterStatus(tableName);
+    } catch (RemoteException e) {
+      throw RemoteExceptionHandler.decodeRemoteException(e);
+    }
+  }
+
+  /**
    * Add a column to an existing table.
    * Asynchronous operation.
    *

Modified: 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/executor/EventHandler.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/executor/EventHandler.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/executor/EventHandler.java 
(original)
+++ 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/executor/EventHandler.java 
Sat Sep  3 02:55:29 2011
@@ -140,6 +140,14 @@ public abstract class EventHandler imple
      * Constructor
      */
     EventType(int value) {}
+    public boolean isOnlineSchemaChangeSupported() {
+      return (
+        this.equals(EventType.C_M_ADD_FAMILY) ||
+        this.equals(EventType.C_M_DELETE_FAMILY) ||
+        this.equals(EventType.C_M_MODIFY_FAMILY) ||
+        this.equals(EventType.C_M_MODIFY_TABLE)
+      );
+    }
   }
 
   /**

Modified: 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/ipc/HMasterInterface.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/ipc/HMasterInterface.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/src/main/java/org/apache/hadoop/hbase/ipc/HMasterInterface.java 
(original)
+++ hbase/trunk/src/main/java/org/apache/hadoop/hbase/ipc/HMasterInterface.java 
Sat Sep  3 02:55:29 2011
@@ -26,6 +26,7 @@ import org.apache.hadoop.hbase.ClusterSt
 import org.apache.hadoop.hbase.HColumnDescriptor;
 import org.apache.hadoop.hbase.HTableDescriptor;
 import org.apache.hadoop.hbase.UnknownRegionException;
+import org.apache.hadoop.hbase.util.Pair;
 import org.apache.hadoop.hbase.ipc.VersionedProtocol;
 
 /**
@@ -72,6 +73,19 @@ public interface HMasterInterface extend
   public void deleteTable(final byte [] tableName) throws IOException;
 
   /**
+   * Used by the client to get the number of regions that have received the
+   * updated schema
+   *
+   * @param tableName
+   * @return Pair indicating the number of regions updated Pair.getFirst() is 
the
+   *         regions that are yet to be updated Pair.getSecond() is the total 
number
+   *         of regions of the table
+   * @throws IOException
+   */
+  public Pair<Integer, Integer> getAlterStatus(byte[] tableName)
+      throws IOException;
+
+  /**
    * Adds a column to the specified table
    * @param tableName table to modify
    * @param column column descriptor
@@ -222,4 +236,4 @@ public interface HMasterInterface extend
    */
   public HTableDescriptor[] getHTableDescriptors(List<String> tableNames);
 
-}
\ No newline at end of file
+}

Modified: 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java 
(original)
+++ 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/AssignmentManager.java 
Sat Sep  3 02:55:29 2011
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.lang.Thread.UncaughtExceptionHandler;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -104,6 +105,12 @@ public class AssignmentManager extends Z
 
   private LoadBalancer balancer;
 
+  /**
+   * Map of regions to reopen after the schema of a table is changed. Key -
+   * encoded region name, value - HRegionInfo
+   */
+  private final Map <String, HRegionInfo> regionsToReopen;
+
   /*
    * Maximum times we recurse an assignment.  See below in {@link #assign()}.
    */
@@ -165,6 +172,8 @@ public class AssignmentManager extends Z
     this.serverManager = serverManager;
     this.catalogTracker = catalogTracker;
     this.executorService = service;
+    this.regionsToReopen = Collections.synchronizedMap
+                           (new HashMap<String, HRegionInfo> ());
     Configuration conf = master.getConfiguration();
     this.timeoutMonitor = new TimeoutMonitor(
       conf.getInt("hbase.master.assignment.timeoutmonitor.period", 10000),
@@ -206,8 +215,61 @@ public class AssignmentManager extends Z
     // sharing.
     return this.zkTable;
   }
+  /**
+   * Returns the RegionServer to which hri is assigned.
+   *
+   * @param hri
+   *          HRegion for which this function returns the region server
+   * @return HServerInfo The region server to which hri belongs
+   */
+  public ServerName getRegionServerOfRegion(HRegionInfo hri) {
+    synchronized (this.regions ) {
+      return regions.get(hri);
+    }
+  }
+
+  /**
+   * Add a regionPlan for the specified region.
+   */
+  public void addPlan(String encodedName, RegionPlan plan) {
+    synchronized (regionPlans) {
+      regionPlans.put(encodedName, plan);
+    }
+  }
+
+  /**
+   * Set the list of regions that will be reopened
+   * because of an update in table schema
+   *
+   * @param regions
+   *          list of regions that should be tracked for reopen
+   */
+  public void setRegionsToReopen(List <HRegionInfo> regions) {
+    for(HRegionInfo hri : regions) {
+      regionsToReopen.put(hri.getEncodedName(), hri);
+    }
+  }
 
   /**
+   * Used by the client to identify if all regions have the schema updates
+   *
+   * @param tableName
+   * @return Pair indicating the status of the alter command
+   * @throws IOException
+   */
+  public Pair<Integer, Integer> getReopenStatus(byte[] tableName)
+      throws IOException {
+    List <HRegionInfo> hris = MetaReader.getTableRegions(
+                              this.master.getCatalogTracker(), tableName);
+    Integer pending = 0;
+    for(HRegionInfo hri : hris) {
+      if(regionsToReopen.get(hri.getEncodedName()) != null) {
+        pending++;
+      }
+    }
+    return new Pair<Integer, Integer>(pending, hris.size());
+  }
+  /**
    * Reset all unassigned znodes.  Called on startup of master.
    * Call {@link #assignAllUserRegions()} after root and meta have been 
assigned.
    * @throws IOException
@@ -467,6 +529,18 @@ public class AssignmentManager extends Z
   }
 
   /**
+   * When a region is closed, it should be removed from the regionsToReopen
+   * @param hri HRegionInfo of the region which was closed
+   */
+  public void removeClosedRegion(HRegionInfo hri) {
+    if (!regionsToReopen.isEmpty()) {
+      if (regionsToReopen.remove(hri.getEncodedName()) != null) {
+          LOG.debug("Removed region from reopening regions because it was 
closed");
+      }
+    }
+  }
+
+  /**
    * @param regionInfo
    * @param deadServers Map of deadServers and the regions they were carrying;
    * can be null.
@@ -601,6 +675,7 @@ public class AssignmentManager extends Z
           // what follows will fail because not in expected state.
           regionState.update(RegionState.State.CLOSED,
               data.getStamp(), data.getOrigin());
+         removeClosedRegion(regionState.getRegion());
           this.executorService.submit(new ClosedRegionHandler(master,
             this, regionState.getRegion()));
           break;
@@ -1390,6 +1465,11 @@ public class AssignmentManager extends Z
     RegionPlan existingPlan = null;
     synchronized (this.regionPlans) {
       existingPlan = this.regionPlans.get(encodedName);
+      if (existingPlan != null && existingPlan.getDestination() != null) {
+        LOG.debug("Found an existing plan for " +
+            state.getRegion().getRegionNameAsString() +
+       " destination server is + " + existingPlan.getDestination().toString());
+      }
       if (forceNewPlan || existingPlan == null 
               || existingPlan.getDestination() == null 
               || existingPlan.getDestination().equals(serverToExclude)) {
@@ -1412,6 +1492,37 @@ public class AssignmentManager extends Z
   }
 
   /**
+   * Unassign the list of regions. Configuration knobs:
+   * hbase.bulk.waitbetween.reopen indicates the number of milliseconds to
+   * wait before unassigning another region from this region server
+   *
+   * @param regions
+   * @throws InterruptedException
+   */
+  public void unassign(List<HRegionInfo> regions) {
+    int waitTime = this.master.getConfiguration().getInt(
+        "hbase.bulk.waitbetween.reopen", 0);
+    for (HRegionInfo region : regions) {
+      if (isRegionInTransition(region) != null)
+        continue;
+      unassign(region, false);
+      while (isRegionInTransition(region) != null) {
+        try {
+          Thread.sleep(10);
+        } catch (InterruptedException e) {
+          // Do nothing, continue
+        }
+      }
+      if (waitTime > 0)
+        try {
+          Thread.sleep(waitTime);
+        } catch (InterruptedException e) {
+          // Do nothing, continue
+        }
+    }
+  }
+
+  /**
    * Unassigns the specified region.
    * <p>
    * Updates the RegionState and sends the CLOSE RPC.

Added: hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/BulkReOpen.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/BulkReOpen.java?rev=1164797&view=auto
==============================================================================
--- hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/BulkReOpen.java 
(added)
+++ hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/BulkReOpen.java 
Sat Sep  3 02:55:29 2011
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2010 The Apache Software Foundation
+ *
+ * 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.hadoop.hbase.master;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.hbase.HRegionInfo;
+import org.apache.hadoop.hbase.Server;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.master.AssignmentManager;
+import org.apache.hadoop.hbase.master.BulkAssigner;
+import org.apache.hadoop.hbase.master.RegionPlan;
+import org.apache.commons.logging.Log;
+
+/**
+ * Performs bulk reopen of the list of regions provided to it.
+ */
+public class BulkReOpen extends BulkAssigner {
+  private final Map<ServerName, List<HRegionInfo>> rsToRegions;
+  private final AssignmentManager assignmentManager;
+  private static final Log LOG = LogFactory.getLog(BulkReOpen.class);
+
+  public BulkReOpen(final Server server,
+      final Map<ServerName, List<HRegionInfo>> serverToRegions,
+    final AssignmentManager am) {
+    super(server);
+    this.assignmentManager = am;
+    this.rsToRegions = serverToRegions;
+  }
+
+  /**
+   * Unassign all regions, so that they go through the regular region
+   * assignment flow (in assignment manager) and are re-opened.
+   */
+  @Override
+  protected void populatePool(ExecutorService pool) {
+    LOG.debug("Creating threads for each region server ");
+    for (Map.Entry<ServerName, List<HRegionInfo>> e : rsToRegions
+        .entrySet()) {
+      final List<HRegionInfo> hris = e.getValue();
+      // add a plan for each of the regions that needs to be reopened
+      for (HRegionInfo hri : hris) {
+        RegionPlan reOpenPlan = new RegionPlan(hri, null,
+            assignmentManager.getRegionServerOfRegion(hri));
+        assignmentManager.addPlan(hri.getEncodedName(), reOpenPlan);
+      }
+      pool.execute(new Runnable() {
+        public void run() {
+          assignmentManager.unassign(hris);
+        }
+      });
+    }
+  }
+
+ /**
+  * Reopen the regions asynchronously, so always returns true immediately.
+  * @return true
+  */
+  @Override
+  protected boolean waitUntilDone(long timeout) {
+    return true;
+  }
+
+  /**
+   * Configuration knobs "hbase.bulk.reopen.threadpool.size" number of regions
+   * that can be reopened concurrently. The maximum number of threads the 
master
+   * creates is never more than the number of region servers.
+   * If configuration is not defined it defaults to 20
+   */
+  protected int getThreadCount() {
+    int defaultThreadCount = super.getThreadCount();
+    return this.server.getConfiguration().getInt(
+        "hbase.bulk.reopen.threadpool.size", defaultThreadCount);
+  }
+
+  public boolean bulkReOpen() throws InterruptedException {
+    return bulkAssign();
+  }
+}

Modified: hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/HMaster.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/HMaster.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/HMaster.java 
(original)
+++ hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/HMaster.java Sat 
Sep  3 02:55:29 2011
@@ -991,6 +991,18 @@ implements HMasterInterface, HMasterRegi
     }
   }
 
+  /**
+   * Get the number of regions of the table that have been updated by the 
alter.
+   *
+   * @return Pair indicating the number of regions updated Pair.getFirst is the
+   *         regions that are yet to be updated Pair.getSecond is the total 
number
+   *         of regions of the table
+   */
+  public Pair<Integer, Integer> getAlterStatus(byte[] tableName)
+  throws IOException {
+    return this.assignmentManager.getReopenStatus(tableName);
+  }
+
   public void addColumn(byte [] tableName, HColumnDescriptor column)
   throws IOException {
     if (cpHost != null) {

Modified: 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/ClosedRegionHandler.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/ClosedRegionHandler.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/ClosedRegionHandler.java
 (original)
+++ 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/ClosedRegionHandler.java
 Sat Sep  3 02:55:29 2011
@@ -98,6 +98,7 @@ public class ClosedRegionHandler extends
     }
     // ZK Node is in CLOSED state, assign it.
     assignmentManager.setOffline(regionInfo);
+    assignmentManager.removeClosedRegion(regionInfo);
     assignmentManager.assign(regionInfo, true);
   }
 }

Modified: 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableAddFamilyHandler.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableAddFamilyHandler.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableAddFamilyHandler.java
 (original)
+++ 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableAddFamilyHandler.java
 Sat Sep  3 02:55:29 2011
@@ -27,7 +27,6 @@ import org.apache.hadoop.hbase.HRegionIn
 import org.apache.hadoop.hbase.HTableDescriptor;
 import org.apache.hadoop.hbase.InvalidFamilyOperationException;
 import org.apache.hadoop.hbase.Server;
-import org.apache.hadoop.hbase.master.AssignmentManager;
 import org.apache.hadoop.hbase.master.MasterServices;
 import org.apache.hadoop.hbase.util.Bytes;
 
@@ -47,8 +46,8 @@ public class TableAddFamilyHandler exten
   @Override
   protected void handleTableOperation(List<HRegionInfo> hris)
   throws IOException {
-    AssignmentManager am = this.masterServices.getAssignmentManager();
-    HTableDescriptor htd = 
this.masterServices.getTableDescriptors().get(Bytes.toString(tableName));
+    HTableDescriptor htd = this.masterServices.getTableDescriptors().
+                                      get(Bytes.toString(tableName));
     byte [] familyName = familyDesc.getName();
     if (htd == null) {
       throw new IOException("Add Family operation could not be completed as " +
@@ -76,7 +75,8 @@ public class TableAddFamilyHandler exten
     if(familyDesc != null) {
       family = familyDesc.getNameAsString();
     }
-    return getClass().getSimpleName() + "-" + name + "-" + getSeqid() + "-" + 
tableNameStr + "-" + family;
+    return getClass().getSimpleName() + "-" + name + "-" +
+                              getSeqid() + "-" + tableNameStr + "-" + family;
   }
 
 }

Modified: 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableEventHandler.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableEventHandler.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableEventHandler.java
 (original)
+++ 
hbase/trunk/src/main/java/org/apache/hadoop/hbase/master/handler/TableEventHandler.java
 Sat Sep  3 02:55:29 2011
@@ -20,18 +20,28 @@
 package org.apache.hadoop.hbase.master.handler;
 
 import java.io.IOException;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.NavigableMap;
+import java.util.TreeMap;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.hadoop.hbase.HRegionInfo;
 import org.apache.hadoop.hbase.Server;
+import org.apache.hadoop.hbase.ServerName;
+import org.apache.hadoop.hbase.TableNotDisabledException;
 import org.apache.hadoop.hbase.catalog.MetaReader;
+import org.apache.hadoop.hbase.client.HTable;
 import org.apache.hadoop.hbase.executor.EventHandler;
+import org.apache.hadoop.hbase.master.BulkReOpen;
 import org.apache.hadoop.hbase.master.MasterServices;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.zookeeper.KeeperException;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
 /**
  * Base class for performing operations against tables.
  * Checks on whether the process can go forward are done in constructor rather
@@ -51,7 +61,16 @@ public abstract class TableEventHandler 
     super(server, eventType);
     this.masterServices = masterServices;
     this.tableName = tableName;
-    this.masterServices.checkTableModifiable(tableName);
+    try {
+      this.masterServices.checkTableModifiable(tableName);
+    } catch (TableNotDisabledException ex)  {
+      if (eventType.isOnlineSchemaChangeSupported()) {
+        LOG.debug("Ignoring table not disabled exception " +
+            "for supporting online schema changes.");
+      }        else {
+        throw ex;
+      }
+    }
     this.tableNameStr = Bytes.toString(this.tableName);
   }
 
@@ -64,6 +83,17 @@ public abstract class TableEventHandler 
         MetaReader.getTableRegions(this.server.getCatalogTracker(),
           tableName);
       handleTableOperation(hris);
+      if (eventType.isOnlineSchemaChangeSupported() && this.masterServices.
+          getAssignmentManager().getZKTable().
+          isEnabledTable(Bytes.toString(tableName))) {
+        this.masterServices.getAssignmentManager().setRegionsToReopen(hris);
+        if (reOpenAllRegions(hris)) {
+          LOG.info("Completed table operation " + eventType + " on table " +
+              Bytes.toString(tableName));
+        } else {
+          LOG.warn("Error on reopening the regions");
+        }
+      }
     } catch (IOException e) {
       LOG.error("Error manipulating table " + Bytes.toString(tableName), e);
     } catch (KeeperException e) {
@@ -71,6 +101,43 @@ public abstract class TableEventHandler 
     }
   }
 
+  public boolean reOpenAllRegions(List<HRegionInfo> regions) throws 
IOException {
+    boolean done = false;
+    LOG.info("Bucketing regions by region server...");
+    HTable table = new HTable(masterServices.getConfiguration(), tableName);
+    TreeMap<ServerName, List<HRegionInfo>> serverToRegions = Maps
+        .newTreeMap();
+    NavigableMap<HRegionInfo, ServerName> hriHserverMapping = 
table.getRegionLocations();
+
+    for (HRegionInfo hri : regions) {
+      ServerName rsLocation = hriHserverMapping.get(hri);
+      if (!serverToRegions.containsKey(rsLocation)) {
+        LinkedList<HRegionInfo> hriList = Lists.newLinkedList();
+        serverToRegions.put(rsLocation, hriList);
+      }
+      serverToRegions.get(rsLocation).add(hri);
+    }
+    LOG.info("Reopening " + regions.size() + " regions on "
+        + serverToRegions.size() + " region servers.");
+    BulkReOpen bulkReopen = new BulkReOpen(this.server, serverToRegions,
+        this.masterServices.getAssignmentManager());
+    while (true) {
+      try {
+        if (bulkReopen.bulkReOpen()) {
+          done = true;
+          break;
+        } else {
+          LOG.warn("Timeout before reopening all regions");
+        }
+      } catch (InterruptedException e) {
+        LOG.warn("Reopen was interrupted");
+        // Preserve the interrupt.
+        Thread.currentThread().interrupt();
+        break;
+      }
+    }
+    return done;
+  }
   protected abstract void handleTableOperation(List<HRegionInfo> regions)
   throws IOException, KeeperException;
-}
\ No newline at end of file
+}

Modified: hbase/trunk/src/main/ruby/hbase/admin.rb
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/ruby/hbase/admin.rb?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/src/main/ruby/hbase/admin.rb (original)
+++ hbase/trunk/src/main/ruby/hbase/admin.rb Sat Sep  3 02:55:29 2011
@@ -19,6 +19,7 @@
 #
 
 include Java
+java_import org.apache.hadoop.hbase.util.Pair
 
 # Wrapper for org.apache.hadoop.hbase.client.HBaseAdmin
 
@@ -270,16 +271,31 @@ module Hbase
     end
 
     
#----------------------------------------------------------------------------------------------
-    # Change table structure or table options
-    def alter(table_name, *args)
+    # Check the status of alter command (number of regions reopened)
+    def alter_status(table_name)
       # Table name should be a string
       raise(ArgumentError, "Table name must be of type String") unless 
table_name.kind_of?(String)
 
       # Table should exist
       raise(ArgumentError, "Can't find a table: #{table_name}") unless 
exists?(table_name)
 
-      # Table should be disabled
-      raise(ArgumentError, "Table #{table_name} is enabled. Disable it first 
before altering.") if enabled?(table_name)
+      status = Pair.new()
+      begin
+        status = @admin.getAlterStatus(table_name.to_java_bytes)
+        puts "#{status.getSecond() - status.getFirst()}/#{status.getSecond()} 
regions updated."
+             sleep 1
+      end while status != nil && status.getFirst() != 0
+      puts "Done."
+    end
+
+    
#----------------------------------------------------------------------------------------------
+    # Change table structure or table options
+    def alter(table_name, wait = true, *args)
+      # Table name should be a string
+      raise(ArgumentError, "Table name must be of type String") unless 
table_name.kind_of?(String)
+
+      # Table should exist
+      raise(ArgumentError, "Can't find a table: #{table_name}") unless 
exists?(table_name)
 
       # There should be at least one argument
       raise(ArgumentError, "There should be at least one argument but the 
table name") if args.empty?
@@ -306,8 +322,16 @@ module Hbase
           # If column already exist, then try to alter it. Create otherwise.
           if htd.hasFamily(column_name.to_java_bytes)
             @admin.modifyColumn(table_name, column_name, descriptor)
+            if wait == true
+              puts "Updating all regions with the new schema..."
+              alter_status(table_name)
+            end
           else
             @admin.addColumn(table_name, descriptor)
+            if wait == true
+              puts "Updating all regions with the new schema..."
+              alter_status(table_name)
+            end
           end
           next
         end
@@ -316,6 +340,10 @@ module Hbase
         if method == "delete"
           raise(ArgumentError, "NAME parameter missing for delete method") 
unless arg[NAME]
           @admin.deleteColumn(table_name, arg[NAME])
+          if wait == true
+            puts "Updating all regions with the new schema..."
+            alter_status(table_name)
+          end
           next
         end
 
@@ -326,6 +354,10 @@ module Hbase
           htd.setMemStoreFlushSize(JLong.valueOf(arg[MEMSTORE_FLUSHSIZE])) if 
arg[MEMSTORE_FLUSHSIZE]
           htd.setDeferredLogFlush(JBoolean.valueOf(arg[DEFERRED_LOG_FLUSH])) 
if arg[DEFERRED_LOG_FLUSH]
           @admin.modifyTable(table_name.to_java_bytes, htd)
+          if wait == true
+            puts "Updating all regions with the new schema..."
+            alter_status(table_name)
+          end
           next
         end
 

Modified: hbase/trunk/src/main/ruby/shell.rb
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/ruby/shell.rb?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/src/main/ruby/shell.rb (original)
+++ hbase/trunk/src/main/ruby/shell.rb Sat Sep  3 02:55:29 2011
@@ -228,6 +228,8 @@ Shell.load_command_group(
     exists
     list
     show_filters
+    alter_status
+    alter_async
   ]
 )
 

Modified: hbase/trunk/src/main/ruby/shell/commands/alter.rb
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/ruby/shell/commands/alter.rb?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/src/main/ruby/shell/commands/alter.rb (original)
+++ hbase/trunk/src/main/ruby/shell/commands/alter.rb Sat Sep  3 02:55:29 2011
@@ -56,7 +56,7 @@ EOF
 
       def command(table, *args)
         format_simple_command do
-          admin.alter(table, *args)
+          admin.alter(table, true, *args)
         end
       end
     end

Copied: hbase/trunk/src/main/ruby/shell/commands/alter_async.rb (from r1164762, 
hbase/trunk/src/main/ruby/shell/commands/alter.rb)
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/ruby/shell/commands/alter_async.rb?p2=hbase/trunk/src/main/ruby/shell/commands/alter_async.rb&p1=hbase/trunk/src/main/ruby/shell/commands/alter.rb&r1=1164762&r2=1164797&rev=1164797&view=diff
==============================================================================
--- hbase/trunk/src/main/ruby/shell/commands/alter.rb (original)
+++ hbase/trunk/src/main/ruby/shell/commands/alter_async.rb Sat Sep  3 02:55:29 
2011
@@ -20,26 +20,26 @@
 
 module Shell
   module Commands
-    class Alter < Command
+    class AlterAsync < Command
       def help
         return <<-EOF
-Alter column family schema;  pass table name and a dictionary
-specifying new column family schema. Dictionaries are described
-on the main help command output. Dictionary must include name
-of column family to alter. For example,
+Alter column family schema, does not wait for all regions to receive the
+schema changes. Pass table name and a dictionary specifying new column
+family schema. Dictionaries are described on the main help command output.
+Dictionary must include name of column family to alter. For example,
 
 To change or add the 'f1' column family in table 't1' from defaults
 to instead keep a maximum of 5 cell VERSIONS, do:
 
-  hbase> alter 't1', NAME => 'f1', VERSIONS => 5
+  hbase> alter_async 't1', NAME => 'f1', VERSIONS => 5
 
 To delete the 'f1' column family in table 't1', do:
 
-  hbase> alter 't1', NAME => 'f1', METHOD => 'delete'
+  hbase> alter_async 't1', NAME => 'f1', METHOD => 'delete'
 
 or a shorter version:
 
-  hbase> alter 't1', 'delete' => 'f1'
+  hbase> alter_async 't1', 'delete' => 'f1'
 
 You can also change table-scope attributes like MAX_FILESIZE
 MEMSTORE_FLUSHSIZE, READONLY, and DEFERRED_LOG_FLUSH.
@@ -51,12 +51,14 @@ For example, to change the max size of a
 There could be more than one alteration in one command:
 
   hbase> alter 't1', {NAME => 'f1'}, {NAME => 'f2', METHOD => 'delete'}
+
+To check if all the regions have been updated, use alter_status <table_name>
 EOF
       end
 
       def command(table, *args)
         format_simple_command do
-          admin.alter(table, *args)
+          admin.alter(table, false, *args)
         end
       end
     end

Added: hbase/trunk/src/main/ruby/shell/commands/alter_status.rb
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/main/ruby/shell/commands/alter_status.rb?rev=1164797&view=auto
==============================================================================
--- hbase/trunk/src/main/ruby/shell/commands/alter_status.rb (added)
+++ hbase/trunk/src/main/ruby/shell/commands/alter_status.rb Sat Sep  3 
02:55:29 2011
@@ -0,0 +1,38 @@
+#
+# Copyright 2010 The Apache Software Foundation
+#
+# 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.
+#
+
+module Shell
+  module Commands
+    class AlterStatus < Command
+      def help
+        return <<-EOF
+Get the status of the alter command. Indicates the number of regions of the
+table that have received the updated schema
+Pass table name.
+
+hbase> alter_status 't1'
+EOF
+      end
+      def command(table)
+        admin.alter_status(table)
+      end
+    end
+  end
+end

Modified: 
hbase/trunk/src/test/java/org/apache/hadoop/hbase/client/TestAdmin.java
URL: 
http://svn.apache.org/viewvc/hbase/trunk/src/test/java/org/apache/hadoop/hbase/client/TestAdmin.java?rev=1164797&r1=1164796&r2=1164797&view=diff
==============================================================================
--- hbase/trunk/src/test/java/org/apache/hadoop/hbase/client/TestAdmin.java 
(original)
+++ hbase/trunk/src/test/java/org/apache/hadoop/hbase/client/TestAdmin.java Sat 
Sep  3 02:55:29 2011
@@ -42,8 +42,10 @@ import org.apache.hadoop.hbase.HColumnDe
 import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.HRegionInfo;
 import org.apache.hadoop.hbase.HServerAddress;
+import org.apache.hadoop.hbase.HServerInfo;
 import org.apache.hadoop.hbase.HTableDescriptor;
 import org.apache.hadoop.hbase.NotServingRegionException;
+import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.TableExistsException;
 import org.apache.hadoop.hbase.TableNotDisabledException;
 import org.apache.hadoop.hbase.TableNotEnabledException;
@@ -51,6 +53,7 @@ import org.apache.hadoop.hbase.TableNotF
 import org.apache.hadoop.hbase.executor.EventHandler;
 import org.apache.hadoop.hbase.executor.EventHandler.EventType;
 import org.apache.hadoop.hbase.executor.ExecutorService;
+import org.apache.hadoop.hbase.ipc.HRegionInterface;
 import org.apache.hadoop.hbase.master.MasterServices;
 import org.apache.hadoop.hbase.regionserver.HRegionServer;
 import org.apache.hadoop.hbase.util.Bytes;
@@ -217,11 +220,13 @@ public class TestAdmin {
   /**
    * Verify schema modification takes.
    * @throws IOException
+   * @throws InterruptedException
    */
-  @Test
-  public void testChangeTableSchema() throws IOException {
-    final byte [] tableName = Bytes.toBytes("changeTableSchema");
+  @Test 
+  public void testOnlineChangeTableSchema() throws IOException, 
InterruptedException {
+    final byte [] tableName = Bytes.toBytes("changeTableSchemaOnline");
     HTableDescriptor [] tables = admin.listTables();
+    MasterServices masterServices = 
TEST_UTIL.getMiniHBaseCluster().getMaster();
     int numTables = tables.length;
     TEST_UTIL.createTable(tableName, HConstants.CATALOG_FAMILY);
     tables = this.admin.listTables();
@@ -240,14 +245,11 @@ public class TestAdmin {
     copy.setValue(key, key);
     boolean expectedException = false;
     try {
-      this.admin.modifyTable(tableName, copy);
+      modifyTable(tableName, copy);
     } catch (TableNotDisabledException re) {
       expectedException = true;
     }
-    assertTrue(expectedException);
-    this.admin.disableTable(tableName);
-    assertTrue(this.admin.isTableDisabled(tableName));
-    modifyTable(tableName, copy);
+    assertFalse(expectedException);
     HTableDescriptor modifiedHtd = this.admin.getTableDescriptor(tableName);
     // Assert returned modifiedhcd is same as the copy.
     assertFalse(htd.equals(modifiedHtd));
@@ -255,11 +257,8 @@ public class TestAdmin {
     assertEquals(newFlushSize, modifiedHtd.getMemStoreFlushSize());
     assertEquals(key, modifiedHtd.getValue(key));
 
-    // Reenable table to test it fails if not disabled.
-    this.admin.enableTable(tableName);
-    assertFalse(this.admin.isTableDisabled(tableName));
-
     // Now work on column family changes.
+    htd = this.admin.getTableDescriptor(tableName);
     int countOfFamilies = modifiedHtd.getFamilies().size();
     assertTrue(countOfFamilies > 0);
     HColumnDescriptor hcd = modifiedHtd.getFamilies().iterator().next();
@@ -273,31 +272,25 @@ public class TestAdmin {
     } catch (TableNotDisabledException re) {
       expectedException = true;
     }
-    assertTrue(expectedException);
-    this.admin.disableTable(tableName);
-    assertTrue(this.admin.isTableDisabled(tableName));
-    // Modify Column is synchronous
-    this.admin.modifyColumn(tableName, hcd);
+    assertFalse(expectedException);
     modifiedHtd = this.admin.getTableDescriptor(tableName);
     HColumnDescriptor modifiedHcd = modifiedHtd.getFamily(hcdName);
     assertEquals(newMaxVersions, modifiedHcd.getMaxVersions());
 
     // Try adding a column
-    // Reenable table to test it fails if not disabled.
-    this.admin.enableTable(tableName);
     assertFalse(this.admin.isTableDisabled(tableName));
     final String xtracolName = "xtracol";
+    htd = this.admin.getTableDescriptor(tableName);
     HColumnDescriptor xtracol = new HColumnDescriptor(xtracolName);
     xtracol.setValue(xtracolName, xtracolName);
+    expectedException = false;
     try {
       this.admin.addColumn(tableName, xtracol);
     } catch (TableNotDisabledException re) {
       expectedException = true;
     }
-    assertTrue(expectedException);
-    this.admin.disableTable(tableName);
-    assertTrue(this.admin.isTableDisabled(tableName));
-    this.admin.addColumn(tableName, xtracol);
+    // Add column should work even if the table is enabled
+    assertFalse(expectedException);
     modifiedHtd = this.admin.getTableDescriptor(tableName);
     hcd = modifiedHtd.getFamily(xtracol.getName());
     assertTrue(hcd != null);
@@ -310,6 +303,7 @@ public class TestAdmin {
     assertTrue(hcd == null);
 
     // Delete the table
+    this.admin.disableTable(tableName);
     this.admin.deleteTable(tableName);
     this.admin.listTables();
     assertFalse(this.admin.tableExists(tableName));


Reply via email to