This is an automated email from the ASF dual-hosted git repository.
fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git
The following commit(s) were added to refs/heads/main by this push:
new 20b008ce5 [server] Limit max bucket num per table partition (#3597)
20b008ce5 is described below
commit 20b008ce566740f5d92527c89a0fc731b59dc11f
Author: yunhong <[email protected]>
AuthorDate: Tue Jul 7 01:05:40 2026 +0800
[server] Limit max bucket num per table partition (#3597)
---
.../java/org/apache/fluss/client/admin/Admin.java | 2 +-
.../fluss/client/admin/FlussAdminITCase.java | 87 ++++++++++++++--------
.../org/apache/fluss/config/ConfigOptions.java | 9 ++-
.../fluss/exception/TooManyBucketsException.java | 3 +-
.../server/coordinator/AutoPartitionManager.java | 2 +-
.../fluss/server/coordinator/MetadataManager.java | 27 ++-----
.../server/utils/TableDescriptorValidation.java | 2 +-
.../coordinator/AutoPartitionManagerTest.java | 15 ++--
website/docs/maintenance/configuration.md | 2 +-
.../table-design/data-distribution/partitioning.md | 2 +-
10 files changed, 84 insertions(+), 67 deletions(-)
diff --git
a/fluss-client/src/main/java/org/apache/fluss/client/admin/Admin.java
b/fluss-client/src/main/java/org/apache/fluss/client/admin/Admin.java
index 6d60e4098..5cc3e7971 100644
--- a/fluss-client/src/main/java/org/apache/fluss/client/admin/Admin.java
+++ b/fluss-client/src/main/java/org/apache/fluss/client/admin/Admin.java
@@ -356,7 +356,7 @@ public interface Admin extends AutoCloseable {
* <li>{@link TooManyPartitionsException} if the number of partitions is
larger than the
* maximum number of partitions of one table, see {@link
ConfigOptions#MAX_PARTITION_NUM}.
* <li>{@link TooManyBucketsException} if the number of buckets is
larger than the maximum
- * number of buckets of one table, see {@link
ConfigOptions#MAX_BUCKET_NUM}.
+ * number of buckets of one partition, see {@link
ConfigOptions#MAX_BUCKET_NUM}.
* </ul>
*
* @param tablePath The table path of the table.
diff --git
a/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
b/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
index 18f177b64..cda9fa334 100644
---
a/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
+++
b/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
@@ -1835,12 +1835,8 @@ class FlussAdminITCase extends ClientToServerITCaseBase {
.collect(Collectors.toList());
}
- /**
- * Test that creating a partitioned table with bucket count exceeding the
maximum throws
- * TooManyBucketsException.
- */
@Test
- public void testAddTooManyBuckets() throws Exception {
+ public void testBucketLimitForPartitionedTableAppliesPerPartition() throws
Exception {
// Already set low maximum bucket limit to 30 for this test in
ClientToServerITCaseBase
String dbName = DEFAULT_TABLE_PATH.getDatabaseName();
TableDescriptor partitionedTable =
@@ -1857,35 +1853,46 @@ class FlussAdminITCase extends ClientToServerITCaseBase
{
TablePath tablePath = TablePath.of(dbName,
"test_add_too_many_buckets_table");
admin.createTable(tablePath, partitionedTable, true).get();
- // Add 3 partitions (3 * 10 = 30 buckets, which is the limit)
- for (int i = 0; i < 3; i++) {
+ // Add 4 partitions. The total bucket count is 40, which is above the
configured limit,
+ // but each partition only has 10 buckets.
+ for (int i = 0; i < 4; i++) {
admin.createPartition(tablePath, newPartitionSpec("age",
String.valueOf(i)), false)
.get();
}
+ assertThat(admin.listPartitionInfos(tablePath).get()).hasSize(4);
+
+ TableDescriptor tooManyBucketsPartitionedTable =
+ TableDescriptor.builder()
+ .schema(
+ Schema.newBuilder()
+ .column("id", DataTypes.STRING())
+ .column("name", DataTypes.STRING())
+ .column("age", DataTypes.STRING())
+ .build())
+ .distributedBy(40, "id")
+ .partitionedBy("age")
+ .build();
+ TablePath tooManyBucketsTablePath =
+ TablePath.of(dbName, "test_too_many_buckets_partitioned");
- // Try to add one more partition, exceeding the bucket limit (4 * 10 >
30)
assertThatThrownBy(
() ->
- admin.createPartition(
- tablePath,
newPartitionSpec("age", "4"), false)
+ admin.createTable(
+ tooManyBucketsTablePath,
+ tooManyBucketsPartitionedTable,
+ false)
.get())
.cause()
.isInstanceOf(TooManyBucketsException.class)
- .hasMessageContaining("exceeding the maximum of 30 buckets");
+ .hasMessageContaining(
+ "Bucket count 40 exceeds the maximum limit 30 for a
non-partitioned table or partition.");
}
- /**
- * Test that creating a non-partitioned table with bucket count exceeding
the maximum throws
- * TooManyBucketsException.
- */
@Test
public void testBucketLimitForNonPartitionedTable() throws Exception {
- // Set a low maximum bucket limit for this test
- // (Assuming the configuration is already set to 30 in
ClientToServerITCaseBase)
String dbName = DEFAULT_TABLE_PATH.getDatabaseName();
- // Create a non-partitioned table with 40 buckets (exceeding limit of
30)
- TableDescriptor nonPartitionedTable =
+ TableDescriptor maxBucketsNonPartitionedTable =
TableDescriptor.builder()
.schema(
Schema.newBuilder()
@@ -1893,21 +1900,42 @@ class FlussAdminITCase extends ClientToServerITCaseBase
{
.column("name", DataTypes.STRING())
.column("value", DataTypes.STRING())
.build())
- .distributedBy(40, "id") // 40 buckets exceeds the
limit of 30
- .build(); // No partitionedBy call makes this
non-partitioned
+ .distributedBy(30, "id")
+ .build();
+ TablePath maxBucketsTablePath = TablePath.of(dbName,
"test_max_buckets_non_partitioned");
- TablePath tablePath = TablePath.of(dbName,
"test_too_many_buckets_non_partitioned");
+ admin.createTable(maxBucketsTablePath, maxBucketsNonPartitionedTable,
false).get();
- // Creating this table should throw TooManyBucketsException
- assertThatThrownBy(() -> admin.createTable(tablePath,
nonPartitionedTable, false).get())
+ TableDescriptor tooManyBucketsNonPartitionedTable =
+ TableDescriptor.builder()
+ .schema(
+ Schema.newBuilder()
+ .column("id", DataTypes.STRING())
+ .column("name", DataTypes.STRING())
+ .column("value", DataTypes.STRING())
+ .build())
+ .distributedBy(31, "id")
+ .build();
+ TablePath tooManyBucketsTablePath =
+ TablePath.of(dbName, "test_too_many_buckets_non_partitioned");
+
+ assertThatThrownBy(
+ () ->
+ admin.createTable(
+ tooManyBucketsTablePath,
+
tooManyBucketsNonPartitionedTable,
+ false)
+ .get())
.cause()
.isInstanceOf(TooManyBucketsException.class)
- .hasMessageContaining("exceeds the maximum limit");
+ .hasMessageContaining(
+ "Bucket count 31 exceeds the maximum limit 30 for a
non-partitioned table or partition.");
}
@Test
- public void testDefaultBucketLimitForNonPartitionedTable() throws
Exception {
-
assertThat(ConfigOptions.MAX_BUCKET_NUM.defaultValue()).isEqualTo(20000);
+ public void testDefaultPartitionAndBucketLimitsForNonPartitionedTable()
throws Exception {
+
assertThat(ConfigOptions.MAX_PARTITION_NUM.defaultValue()).isEqualTo(1000);
+
assertThat(ConfigOptions.MAX_BUCKET_NUM.defaultValue()).isEqualTo(4096);
FlussClusterExtension defaultLimitCluster =
FlussClusterExtension.builder().setNumOfTabletServers(1).build();
@@ -1925,7 +1953,7 @@ class FlussAdminITCase extends ClientToServerITCaseBase {
.column("id", DataTypes.STRING())
.column("name", DataTypes.STRING())
.build())
- .distributedBy(30000, "id")
+ .distributedBy(4097, "id")
.build();
defaultLimitAdmin.createDatabase(dbName, DatabaseDescriptor.EMPTY,
false).get();
@@ -1936,7 +1964,8 @@ class FlussAdminITCase extends ClientToServerITCaseBase {
.get())
.cause()
.isInstanceOf(TooManyBucketsException.class)
- .hasMessageContaining("Bucket count 30000 exceeds the
maximum limit 20000.");
+ .hasMessageContaining(
+ "Bucket count 4097 exceeds the maximum limit 4096
for a non-partitioned table or partition.");
} finally {
defaultLimitCluster.close();
}
diff --git
a/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
b/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
index f3acc8279..aca5edaa8 100644
--- a/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
+++ b/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
@@ -290,11 +290,12 @@ public class ConfigOptions {
public static final ConfigOption<Integer> MAX_BUCKET_NUM =
key("max.bucket.num")
.intType()
- .defaultValue(20000)
+ .defaultValue(4096)
.withDescription(
- "The maximum number of buckets that can be created
for a table. "
- + "The default value is 20000. "
- + "This default is capped to reduce the
risk that the table assignment znode exceeds "
+ "The maximum number of buckets that can be created
for a non-partitioned table "
+ + "or for each partition of a partitioned
table. "
+ + "The default value is 4096. "
+ + "This default is capped to reduce the
risk that an assignment znode exceeds "
+ "ZooKeeper's packet size limit.");
/**
diff --git
a/fluss-common/src/main/java/org/apache/fluss/exception/TooManyBucketsException.java
b/fluss-common/src/main/java/org/apache/fluss/exception/TooManyBucketsException.java
index 71258346e..0a9c5f379 100644
---
a/fluss-common/src/main/java/org/apache/fluss/exception/TooManyBucketsException.java
+++
b/fluss-common/src/main/java/org/apache/fluss/exception/TooManyBucketsException.java
@@ -20,7 +20,8 @@ package org.apache.fluss.exception;
import org.apache.fluss.annotation.PublicEvolving;
/**
- * This exception is thrown if the number of table buckets is exceed
max.bucket.num.
+ * This exception is thrown if the number of buckets for a non-partitioned
table or partition
+ * exceeds max.bucket.num.
*
* @since 0.7
*/
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/AutoPartitionManager.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/AutoPartitionManager.java
index 37bc7c376..7ac0b2fcf 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/AutoPartitionManager.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/AutoPartitionManager.java
@@ -441,7 +441,7 @@ public class AutoPartitionManager implements AutoCloseable {
} catch (TooManyBucketsException t) {
LOG.warn(
"Auto partitioning skip to create partition {} for
table [{}], "
- + "because exceed the maximum number of
buckets.",
+ + "because exceed the maximum number of
buckets per partition.",
partition,
tablePath);
} catch (Exception e) {
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
index 65b2ee24b..d87b85d5e 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
@@ -828,9 +828,8 @@ public class MetadataManager {
partition.getPartitionQualifiedName(), tablePath));
}
- final int partitionNumber;
try {
- partitionNumber = zookeeperClient.getPartitionNumber(tablePath);
+ int partitionNumber =
zookeeperClient.getPartitionNumber(tablePath);
if (partitionNumber + 1 > maxPartitionNum) {
throw new TooManyPartitionsException(
String.format(
@@ -847,24 +846,12 @@ public class MetadataManager {
e);
}
- try {
- int bucketCount =
partitionAssignment.getBucketAssignments().size();
- // currently, every partition has the same bucket count
- int totalBuckets = bucketCount * (partitionNumber + 1);
- if (totalBuckets > maxBucketNum) {
- throw new TooManyBucketsException(
- String.format(
- "Adding partition '%s' would result in %d
total buckets for table %s, exceeding the maximum of %d buckets.",
- partition.getPartitionName(),
- totalBuckets,
- tablePath,
- maxBucketNum));
- }
- } catch (TooManyBucketsException e) {
- throw e;
- } catch (Exception e) {
- throw new FlussRuntimeException(
- String.format("Failed to check total bucket count for
table %s", tablePath), e);
+ int bucketCount = partitionAssignment.getBucketAssignments().size();
+ if (bucketCount > maxBucketNum) {
+ throw new TooManyBucketsException(
+ String.format(
+ "Partition '%s' has %d buckets for table %s,
exceeding the maximum of %d buckets per partition.",
+ partition.getPartitionName(), bucketCount,
tablePath, maxBucketNum));
}
try {
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
b/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
index 6b60a9409..4be0a0534 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
@@ -253,7 +253,7 @@ public class TableDescriptorValidation {
if (bucketCount > maxBucketNum) {
throw new TooManyBucketsException(
String.format(
- "Bucket count %s exceeds the maximum limit %s.",
+ "Bucket count %s exceeds the maximum limit %s for
a non-partitioned table or partition.",
bucketCount, maxBucketNum));
}
List<String> bucketKeys =
tableDescriptor.getTableDistribution().get().getBucketKeys();
diff --git
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/AutoPartitionManagerTest.java
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/AutoPartitionManagerTest.java
index 641ae7d9e..a24ab80db 100644
---
a/fluss-server/src/test/java/org/apache/fluss/server/coordinator/AutoPartitionManagerTest.java
+++
b/fluss-server/src/test/java/org/apache/fluss/server/coordinator/AutoPartitionManagerTest.java
@@ -582,14 +582,14 @@ class AutoPartitionManagerTest {
}
/**
- * Test if AutoPartionManager.createPartition adheres to maxBucketLimit
while adding new
- * parition automatically, skip if it breaches limit.
+ * Test if AutoPartitionManager.createPartition applies maxBucketLimit per
partition while
+ * adding new partition automatically.
*/
@Test
- void testMaxBucketNum() throws Exception {
+ void testMaxBucketNumPerPartition() throws Exception {
int bucketCountPerPartition = 10;
- int maxBucketNum = 30; // Allow only 3 partitions with 10 buckets each
+ int maxBucketNum = 30;
Configuration config = new Configuration();
config.set(ConfigOptions.MAX_BUCKET_NUM, maxBucketNum);
@@ -626,16 +626,15 @@ class AutoPartitionManagerTest {
periodicExecutor.triggerNonPeriodicScheduledTask();
int partitionsNum = zookeeperClient.getPartitionNumber(tablePath);
- // Only 3 partitions should be created (3 * 10 = 30 buckets) out of
the 4 requested
- assertThat(partitionsNum).isEqualTo(3);
+ // All 4 requested partitions should be created because each partition
is below the limit.
+ assertThat(partitionsNum).isEqualTo(4);
// Advance time to trigger another auto-partition cycle
clock.advanceTime(Duration.ofDays(1).plusHours(23));
periodicExecutor.triggerPeriodicScheduledTasks();
- // Check partitions again - should still have only 3 due to bucket
limit
partitionsNum = zookeeperClient.getPartitionNumber(tablePath);
- assertThat(partitionsNum).isEqualTo(3);
+ assertThat(partitionsNum).isEqualTo(5);
}
@Test
diff --git a/website/docs/maintenance/configuration.md
b/website/docs/maintenance/configuration.md
index 55601246b..605bada74 100644
--- a/website/docs/maintenance/configuration.md
+++ b/website/docs/maintenance/configuration.md
@@ -46,7 +46,7 @@ during the Fluss cluster working.
| allow.create.log.tables | Boolean |
true
| Whether to allow creation of log tables. When set to false, attempts
to create log tables (tables without primary key) will be rejected. The default
value is true.
[...]
| allow.create.kv.tables | Boolean |
true
| Whether to allow creation of kv tables (primary key tables). When
set to false, attempts to create kv tables (tables with primary key) will be
rejected. The default value is true.
[...]
| max.partition.num | Integer |
1000
| Limits the maximum number of partitions that can be created for a
partitioned table to avoid creating too many partitions.
[...]
-| max.bucket.num | Integer |
20000
| The maximum number of buckets that can be created for a table. The
default value is 20000. This default is capped to reduce the risk that the
table assignment znode exceeds ZooKeeper's packet size limit.
[...]
+| max.bucket.num | Integer |
4096
| The maximum number of buckets that can be created for a
non-partitioned table or for each partition of a partitioned table. The default
value is 4096. This default is capped to reduce the risk that an assignment
znode exceeds ZooKeeper's packet [...]
| acl.notification.expiration-time | Duration |
15min
| The duration for which ACL notifications are valid before they
expire. This configuration determines the time window during which an ACL
notification is considered active. After this duration, the notification will
no longer be valid and will b [...]
| authorizer.enabled | Boolean |
false
| Specifies whether to enable the authorization feature. If enabled,
access control is enforced based on the authorization rules defined in the
configuration. If disabled, all operations and resources are accessible to all
users. [...]
| authorizer.type | String |
default
| Specifies the type of authorizer to be used for access control. This
value corresponds to the identifier of the authorization plugin. The default
value is `default`, which indicates the built-in authorizer implementation.
Custom authorizers can [...]
diff --git a/website/docs/table-design/data-distribution/partitioning.md
b/website/docs/table-design/data-distribution/partitioning.md
index 2228134e7..69255bd0f 100644
--- a/website/docs/table-design/data-distribution/partitioning.md
+++ b/website/docs/table-design/data-distribution/partitioning.md
@@ -86,7 +86,7 @@ Below are the configuration items related to Fluss cluster
and automatic partiti
**Dynamic partitioning** is a feature that is enabled by default on client,
allowing the client to automatically create partitions based on the data being
written to the table. This feature is especially valuable when the set of
partitions is not known in advance, eliminating the need for manual partition
creation. It is also particularly useful when working with multi-field
partitions, as auto-partitioning currently only supports single-field
partitioning creation.
-Please note that the number of dynamically created partitions is also subject
to the `max.partition.num` and `max.bucket.num` limit configured on the Fluss
cluster.
+Please note that dynamically created partitions are subject to the
`max.partition.num` limit, and each partition's bucket count is subject to the
`max.bucket.num` limit configured on the Fluss cluster.
### Client Options
| Option | Type | Required |
Default | Description
|