Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,41 @@ ListOffsetsResult listOffsets(
*/
CompletableFuture<Void> removeServerTag(List<Integer> tabletServers, ServerTag serverTag);

/**
* Add a server tag to all currently registered tabletServers in the specified racks.
*
* <p>Rack membership is resolved once to a snapshot of server IDs. TabletServers registered in
* those racks later do not inherit the tag. Callers must cordon the racks before invoking this
* method if new tabletServers must not enter them.
*
* <p>If no registered tabletServer matches, authorization is still checked and the operation
* completes successfully without making changes.
*
* @param racks the rack identifiers to match. Must not be null or empty and must not contain
* null, empty, or whitespace-only elements.
* @param serverTag the server tag to add. Must not be null.
* @throws IllegalArgumentException if {@code racks} is empty or contains a null, empty, or
* whitespace-only element.
*/
CompletableFuture<Void> addServerTagByRack(List<String> racks, ServerTag serverTag);

/**
* Remove a server tag from all currently registered tabletServers in the specified racks.
*
* <p>Only current rack membership is considered. Tags on offline tabletServers or tabletServers
* that moved to another rack must be removed by ID with {@link #removeServerTag}.
*
* <p>If no registered tabletServer matches, authorization is still checked and the operation
* completes successfully without making changes.
*
* @param racks the rack identifiers to match. Must not be null or empty and must not contain
* null, empty, or whitespace-only elements.
* @param serverTag the server tag to remove. Must not be null.
* @throws IllegalArgumentException if {@code racks} is empty or contains a null, empty, or
* whitespace-only element.
*/
CompletableFuture<Void> removeServerTagByRack(List<String> racks, ServerTag serverTag);

/**
* Based on the provided {@code priorityGoals}, Fluss performs load balancing on the cluster's
* bucket load.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.apache.fluss.rpc.gateway.AdminGateway;
import org.apache.fluss.rpc.gateway.AdminReadOnlyGateway;
import org.apache.fluss.rpc.gateway.TabletServerGateway;
import org.apache.fluss.rpc.messages.AddServerTagByRackRequest;
import org.apache.fluss.rpc.messages.AddServerTagRequest;
import org.apache.fluss.rpc.messages.AlterClusterConfigsRequest;
import org.apache.fluss.rpc.messages.AlterDatabaseRequest;
Expand Down Expand Up @@ -98,6 +99,7 @@
import org.apache.fluss.rpc.messages.PbTableStatsRespForBucket;
import org.apache.fluss.rpc.messages.RebalanceRequest;
import org.apache.fluss.rpc.messages.RebalanceResponse;
import org.apache.fluss.rpc.messages.RemoveServerTagByRackRequest;
import org.apache.fluss.rpc.messages.RemoveServerTagRequest;
import org.apache.fluss.rpc.messages.TableExistsRequest;
import org.apache.fluss.rpc.messages.TableExistsResponse;
Expand Down Expand Up @@ -135,7 +137,9 @@
import static org.apache.fluss.rpc.util.CommonRpcMessageUtils.toPbAclBindingFilters;
import static org.apache.fluss.rpc.util.CommonRpcMessageUtils.toPbAclFilter;
import static org.apache.fluss.rpc.util.CommonRpcMessageUtils.toPbAclInfos;
import static org.apache.fluss.utils.Preconditions.checkArgument;
import static org.apache.fluss.utils.Preconditions.checkNotNull;
import static org.apache.fluss.utils.StringUtils.isNullOrWhitespaceOnly;

/**
* The default implementation of {@link Admin}.
Expand Down Expand Up @@ -727,6 +731,22 @@ public CompletableFuture<Void> removeServerTag(
return gateway.removeServerTag(request).thenApply(r -> null);
}

@Override
public CompletableFuture<Void> addServerTagByRack(List<String> racks, ServerTag serverTag) {
validateRackRequest(racks, serverTag);
AddServerTagByRackRequest request =
new AddServerTagByRackRequest().addAllRacks(racks).setServerTag(serverTag.value);
return gateway.addServerTagByRack(request).thenApply(r -> null);
}

@Override
public CompletableFuture<Void> removeServerTagByRack(List<String> racks, ServerTag serverTag) {
validateRackRequest(racks, serverTag);
RemoveServerTagByRackRequest request =
new RemoveServerTagByRackRequest().addAllRacks(racks).setServerTag(serverTag.value);
return gateway.removeServerTagByRack(request).thenApply(r -> null);
}

@Override
public CompletableFuture<String> rebalance(List<GoalType> priorityGoals) {
RebalanceRequest request = new RebalanceRequest();
Expand Down Expand Up @@ -951,4 +971,15 @@ public AdminGateway getAdminGateway() {
public AdminReadOnlyGateway getAdminReadOnlyGateway() {
return readOnlyGateway;
}

private static void validateRackRequest(List<String> racks, ServerTag serverTag) {
checkNotNull(serverTag, "serverTag must not be null");
checkNotNull(racks, "racks must not be null");
checkArgument(!racks.isEmpty(), "racks must not be empty");
for (String rack : racks) {
checkArgument(
!isNullOrWhitespaceOnly(rack),
"rack element must not be null, empty, or whitespace-only");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@

import org.apache.fluss.client.Connection;
import org.apache.fluss.client.ConnectionFactory;
import org.apache.fluss.cluster.rebalance.ServerTag;
import org.apache.fluss.config.ConfigOptions;
import org.apache.fluss.config.Configuration;
import org.apache.fluss.metadata.DatabaseDescriptor;
import org.apache.fluss.metadata.TableDescriptor;
import org.apache.fluss.metadata.TableInfo;
import org.apache.fluss.metadata.TablePath;
import org.apache.fluss.server.testutils.FlussClusterExtension;
import org.apache.fluss.server.zk.ZooKeeperClient;
import org.apache.fluss.server.zk.data.BucketAssignment;
import org.apache.fluss.server.zk.data.TableAssignment;

Expand All @@ -34,11 +36,14 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;

import static org.apache.fluss.client.admin.FlussAdminITCase.DEFAULT_SCHEMA;
import static org.apache.fluss.client.admin.FlussAdminITCase.DEFAULT_TABLE_PATH;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/** ITCase for rack aware cluster. */
public class RackAwareClusterITCase {
Expand Down Expand Up @@ -107,4 +112,82 @@ void testCreateTableWithInsufficientRack() throws Exception {
admin.dropTable(tablePath, false).get();
admin.dropDatabase(DEFAULT_TABLE_PATH.getDatabaseName(), false, false).get();
}

@Test
void testAddAndRemoveServerTagByRack() throws Exception {
// Cluster racks: server-0 -> rack-0, server-1 -> rack-1,
// server-2 -> rack-2, server-3 -> rack-0
ZooKeeperClient zkClient = FLUSS_CLUSTER_EXTENSION.getZooKeeperClient();

// 1. Add a server tag by a single rack (rack-0: server-0 and server-3).
admin.addServerTagByRack(Collections.singletonList("rack-0"), ServerTag.TEMPORARY_OFFLINE)
.get();
assertThat(zkClient.getServerTags()).isPresent();
assertThat(zkClient.getServerTags().get().getServerTags())
.containsEntry(0, ServerTag.TEMPORARY_OFFLINE)
.containsEntry(3, ServerTag.TEMPORARY_OFFLINE)
.doesNotContainKey(1)
.doesNotContainKey(2);

// 2. Remove a server tag by a single rack.
admin.removeServerTagByRack(
Collections.singletonList("rack-0"), ServerTag.TEMPORARY_OFFLINE)
.get();
assertThat(zkClient.getServerTags()).isNotPresent();

// 3. Add a server tag by multiple racks (rack-0, rack-1: server-0, server-1,
// server-3).
admin.addServerTagByRack(Arrays.asList("rack-0", "rack-1"), ServerTag.PERMANENT_OFFLINE)
.get();
assertThat(zkClient.getServerTags().get().getServerTags())
.containsEntry(0, ServerTag.PERMANENT_OFFLINE)
.containsEntry(1, ServerTag.PERMANENT_OFFLINE)
.containsEntry(3, ServerTag.PERMANENT_OFFLINE)
.doesNotContainKey(2);

// cleanup
admin.removeServerTagByRack(Arrays.asList("rack-0", "rack-1"), ServerTag.PERMANENT_OFFLINE)
.get();
assertThat(zkClient.getServerTags()).isNotPresent();

// 4. null racks -> NPE (thrown synchronously by FlussAdmin validation).
assertThatThrownBy(() -> admin.addServerTagByRack(null, ServerTag.TEMPORARY_OFFLINE))
.isInstanceOf(NullPointerException.class);

// 5. empty racks -> IAE (thrown synchronously by FlussAdmin validation).
assertThatThrownBy(
() ->
admin.addServerTagByRack(
Collections.emptyList(), ServerTag.TEMPORARY_OFFLINE))
.isInstanceOf(IllegalArgumentException.class);

// 6. blank rack -> IAE.
assertThatThrownBy(
() ->
admin.addServerTagByRack(
Collections.singletonList(" "),
ServerTag.TEMPORARY_OFFLINE))
.isInstanceOf(IllegalArgumentException.class);

// 7. A rack with no currently registered TabletServer is a no-op.
admin.addServerTagByRack(Collections.singletonList("rack-999"), ServerTag.TEMPORARY_OFFLINE)
.get();
assertThat(zkClient.getServerTags()).isNotPresent();

// 8. Removing by a rack with no currently registered TabletServer is also a no-op.
admin.removeServerTagByRack(
Collections.singletonList("rack-999"), ServerTag.TEMPORARY_OFFLINE)
.get();
assertThat(zkClient.getServerTags()).isNotPresent();

// 9. serverTag is validated before a no-match operation returns.
assertThatThrownBy(
() -> admin.addServerTagByRack(Collections.singletonList("rack-999"), null))
.isInstanceOf(NullPointerException.class);
assertThatThrownBy(
() ->
admin.removeServerTagByRack(
Collections.singletonList("rack-999"), null))
.isInstanceOf(NullPointerException.class);
}
}
Loading
Loading