From c805a0973fdae41d5d498cc5606e08b00d62260f Mon Sep 17 00:00:00 2001 From: arbaazkhan1 Date: Tue, 18 Aug 2026 09:30:31 -0400 Subject: [PATCH] alternative solution to issue 4979 --- .../util/adminCommand/PreSplitUtil.java | 130 ++++++++++++++++++ .../util/adminCommand/PreSplitUtilTest.java | 79 +++++++++++ 2 files changed, 209 insertions(+) create mode 100644 server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtil.java create mode 100644 server/base/src/test/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtilTest.java diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtil.java b/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtil.java new file mode 100644 index 00000000000..fa24174d902 --- /dev/null +++ b/server/base/src/main/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtil.java @@ -0,0 +1,130 @@ +/* + * 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 + * + * https://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.server.util.adminCommand; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.apache.accumulo.core.cli.ServerOpts; +import org.apache.accumulo.server.util.ServerKeywordExecutable; +import org.apache.accumulo.start.spi.CommandGroup; +import org.apache.accumulo.start.spi.CommandGroups; +import org.apache.accumulo.start.spi.KeywordExecutable; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.google.auto.service.AutoService; +import com.google.common.base.Preconditions; + +@AutoService(KeywordExecutable.class) +public class PreSplitUtil extends ServerKeywordExecutable { + + @Override + public String keyword() { + return "pre-split"; + } + + @Override + public String description() { + return "Generates UUID-based split points for any Accumulo table whose row keys are UUIDs."; + } + + @Override + public CommandGroup commandGroup() { + return CommandGroups.INSTANCE; + } + + @Override + public void execute(JCommander cl, PreSplitOpts options) throws Exception { + validateOptions(options); + List splits = generateSplits(options.numSplits); + String tableName = options.tableName != null ? options.tableName : ""; + System.out.println("Generating " + splits.size() + " split points for table: " + tableName); + + if (options.splitsFile != null) { + Path splitsPath = Path.of(options.splitsFile); + try (var out = Files.newOutputStream(splitsPath); + var osw = new OutputStreamWriter(out, UTF_8); var bw = new BufferedWriter(osw); + var writer = new PrintWriter(bw)) { + splits.forEach(writer::println); + } + System.out.println("Wrote " + splits.size() + " split point(s) to " + options.splitsFile); + } else { + splits.forEach(System.out::println); + } + } + + static class PreSplitOpts extends ServerOpts { + @Parameter(names = {"-t", "--table"}, + description = "The names of the table for which to generate splits points.") + String tableName = null; + + @Parameter(names = {"-n", "--num-splits"}, + description = "Generate N split points for the fate table and print to stdout. N must be >= 1.") + int numSplits = -1; + + @Parameter(names = {"-sf", "--splitsFile"}, + description = "Write split points to a file. Used with -n or --num-splits.") + String splitsFile = null; + } + + public PreSplitUtil() { + super(new PreSplitOpts()); + } + + public static void main(String[] args) throws Exception { + new PreSplitUtil().execute(args); + } + + static List generateSplits(int numSplits) { + Preconditions.checkArgument(numSplits >= 1, + "Number of splits must be greater than 1. Specifying 0 would generate no splits and leave the table unchanged."); + + // Same logic as in FateManager.getDesiredPartitions() + // Work w/ 60 bit unsigned integers to partition the space and then shift over by 4. Used 60 + // bits instead of 63 so it nicely aligns w/ hex in the uuid. + long jump = (1L << 60) / (numSplits + 1); + List splits = new ArrayList<>(numSplits); + for (int i = 1; i <= numSplits; i++) { + long start = (i * jump) << 4; + splits.add(new UUID(start, 0).toString()); + } + + return Collections.unmodifiableList(splits); + } + + private void validateOptions(PreSplitOpts opts) { + if (opts.numSplits == 0) { + throw new IllegalArgumentException( + "-n / --num-splits must be >= 1. Specifying 0 generates no splits and leaves the table unchanged."); + } + if (opts.numSplits < 0) { + throw new IllegalArgumentException("-n / --num-splits is required and must be >= 1."); + } + } +} diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtilTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtilTest.java new file mode 100644 index 00000000000..2ef28795121 --- /dev/null +++ b/server/base/src/test/java/org/apache/accumulo/server/util/adminCommand/PreSplitUtilTest.java @@ -0,0 +1,79 @@ +/* + * 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 + * + * https://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.server.util.adminCommand; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +public class PreSplitUtilTest { + @Test + public void testSplitCount() { + assertEquals(1, PreSplitUtil.generateSplits(1).size()); + assertEquals(4, PreSplitUtil.generateSplits(4).size()); + assertEquals(10, PreSplitUtil.generateSplits(10).size()); + } + + @Test + public void testAllValidUUIDs() { + List splits = PreSplitUtil.generateSplits(8); + for (String s : splits) { + assertEquals(s, UUID.fromString(s).toString(), + "Split point is not a valid UUID string: " + s); + } + } + + @Test + public void testSplitsAreAscending() { + List splits = PreSplitUtil.generateSplits(16); + for (int i = 0; i < splits.size() - 1; i++) { + assertTrue(splits.get(i).compareTo(splits.get(i + 1)) < 0, + "Splits are not in ascending lexicographic order at index " + i); + } + } + + @Test + public void testSingleSplitIsMidpoint() { + String expected = new UUID(Long.MIN_VALUE, 0).toString(); + assertEquals(expected, PreSplitUtil.generateSplits(1).get(0)); + } + + @Test + public void testZeroThrows() { + assertThrows(IllegalArgumentException.class, () -> PreSplitUtil.generateSplits(0)); + } + + @Test + public void testNegativeThrows() { + assertThrows(IllegalArgumentException.class, () -> PreSplitUtil.generateSplits(-1)); + assertThrows(IllegalArgumentException.class, () -> PreSplitUtil.generateSplits(-100)); + } + + @Test + public void testResultIsImmutable() { + List splits = PreSplitUtil.generateSplits(4); + assertThrows(UnsupportedOperationException.class, () -> splits.add("extra")); + } +}