You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@accumulo.apache.org by jm...@apache.org on 2023/05/05 15:35:24 UTC

[accumulo] branch elasticity updated: Allow setting of initial hosting goal with NewTableConfiguration (#3379)

This is an automated email from the ASF dual-hosted git repository.

jmark99 pushed a commit to branch elasticity
in repository https://gitbox.apache.org/repos/asf/accumulo.git


The following commit(s) were added to refs/heads/elasticity by this push:
     new 6559f8c694 Allow setting of initial hosting goal with NewTableConfiguration (#3379)
6559f8c694 is described below

commit 6559f8c6940904907bdb60758ac54b4faa4b4bdf
Author: Mark Owens <jm...@apache.org>
AuthorDate: Fri May 5 11:35:18 2023 -0400

    Allow setting of initial hosting goal with NewTableConfiguration (#3379)
    
    Modified NewTableConfiguration to support users setting initial hosting goals for tables at time of table creation.
    
    Updates allow for setting goals via API and via the Accumulo shell.
    
    CreateTable fate operation was updated to set initial goal upon creation of tables. If splits are set at table creation, each tablet will have the appropriate hosting goal set for the tablet.
    
    Unit and IT tests were created to verify the changes.
---
 .../core/client/admin/NewTableConfiguration.java   | 10 +++
 .../core/clientImpl/TableOperationsImpl.java       |  2 +
 .../client/admin/NewTableConfigurationTest.java    | 19 +++++
 .../accumulo/manager/FateServiceHandler.java       | 11 ++-
 .../accumulo/manager/tableOps/TableInfo.java       | 11 +++
 .../manager/tableOps/create/CreateTable.java       |  6 +-
 .../manager/tableOps/create/PopulateMetadata.java  | 21 ++++++
 .../manager/tableOps/goal/SetHostingGoal.java      |  5 +-
 .../shell/commands/CreateTableCommand.java         | 24 ++++++
 .../accumulo/test/NewTableConfigurationIT.java     | 87 ++++++++++++++++++++++
 .../accumulo/test/shell/ShellCreateTableIT.java    | 60 +++++++++++++++
 11 files changed, 248 insertions(+), 8 deletions(-)

diff --git a/core/src/main/java/org/apache/accumulo/core/client/admin/NewTableConfiguration.java b/core/src/main/java/org/apache/accumulo/core/client/admin/NewTableConfiguration.java
index fc6ba06c3f..f4412d42f1 100644
--- a/core/src/main/java/org/apache/accumulo/core/client/admin/NewTableConfiguration.java
+++ b/core/src/main/java/org/apache/accumulo/core/client/admin/NewTableConfiguration.java
@@ -72,6 +72,7 @@ public class NewTableConfiguration {
   private Map<String,String> localityProps = Collections.emptyMap();
   private final Map<String,String> iteratorProps = new HashMap<>();
   private SortedSet<Text> splitProps = Collections.emptySortedSet();
+  private TabletHostingGoal initialHostingGoal = TabletHostingGoal.ONDEMAND;
 
   private void checkDisjoint(Map<String,String> props, Map<String,String> derivedProps,
       String kind) {
@@ -309,6 +310,15 @@ public class NewTableConfiguration {
     return this;
   }
 
+  public NewTableConfiguration withInitialHostingGoal(final TabletHostingGoal goal) {
+    this.initialHostingGoal = goal;
+    return this;
+  }
+
+  public TabletHostingGoal getInitialHostingGoal() {
+    return this.initialHostingGoal;
+  }
+
   /**
    * Verify the provided properties are valid table properties.
    */
diff --git a/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java b/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java
index a813325020..1338510053 100644
--- a/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java
+++ b/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java
@@ -246,6 +246,8 @@ public class TableOperationsImpl extends TableOperationsHelper {
     args.add(ByteBuffer.wrap(ntc.getTimeType().name().getBytes(UTF_8)));
     // Send info relating to initial table creation i.e, create online or offline
     args.add(ByteBuffer.wrap(ntc.getInitialTableState().name().getBytes(UTF_8)));
+    // send initialHostingGoal information
+    args.add(ByteBuffer.wrap(ntc.getInitialHostingGoal().name().getBytes(UTF_8)));
     // Check for possible initial splits to be added at table creation
     // Always send number of initial splits to be created, even if zero. If greater than zero,
     // add the splits to the argument List which will be used by the FATE operations.
diff --git a/core/src/test/java/org/apache/accumulo/core/client/admin/NewTableConfigurationTest.java b/core/src/test/java/org/apache/accumulo/core/client/admin/NewTableConfigurationTest.java
index 89930fb37a..15695f75c2 100644
--- a/core/src/test/java/org/apache/accumulo/core/client/admin/NewTableConfigurationTest.java
+++ b/core/src/test/java/org/apache/accumulo/core/client/admin/NewTableConfigurationTest.java
@@ -103,6 +103,25 @@ public class NewTableConfigurationTest {
 
   }
 
+  @Test
+  public void testWithAndGetInitialHostingGoals() {
+    NewTableConfiguration ntc = new NewTableConfiguration();
+    TabletHostingGoal initialHostingGoal = ntc.getInitialHostingGoal();
+    assertEquals(TabletHostingGoal.ONDEMAND, initialHostingGoal);
+
+    ntc = new NewTableConfiguration().withInitialHostingGoal(TabletHostingGoal.ONDEMAND);
+    initialHostingGoal = ntc.getInitialHostingGoal();
+    assertEquals(TabletHostingGoal.ONDEMAND, initialHostingGoal);
+
+    ntc = new NewTableConfiguration().withInitialHostingGoal(TabletHostingGoal.ALWAYS);
+    initialHostingGoal = ntc.getInitialHostingGoal();
+    assertEquals(TabletHostingGoal.ALWAYS, initialHostingGoal);
+
+    ntc = new NewTableConfiguration().withInitialHostingGoal(TabletHostingGoal.NEVER);
+    initialHostingGoal = ntc.getInitialHostingGoal();
+    assertEquals(TabletHostingGoal.NEVER, initialHostingGoal);
+  }
+
   /**
    * Verify that createOffline option
    */
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java b/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java
index b764ac1417..785c056874 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java
@@ -179,7 +179,7 @@ class FateServiceHandler implements FateService.Iface {
       }
       case TABLE_CREATE: {
         TableOperation tableOp = TableOperation.CREATE;
-        int SPLIT_OFFSET = 4; // offset where split data begins in arguments list
+        int SPLIT_OFFSET = 5; // offset where split data begins in arguments list
         if (arguments.size() < SPLIT_OFFSET) {
           throw new ThriftTableOperationException(null, null, tableOp,
               TableOperationExceptionType.OTHER,
@@ -190,7 +190,9 @@ class FateServiceHandler implements FateService.Iface {
         TimeType timeType = TimeType.valueOf(ByteBufferUtil.toString(arguments.get(1)));
         InitialTableState initialTableState =
             InitialTableState.valueOf(ByteBufferUtil.toString(arguments.get(2)));
-        int splitCount = Integer.parseInt(ByteBufferUtil.toString(arguments.get(3)));
+        TabletHostingGoal initialHostingGoal =
+            TabletHostingGoal.valueOf(ByteBufferUtil.toString(arguments.get(3)));
+        int splitCount = Integer.parseInt(ByteBufferUtil.toString(arguments.get(4)));
         validateArgumentCount(arguments, tableOp, SPLIT_OFFSET + splitCount);
         Path splitsPath = null;
         Path splitsDirsPath = null;
@@ -232,11 +234,12 @@ class FateServiceHandler implements FateService.Iface {
         }
 
         goalMessage += "Create table " + tableName + " " + initialTableState + " with " + splitCount
-            + " splits.";
+            + " splits and initial hosting goal of " + initialHostingGoal;
 
         manager.fate().seedTransaction(op.toString(), opid,
             new TraceRepo<>(new CreateTable(c.getPrincipal(), tableName, timeType, options,
-                splitsPath, splitCount, splitsDirsPath, initialTableState, namespaceId)),
+                splitsPath, splitCount, splitsDirsPath, initialTableState, initialHostingGoal,
+                namespaceId)),
             autoCleanup, goalMessage);
 
         break;
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/TableInfo.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/TableInfo.java
index 4abb9452fb..af04d50172 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/TableInfo.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/TableInfo.java
@@ -22,6 +22,7 @@ import java.io.Serializable;
 import java.util.Map;
 
 import org.apache.accumulo.core.client.admin.InitialTableState;
+import org.apache.accumulo.core.client.admin.TabletHostingGoal;
 import org.apache.accumulo.core.client.admin.TimeType;
 import org.apache.accumulo.core.data.NamespaceId;
 import org.apache.accumulo.core.data.TableId;
@@ -48,6 +49,16 @@ public class TableInfo implements Serializable {
 
   public Map<String,String> props;
 
+  private TabletHostingGoal initialHostingGoal;
+
+  public TabletHostingGoal getInitialHostingGoal() {
+    return initialHostingGoal;
+  }
+
+  public void setInitialHostingGoal(TabletHostingGoal initialHostingGoal) {
+    this.initialHostingGoal = initialHostingGoal;
+  }
+
   public String getTableName() {
     return tableName;
   }
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/CreateTable.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/CreateTable.java
index f1f71a8642..bfbb5a9287 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/CreateTable.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/CreateTable.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.util.Map;
 
 import org.apache.accumulo.core.client.admin.InitialTableState;
+import org.apache.accumulo.core.client.admin.TabletHostingGoal;
 import org.apache.accumulo.core.client.admin.TimeType;
 import org.apache.accumulo.core.clientImpl.thrift.TableOperation;
 import org.apache.accumulo.core.data.NamespaceId;
@@ -40,11 +41,11 @@ public class CreateTable extends ManagerRepo {
   private static final long serialVersionUID = 1L;
   private static final Logger log = LoggerFactory.getLogger(CreateTable.class);
 
-  private TableInfo tableInfo;
+  private final TableInfo tableInfo;
 
   public CreateTable(String user, String tableName, TimeType timeType, Map<String,String> props,
       Path splitPath, int splitCount, Path splitDirsPath, InitialTableState initialTableState,
-      NamespaceId namespaceId) {
+      TabletHostingGoal initialHostingGoal, NamespaceId namespaceId) {
     tableInfo = new TableInfo();
     tableInfo.setTableName(tableName);
     tableInfo.setTimeType(timeType);
@@ -55,6 +56,7 @@ public class CreateTable extends ManagerRepo {
     tableInfo.setInitialSplitSize(splitCount);
     tableInfo.setInitialTableState(initialTableState);
     tableInfo.setSplitDirsPath(splitDirsPath);
+    tableInfo.setInitialHostingGoal(initialHostingGoal);
   }
 
   @Override
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateMetadata.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateMetadata.java
index 8a3363e7b0..b2275aec50 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateMetadata.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateMetadata.java
@@ -26,6 +26,7 @@ import java.util.stream.Stream;
 
 import org.apache.accumulo.core.client.BatchWriter;
 import org.apache.accumulo.core.client.MutationsRejectedException;
+import org.apache.accumulo.core.client.admin.TabletHostingGoal;
 import org.apache.accumulo.core.client.admin.TimeType;
 import org.apache.accumulo.core.data.Mutation;
 import org.apache.accumulo.core.data.TableId;
@@ -34,9 +35,12 @@ import org.apache.accumulo.core.dataImpl.KeyExtent;
 import org.apache.accumulo.core.fate.Repo;
 import org.apache.accumulo.core.lock.ServiceLock;
 import org.apache.accumulo.core.metadata.MetadataTable;
+import org.apache.accumulo.core.metadata.schema.Ample;
 import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.ServerColumnFamily;
 import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.TabletColumnFamily;
 import org.apache.accumulo.core.metadata.schema.MetadataTime;
+import org.apache.accumulo.core.metadata.schema.TabletMetadata;
+import org.apache.accumulo.core.metadata.schema.TabletsMetadata;
 import org.apache.accumulo.manager.Manager;
 import org.apache.accumulo.manager.tableOps.ManagerRepo;
 import org.apache.accumulo.manager.tableOps.TableInfo;
@@ -77,9 +81,26 @@ class PopulateMetadata extends ManagerRepo {
             tableInfo.getTimeType(), env.getManagerLock(), bw);
       }
     }
+
+    setInitialHostingGoal(env.getContext(), tableInfo.getTableId(),
+        tableInfo.getInitialHostingGoal());
+
     return new FinishCreateTable(tableInfo);
   }
 
+  private void setInitialHostingGoal(ServerContext context, TableId tableId,
+      TabletHostingGoal goal) {
+    try (TabletsMetadata m = context.getAmple().readTablets().forTable(tableId)
+        .overlapping((byte[]) null, null).build()) {
+      try (Ample.TabletsMutator mutator = context.getAmple().mutateTablets()) {
+        for (TabletMetadata tm : m) {
+          final KeyExtent tabletExtent = tm.getExtent();
+          mutator.mutateTablet(tabletExtent).setHostingGoal(goal).mutate();
+        }
+      }
+    }
+  }
+
   private void writeSplitsToMetadataTable(ServerContext context, TableId tableId,
       SortedSet<Text> splits, Map<Text,Text> data, TimeType timeType, ServiceLock lock,
       BatchWriter bw) throws MutationsRejectedException {
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/goal/SetHostingGoal.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/goal/SetHostingGoal.java
index b878f8b8b6..25351060b7 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/goal/SetHostingGoal.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/goal/SetHostingGoal.java
@@ -69,9 +69,10 @@ public class SetHostingGoal extends ManagerRepo {
     final Range range = new Range(tRange);
     LOG.debug("Finding tablets in Range: {} for table:{}", range, tableId);
 
-    // For all practical purposes the the start row is always inclusive, even if the key in the
+    // For all practical purposes the start row is always inclusive, even if the key in the
     // range is exclusive. For example the exclusive key row="a",family="b",qualifier="c" may
-    // exclude the column b:c but its still falls somewhere in the row "a". The only case where this
+    // exclude the column b:c, but it's still falls somewhere in the row "a". The only case where
+    // this
     // would not be true is if the start key in a range is the last possible key in a row. The last
     // possible key in a row would contain 2GB column fields of all 0xff, which is why we assume the
     // row is always inclusive.
diff --git a/shell/src/main/java/org/apache/accumulo/shell/commands/CreateTableCommand.java b/shell/src/main/java/org/apache/accumulo/shell/commands/CreateTableCommand.java
index 41d72f497e..f58be58147 100644
--- a/shell/src/main/java/org/apache/accumulo/shell/commands/CreateTableCommand.java
+++ b/shell/src/main/java/org/apache/accumulo/shell/commands/CreateTableCommand.java
@@ -36,6 +36,7 @@ import org.apache.accumulo.core.client.IteratorSetting;
 import org.apache.accumulo.core.client.TableExistsException;
 import org.apache.accumulo.core.client.TableNotFoundException;
 import org.apache.accumulo.core.client.admin.NewTableConfiguration;
+import org.apache.accumulo.core.client.admin.TabletHostingGoal;
 import org.apache.accumulo.core.client.admin.TimeType;
 import org.apache.accumulo.core.conf.Property;
 import org.apache.accumulo.core.data.constraints.VisibilityConstraint;
@@ -65,6 +66,7 @@ public class CreateTableCommand extends Command {
   private Option createTableOptLocalityProps;
   private Option createTableOptIteratorProps;
   private Option createTableOptOffline;
+  private Option createTableOptInitialHostingGoal;
 
   @Override
   public int execute(final String fullCommand, final CommandLine cl, final Shell shellState)
@@ -104,6 +106,24 @@ public class CreateTableCommand extends Command {
       }
     }
 
+    // set initial hosting goal, if argument supplied.
+    // CreateTable will default to ONDEMAND if argument not supplied
+    if (cl.hasOption(createTableOptInitialHostingGoal.getOpt())) {
+      String goal = cl.getOptionValue(createTableOptInitialHostingGoal.getOpt()).toUpperCase();
+      TabletHostingGoal initialHostingGoal;
+      switch (goal) {
+        case "ALWAYS":
+          initialHostingGoal = TabletHostingGoal.ALWAYS;
+          break;
+        case "NEVER":
+          initialHostingGoal = TabletHostingGoal.NEVER;
+          break;
+        default:
+          initialHostingGoal = TabletHostingGoal.ONDEMAND;
+      }
+      ntc = ntc.withInitialHostingGoal(initialHostingGoal);
+    }
+
     TimeType timeType = TimeType.MILLIS;
     if (cl.hasOption(createTableOptTimeLogical.getOpt())) {
       timeType = TimeType.LOGICAL;
@@ -309,11 +329,14 @@ public class CreateTableCommand extends Command {
     createTableOptFormatter = new Option("f", "formatter", true, "default formatter to set");
     createTableOptInitProp =
         new Option("prop", "init-properties", true, "user defined initial properties");
+    createTableOptInitialHostingGoal =
+        new Option("g", "goal", true, "initial hosting goal (defaults to ONDEMAND)");
     createTableOptCopyConfig.setArgName("table");
     createTableOptCopySplits.setArgName("table");
     createTableOptSplit.setArgName("filename");
     createTableOptFormatter.setArgName("className");
     createTableOptInitProp.setArgName("properties");
+    createTableOptInitialHostingGoal.setArgName("goal");
 
     createTableOptLocalityProps =
         new Option("l", "locality", true, "create locality groups at table creation");
@@ -352,6 +375,7 @@ public class CreateTableCommand extends Command {
     o.addOption(createTableOptLocalityProps);
     o.addOption(createTableOptIteratorProps);
     o.addOption(createTableOptOffline);
+    o.addOption(createTableOptInitialHostingGoal);
 
     return o;
   }
diff --git a/test/src/main/java/org/apache/accumulo/test/NewTableConfigurationIT.java b/test/src/main/java/org/apache/accumulo/test/NewTableConfigurationIT.java
index ae4e1a5685..393fe440e0 100644
--- a/test/src/main/java/org/apache/accumulo/test/NewTableConfigurationIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/NewTableConfigurationIT.java
@@ -28,18 +28,26 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.SortedSet;
 import java.util.TreeMap;
+import java.util.TreeSet;
 
 import org.apache.accumulo.core.client.Accumulo;
 import org.apache.accumulo.core.client.AccumuloClient;
 import org.apache.accumulo.core.client.AccumuloException;
 import org.apache.accumulo.core.client.AccumuloSecurityException;
 import org.apache.accumulo.core.client.IteratorSetting;
+import org.apache.accumulo.core.client.Scanner;
 import org.apache.accumulo.core.client.TableExistsException;
 import org.apache.accumulo.core.client.TableNotFoundException;
 import org.apache.accumulo.core.client.admin.NewTableConfiguration;
+import org.apache.accumulo.core.client.admin.TabletHostingGoal;
 import org.apache.accumulo.core.conf.Property;
+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.iterators.IteratorUtil.IteratorScope;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.HostingColumnFamily;
 import org.apache.accumulo.harness.SharedMiniClusterBase;
 import org.apache.hadoop.io.Text;
 import org.junit.jupiter.api.AfterAll;
@@ -93,6 +101,85 @@ public class NewTableConfigurationIT extends SharedMiniClusterBase {
     }
   }
 
+  @Test
+  public void testCreateTableWithInitialHostingGoal() throws AccumuloException,
+      AccumuloSecurityException, TableNotFoundException, TableExistsException {
+    try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) {
+
+      String[] tableNames = getUniqueNames(8);
+
+      // use a default NewTableConfiguration
+      verifyNtcWithGoal(client, tableNames[0], null, null);
+      // set initial goals for tables upon creation, without splits
+      verifyNtcWithGoal(client, tableNames[1], TabletHostingGoal.ONDEMAND, null);
+      verifyNtcWithGoal(client, tableNames[2], TabletHostingGoal.ALWAYS, null);
+      verifyNtcWithGoal(client, tableNames[3], TabletHostingGoal.NEVER, null);
+
+      SortedSet<Text> splits = new TreeSet<>();
+      splits.add(new Text("d"));
+      splits.add(new Text("h"));
+      splits.add(new Text("m"));
+      splits.add(new Text("r"));
+      splits.add(new Text("w"));
+
+      // Use NTC to set initial splits. Verify each tablet has hosting goal set.
+      // Should work with no goal explicitly supplied as well as each of the accepted goals
+      verifyNtcWithGoal(client, tableNames[4], null, splits);
+      verifyNtcWithGoal(client, tableNames[5], TabletHostingGoal.ONDEMAND, splits);
+      verifyNtcWithGoal(client, tableNames[6], TabletHostingGoal.ALWAYS, splits);
+      verifyNtcWithGoal(client, tableNames[7], TabletHostingGoal.NEVER, splits);
+    }
+  }
+
+  // Verify that NewTableConfiguration correctly sets the initial hosting goal on a table, both with
+  // and without initial splits being set.
+  private void verifyNtcWithGoal(AccumuloClient client, String tableName, TabletHostingGoal goal,
+      SortedSet<Text> splits) throws TableNotFoundException, AccumuloException,
+      AccumuloSecurityException, TableExistsException {
+
+    NewTableConfiguration ntc = new NewTableConfiguration();
+
+    // If goal not supplied via NewTableConfiguration, expect ONDEMAND as default
+    String expectedGoal = TabletHostingGoal.ONDEMAND.toString();
+    if (goal != null) {
+      expectedGoal = goal.toString();
+      ntc.withInitialHostingGoal(goal);
+    }
+
+    // Set expected number of tablets if no splits are provided
+    int expectedTabletCount = 1;
+    if (splits != null) {
+      expectedTabletCount = splits.size() + 1;
+      ntc.withSplits(splits);
+    }
+    client.tableOperations().create(tableName, ntc);
+
+    String tableId = getTableId(tableName);
+    Text beginRow;
+    Text endRow = new Text(tableId + "<");
+    if (expectedTabletCount == 1) {
+      beginRow = endRow;
+    } else {
+      beginRow = new Text(tableId + ";" + splits.first());
+    }
+    Range range = new Range(beginRow, endRow);
+    try (Scanner scanner = client.createScanner("accumulo.metadata")) {
+      HostingColumnFamily.GOAL_COLUMN.fetch(scanner);
+      scanner.setRange(range);
+      for (Map.Entry<Key,Value> entry : scanner) {
+        assertEquals(expectedGoal, entry.getValue().toString());
+      }
+      assertEquals(expectedTabletCount, scanner.stream().count());
+    }
+  }
+
+  private String getTableId(String tableName) {
+    try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) {
+      Map<String,String> idMap = client.tableOperations().tableIdMap();
+      return idMap.get(tableName);
+    }
+  }
+
   /**
    * Test simplest case of setting locality groups at table creation.
    */
diff --git a/test/src/main/java/org/apache/accumulo/test/shell/ShellCreateTableIT.java b/test/src/main/java/org/apache/accumulo/test/shell/ShellCreateTableIT.java
index 0c31d0fa42..ed4a1bd18a 100644
--- a/test/src/main/java/org/apache/accumulo/test/shell/ShellCreateTableIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/shell/ShellCreateTableIT.java
@@ -32,6 +32,7 @@ import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Base64;
@@ -53,6 +54,7 @@ import org.apache.accumulo.core.util.TextUtil;
 import org.apache.accumulo.harness.MiniClusterConfigurationCallback;
 import org.apache.accumulo.harness.SharedMiniClusterBase;
 import org.apache.accumulo.miniclusterImpl.MiniAccumuloConfigImpl;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.io.Text;
 import org.junit.jupiter.api.AfterAll;
@@ -628,6 +630,64 @@ public class ShellCreateTableIT extends SharedMiniClusterBase {
     }
   }
 
+  // Verify that createtable handles initial hosting goal parameters.
+  // Argument should handle upper/lower/mixed case as value.
+  // If splits are supplied, each created tablet should contain the hosting:goal value in the
+  // metadata table.
+  @Test
+  public void testCreateTableWithInitialHostingGoal() throws Exception {
+    final String[] tables = getUniqueNames(5);
+
+    // createtable with no goal argument supplied
+    String createCmd = "createtable " + tables[0];
+    verifyTableWithGoal(createCmd, tables[0], "ONDEMAND", 1);
+
+    // createtable with '-g' argument supplied
+    createCmd = "createtable " + tables[1] + " -g always";
+    verifyTableWithGoal(createCmd, tables[1], "ALWAYS", 1);
+
+    // using --goal
+    createCmd = "createtable " + tables[2] + " --goal NEVER";
+    verifyTableWithGoal(createCmd, tables[2], "NEVER", 1);
+
+    String splitsFile = System.getProperty("user.dir") + "/target/splitsFile";
+    Path splitFilePath = Paths.get(splitsFile);
+    try {
+      generateSplitsFile(splitsFile, 10, 12, false, false, true, false, false);
+      createCmd = "createtable " + tables[3] + " -g Always -sf " + splitsFile;
+      verifyTableWithGoal(createCmd, tables[3], "ALWAYS", 11);
+    } finally {
+      Files.delete(splitFilePath);
+    }
+
+    try {
+      generateSplitsFile(splitsFile, 5, 5, true, true, true, false, false);
+      createCmd = "createtable " + tables[4] + " -g NeVeR -sf " + splitsFile;
+      verifyTableWithGoal(createCmd, tables[4], "NEVER", 6);
+    } finally {
+      Files.delete(splitFilePath);
+    }
+  }
+
+  private void verifyTableWithGoal(String cmd, String tableName, String goal, int expectedTabletCnt)
+      throws Exception {
+    ts.exec(cmd);
+    String tableId = getTableId(tableName);
+    String result =
+        ts.exec("scan -t accumulo.metadata -b " + tableId + " -e " + tableId + "< -c hosting:goal");
+    // the hosting:goal entry should be created at table creation
+    assertTrue(result.contains("hosting:goal"));
+    // There should be a corresponding goal value for each expected tablet
+    assertEquals(expectedTabletCnt, StringUtils.countMatches(result, goal));
+  }
+
+  private String getTableId(String tableName) throws Exception {
+    try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) {
+      Map<String,String> idMap = client.tableOperations().tableIdMap();
+      return idMap.get(tableName);
+    }
+  }
+
   private SortedSet<Text> readSplitsFromFile(final String splitsFile) throws IOException {
     SortedSet<Text> splits = new TreeSet<>();
     try (BufferedReader reader = newBufferedReader(Paths.get(splitsFile))) {