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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 5760787958 [flink] Support managed memory configuration for writer 
slot sharing groups (#10767)
5760787958 is described below

commit 576078795883ff215ec676d053fed74c7ad4f41a
Author: Ran Tao <[email protected]>
AuthorDate: Mon Oct 26 14:24:27 2026 +0800

    [flink] Support managed memory configuration for writer slot sharing groups 
(#10767)
---
 docs/docs/flink/configuration.md                   |  30 +++
 docs/generated/flink_connector_configuration.html  |   6 +
 .../flink/sink/cdc/FlinkCdcMultiTableSink.java     |  29 ++-
 .../sink/cdc/FlinkCdcSyncDatabaseSinkBuilder.java  |   3 +
 .../flink/sink/cdc/FlinkCdcMultiTableSinkTest.java |  66 ++++++
 .../apache/paimon/flink/FlinkConnectorOptions.java |  11 +
 .../org/apache/paimon/flink/sink/FlinkSink.java    |  36 +++-
 .../paimon/flink/sink/WriterManagedMemoryTest.java | 237 +++++++++++++++++++++
 8 files changed, 412 insertions(+), 6 deletions(-)

diff --git a/docs/docs/flink/configuration.md b/docs/docs/flink/configuration.md
index 4cddc0b102..4ece3c5256 100644
--- a/docs/docs/flink/configuration.md
+++ b/docs/docs/flink/configuration.md
@@ -61,6 +61,36 @@ INSERT INTO paimon_table /*+ OPTIONS(
 SELECT * FROM source_table;
 ```
 
+### Managed Memory for an Explicit Writer Resource Group
+
+When setting `sink.writer-memory` to configure the writer slot sharing group's 
task heap,
+use `sink.writer-managed-memory` to also request managed memory for that 
group. This optional
+budget must be positive and requires `sink.writer-memory`; `sink.writer-cpu` 
retains its default
+of one core unless explicitly configured.
+See Flink's [fine-grained resource 
management](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/deployment/finegrained_resource/)
+for the deployment requirements and limitations of explicit slot sharing group 
resources.
+
+```sql
+INSERT INTO paimon_table /*+ OPTIONS(
+    'sink.writer-memory' = '512 mb',
+    'sink.writer-managed-memory' = '1 gb',
+    'sink.use-managed-memory-allocator' = 'true',
+    'sink.managed.writer-buffer-memory' = '256 mb'
+) */
+SELECT * FROM source_table;
+```
+
+The managed budget is a resource request for each shared slot of the writer 
group, not a total
+across all writer subtasks or a guaranteed allocation to the writer alone. 
Actual allocation
+depends on Flink's resource management mode, available resources, and managed 
memory consumers.
+The writer-buffer setting remains a weight: `256 mb` in this example does not 
cap the writer's
+buffer at 256 MiB. Flink determines each writer subtask's share of the slot's 
managed memory.
+
+Setting the budget does not enable the managed allocator. With the allocator 
disabled, writer
+buffers still use heap memory. The existing downstream slot sharing and 
explicit committer
+resource settings are unchanged. Leaving the new option unset preserves 
existing resource
+configuration, including default-slot managed allocation.
+
 ## Setting dynamic options
 
 Dynamic options tune a query or a subsequently submitted job without changing 
the options
diff --git a/docs/generated/flink_connector_configuration.html 
b/docs/generated/flink_connector_configuration.html
index 37638d7f99..b713195233 100644
--- a/docs/generated/flink_connector_configuration.html
+++ b/docs/generated/flink_connector_configuration.html
@@ -368,6 +368,12 @@ under the License.
             <td>Double</td>
             <td>Sink writer cpu to control cpu cores of writer.</td>
         </tr>
+        <tr>
+            <td><h5>sink.writer-managed-memory</h5></td>
+            <td style="word-wrap: break-word;">(none)</td>
+            <td>MemorySize</td>
+            <td>Managed memory resource budget for the writer slot sharing 
group. Must be positive and requires sink.writer-memory. This does not enable 
sink.use-managed-memory-allocator or change the allocation weight 
sink.managed.writer-buffer-memory. The actual writer buffer size depends on the 
slot's allocated resources and managed memory consumers.</td>
+        </tr>
         <tr>
             <td><h5>sink.writer-memory</h5></td>
             <td style="word-wrap: break-word;">(none)</td>
diff --git 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
index b560920a91..fcf9f2745a 100644
--- 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
+++ 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSink.java
@@ -72,6 +72,7 @@ public class FlinkCdcMultiTableSink implements Serializable {
     private final CatalogLoader catalogLoader;
     private final double writeCpuCores;
     private final MemorySize writeHeapMemory;
+    @Nullable private final MemorySize writeManagedMemory;
     private final double commitCpuCores;
     @Nullable private final MemorySize commitHeapMemory;
     private final String commitUser;
@@ -99,6 +100,7 @@ public class FlinkCdcMultiTableSink implements Serializable {
         this.databaseName = null;
         this.writeCpuCores = writeCpuCores;
         this.writeHeapMemory = writeHeapMemory;
+        this.writeManagedMemory = null;
         this.commitCpuCores = commitCpuCores;
         this.commitHeapMemory = commitHeapMemory;
         this.commitUser = commitUser;
@@ -116,10 +118,35 @@ public class FlinkCdcMultiTableSink implements 
Serializable {
             String commitUser,
             boolean eagerInit,
             TableFilter tableFilter) {
+        this(
+                catalogLoader,
+                databaseName,
+                writeCpuCores,
+                writeHeapMemory,
+                null,
+                commitCpuCores,
+                commitHeapMemory,
+                commitUser,
+                eagerInit,
+                tableFilter);
+    }
+
+    public FlinkCdcMultiTableSink(
+            CatalogLoader catalogLoader,
+            String databaseName,
+            double writeCpuCores,
+            @Nullable MemorySize writeHeapMemory,
+            @Nullable MemorySize writeManagedMemory,
+            double commitCpuCores,
+            @Nullable MemorySize commitHeapMemory,
+            String commitUser,
+            boolean eagerInit,
+            TableFilter tableFilter) {
         this.catalogLoader = catalogLoader;
         this.databaseName = Preconditions.checkNotNull(databaseName);
         this.writeCpuCores = writeCpuCores;
         this.writeHeapMemory = writeHeapMemory;
+        this.writeManagedMemory = writeManagedMemory;
         this.commitCpuCores = commitCpuCores;
         this.commitHeapMemory = commitHeapMemory;
         this.commitUser = commitUser;
@@ -195,7 +222,7 @@ public class FlinkCdcMultiTableSink implements Serializable 
{
                 shuffled.transform(
                         WRITER_NAME, typeInfo, 
createWriteOperator(sinkProvider, commitUser));
         forwardParallelism(written, shuffled);
-        configureSlotSharingGroup(written, writeCpuCores, writeHeapMemory);
+        configureSlotSharingGroup(written, writeCpuCores, writeHeapMemory, 
writeManagedMemory);
 
         // shuffle committables by table
         DataStream<MultiTableCommittable> partitioned =
diff --git 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcSyncDatabaseSinkBuilder.java
 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcSyncDatabaseSinkBuilder.java
index 30a6a98d47..2e44c88bbe 100644
--- 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcSyncDatabaseSinkBuilder.java
+++ 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/FlinkCdcSyncDatabaseSinkBuilder.java
@@ -69,6 +69,7 @@ public class FlinkCdcSyncDatabaseSinkBuilder<T> {
     @Nullable private Integer parallelism;
     private double writerCpu;
     @Nullable private MemorySize writerMemory;
+    @Nullable private MemorySize writerManagedMemory;
     private double committerCpu;
     @Nullable private MemorySize committerMemory;
 
@@ -111,6 +112,7 @@ public class FlinkCdcSyncDatabaseSinkBuilder<T> {
         this.parallelism = options.get(FlinkConnectorOptions.SINK_PARALLELISM);
         this.writerCpu = options.get(FlinkConnectorOptions.SINK_WRITER_CPU);
         this.writerMemory = 
options.get(FlinkConnectorOptions.SINK_WRITER_MEMORY);
+        this.writerManagedMemory = 
options.get(FlinkConnectorOptions.SINK_WRITER_MANAGED_MEMORY);
         this.committerCpu = 
options.get(FlinkConnectorOptions.SINK_COMMITTER_CPU);
         this.committerMemory = 
options.get(FlinkConnectorOptions.SINK_COMMITTER_MEMORY);
         this.commitUser = createCommitUser(options);
@@ -191,6 +193,7 @@ public class FlinkCdcSyncDatabaseSinkBuilder<T> {
                         database,
                         writerCpu,
                         writerMemory,
+                        writerManagedMemory,
                         committerCpu,
                         committerMemory,
                         commitUser,
diff --git 
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSinkTest.java
 
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSinkTest.java
index 741a0fb9d1..cee9df8666 100644
--- 
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSinkTest.java
+++ 
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/FlinkCdcMultiTableSinkTest.java
@@ -20,17 +20,24 @@ package org.apache.paimon.flink.sink.cdc;
 
 import org.apache.paimon.flink.FlinkCatalogFactory;
 import org.apache.paimon.flink.FlinkConnectorOptions;
+import org.apache.paimon.flink.action.MultiTablesSinkMode;
+import org.apache.paimon.flink.action.cdc.TypeMapping;
 import org.apache.paimon.flink.sink.FlinkStreamPartitioner;
+import org.apache.paimon.options.MemorySize;
 import org.apache.paimon.options.Options;
 
 import org.apache.flink.api.dag.Transformation;
+import org.apache.flink.runtime.clusterframework.types.ResourceProfile;
 import org.apache.flink.streaming.api.datastream.DataStreamSink;
 import org.apache.flink.streaming.api.datastream.DataStreamSource;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.graph.StreamGraph;
 import org.apache.flink.streaming.api.transformations.OneInputTransformation;
 import org.apache.flink.streaming.api.transformations.PartitionTransformation;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
+import java.util.Collections;
 import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 
@@ -123,6 +130,65 @@ public class FlinkCdcMultiTableSinkTest {
                 .hasMessageContaining("database name");
     }
 
+    @Test
+    public void testDatabaseBuilderPropagatesWriterManagedMemory(
+            @TempDir java.nio.file.Path tempDir) {
+        Options catalogOptions = new Options();
+        catalogOptions.setString("warehouse", tempDir.toString());
+        Options options = new Options();
+        options.setString("sink.writer-memory", "512 mb");
+        options.setString("sink.writer-managed-memory", "128 mb");
+        options.setString("sink.committer-memory", "64 mb");
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        new FlinkCdcSyncDatabaseSinkBuilder<TestCdcEvent>()
+                .withInput(
+                        env.fromData(
+                                TestCdcEvent.class,
+                                new TestCdcEvent("t", Collections.emptyList(), 
0)))
+                .withParserFactory(TestCdcEventParser::new)
+                .withTableOptions(options)
+                .withDatabase("test_db")
+                .withCatalogLoader(() -> 
FlinkCatalogFactory.createPaimonCatalog(catalogOptions))
+                .withTypeMapping(TypeMapping.defaultMapping())
+                .withMode(MultiTablesSinkMode.COMBINED)
+                .build();
+        StreamGraph graph = env.getStreamGraph();
+        ResourceProfile writerResources =
+                graph.getSlotSharingGroupResource("CDC MultiplexWriter").get();
+        
assertThat(writerResources.getManagedMemory().getMebiBytes()).isEqualTo(128);
+        
assertThat(writerResources.getTaskHeapMemory().getMebiBytes()).isEqualTo(512);
+        ResourceProfile committerResources =
+                graph.getSlotSharingGroupResource("Multiplex Global 
Committer").get();
+        assertThat(committerResources.getManagedMemory().getBytes()).isZero();
+        
assertThat(committerResources.getTaskHeapMemory().getMebiBytes()).isEqualTo(64);
+    }
+
+    @Test
+    public void testManagedMemoryWithoutHeapIsRejected() {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        FlinkCdcMultiTableSink sink =
+                new FlinkCdcMultiTableSink(
+                        () -> FlinkCatalogFactory.createPaimonCatalog(new 
Options()),
+                        "test_db",
+                        1.0,
+                        null,
+                        MemorySize.ofMebiBytes(128),
+                        1.0,
+                        null,
+                        "test-user",
+                        false,
+                        null);
+        assertThatThrownBy(
+                        () ->
+                                sink.sinkFrom(
+                                        env.fromData(
+                                                CdcMultiplexRecord.class,
+                                                new CdcMultiplexRecord("", "", 
null))))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("sink.writer-managed-memory")
+                .hasMessageContaining("sink.writer-memory");
+    }
+
     @Test
     public void testDatabaseAwareConstructorRejectsNullDatabase() {
         assertThatThrownBy(
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
index 7bb4f5c468..0deded6d31 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java
@@ -364,6 +364,17 @@ public class FlinkConnectorOptions {
                     .noDefaultValue()
                     .withDescription("Sink writer memory to control heap 
memory of writer.");
 
+    public static final ConfigOption<MemorySize> SINK_WRITER_MANAGED_MEMORY =
+            ConfigOptions.key("sink.writer-managed-memory")
+                    .memoryType()
+                    .noDefaultValue()
+                    .withDescription(
+                            "Managed memory resource budget for the writer 
slot sharing group. "
+                                    + "Must be positive and requires 
sink.writer-memory. This does not "
+                                    + "enable 
sink.use-managed-memory-allocator or change the allocation "
+                                    + "weight 
sink.managed.writer-buffer-memory. The actual writer buffer "
+                                    + "size depends on the slot's allocated 
resources and managed memory consumers.");
+
     public static final ConfigOption<Double> SINK_COMMITTER_CPU =
             ConfigOptions.key("sink.committer-cpu")
                     .doubleType()
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
index c28d280128..21e9b722c4 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/sink/FlinkSink.java
@@ -65,6 +65,7 @@ import static 
org.apache.paimon.flink.FlinkConnectorOptions.SINK_MANAGED_WRITER_
 import static 
org.apache.paimon.flink.FlinkConnectorOptions.SINK_OPERATOR_UID_SUFFIX;
 import static 
org.apache.paimon.flink.FlinkConnectorOptions.SINK_USE_MANAGED_MEMORY;
 import static org.apache.paimon.flink.FlinkConnectorOptions.SINK_WRITER_CPU;
+import static 
org.apache.paimon.flink.FlinkConnectorOptions.SINK_WRITER_MANAGED_MEMORY;
 import static org.apache.paimon.flink.FlinkConnectorOptions.SINK_WRITER_MEMORY;
 import static org.apache.paimon.flink.FlinkConnectorOptions.generateCustomUid;
 import static 
org.apache.paimon.flink.utils.ManagedMemoryUtils.declareManagedMemory;
@@ -167,7 +168,10 @@ public abstract class FlinkSink<T> implements Serializable 
{
         }
 
         configureSlotSharingGroup(
-                written, options.get(SINK_WRITER_CPU), 
options.get(SINK_WRITER_MEMORY));
+                written,
+                options.get(SINK_WRITER_CPU),
+                options.get(SINK_WRITER_MEMORY),
+                options.get(SINK_WRITER_MANAGED_MEMORY));
 
         if (!table.primaryKeys().isEmpty() && options.get(PRECOMMIT_COMPACT)) {
             SingleOutputStreamOperator<Committable> beforeSort =
@@ -295,18 +299,40 @@ public abstract class FlinkSink<T> implements 
Serializable {
             SingleOutputStreamOperator<?> operator,
             double cpuCores,
             @Nullable MemorySize heapMemory) {
+        configureSlotSharingGroup(operator, cpuCores, heapMemory, null);
+    }
+
+    public static void configureSlotSharingGroup(
+            SingleOutputStreamOperator<?> operator,
+            double cpuCores,
+            @Nullable MemorySize heapMemory,
+            @Nullable MemorySize managedMemory) {
+        if (managedMemory != null) {
+            checkArgument(
+                    heapMemory != null,
+                    "'%s' requires '%s' to configure the writer slot sharing 
group.",
+                    SINK_WRITER_MANAGED_MEMORY.key(),
+                    SINK_WRITER_MEMORY.key());
+            checkArgument(
+                    managedMemory.getBytes() > 0,
+                    "'%s' must be positive.",
+                    SINK_WRITER_MANAGED_MEMORY.key());
+        }
         if (heapMemory == null) {
             return;
         }
 
-        SlotSharingGroup slotSharingGroup =
+        SlotSharingGroup.Builder builder =
                 SlotSharingGroup.newBuilder(operator.getName())
                         .setCpuCores(cpuCores)
                         .setTaskHeapMemory(
                                 new org.apache.flink.configuration.MemorySize(
-                                        heapMemory.getBytes()))
-                        .build();
-        operator.slotSharingGroup(slotSharingGroup);
+                                        heapMemory.getBytes()));
+        if (managedMemory != null) {
+            builder.setManagedMemory(
+                    new 
org.apache.flink.configuration.MemorySize(managedMemory.getBytes()));
+        }
+        operator.slotSharingGroup(builder.build());
     }
 
     public static void assertStreamingConfiguration(StreamExecutionEnvironment 
env) {
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/WriterManagedMemoryTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/WriterManagedMemoryTest.java
new file mode 100644
index 0000000000..fe7e106e80
--- /dev/null
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/WriterManagedMemoryTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.paimon.flink.sink;
+
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.schema.FileSystemSchemaManager;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.FileStoreTableFactory;
+import org.apache.paimon.types.DataTypes;
+
+import org.apache.flink.api.common.ExecutionConfig;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.MemorySize;
+import org.apache.flink.core.memory.ManagedMemoryUseCase;
+import org.apache.flink.runtime.clusterframework.types.ResourceProfile;
+import org.apache.flink.runtime.jobgraph.JobVertex;
+import org.apache.flink.runtime.operators.testutils.MockEnvironment;
+import org.apache.flink.streaming.api.datastream.DataStream;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.graph.StreamConfig;
+import org.apache.flink.streaming.api.graph.StreamGraph;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperatorFactory;
+import org.apache.flink.streaming.api.transformations.OneInputTransformation;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for the writer slot sharing group's managed memory budget. */
+class WriterManagedMemoryTest extends CommitterTestBase {
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testManagedResourcesAndDownstreamInheritance(boolean 
managedAllocator) throws Exception {
+        FileStoreTable table = createTable("128 mb", managedAllocator);
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        DataStream<Committable> written = write(env, table);
+        StreamGraph graph = env.getStreamGraph();
+        String writerGroup = written.getTransformation().getName();
+        ResourceProfile resources = 
graph.getSlotSharingGroupResource(writerGroup).get();
+        
assertThat(resources.getManagedMemory()).isEqualTo(MemorySize.ofMebiBytes(128));
+        
assertThat(resources.getTaskHeapMemory()).isEqualTo(MemorySize.ofMebiBytes(512));
+        
assertThat(resources.getCpuCores().getValue().doubleValue()).isEqualTo(2.5);
+        
assertThat(resources.getTaskOffHeapMemory()).isEqualTo(MemorySize.ZERO);
+        
assertThat(written.getTransformation().getManagedMemoryOperatorScopeUseCaseWeights())
+                .isEqualTo(
+                        managedAllocator
+                                ? 
Collections.singletonMap(ManagedMemoryUseCase.OPERATOR, 256)
+                                : Collections.emptyMap());
+        graph.getStreamNodes().stream()
+                .filter(n -> !n.getOperatorName().startsWith("Source:"))
+                .forEach(n -> 
assertThat(n.getSlotSharingGroup()).isEqualTo(writerGroup));
+        
assertThat(writerVertex(graph).getSlotSharingGroup().getResourceProfile())
+                .isEqualTo(resources);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testAbsentOptionPreservesExistingResources(boolean explicitHeap) 
throws Exception {
+        FileStoreTable table =
+                createFileStoreTable(
+                        options -> {
+                            
options.setString("sink.use-managed-memory-allocator", "true");
+                            if (explicitHeap) {
+                                options.setString("sink.writer-memory", "512 
mb");
+                            }
+                        });
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        write(env, table);
+        ResourceProfile resources =
+                
writerVertex(env.getStreamGraph()).getSlotSharingGroup().getResourceProfile();
+        if (explicitHeap) {
+            
assertThat(resources.getManagedMemory()).isEqualTo(MemorySize.ZERO);
+        } else {
+            assertThat(resources).isEqualTo(ResourceProfile.UNKNOWN);
+        }
+    }
+
+    @Test
+    void testManagedMemoryRequiresHeap() throws Exception {
+        FileStoreTable table =
+                createFileStoreTable(
+                        options -> 
options.setString("sink.writer-managed-memory", "128 mb"));
+        assertThatThrownBy(() -> 
write(StreamExecutionEnvironment.getExecutionEnvironment(), table))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("sink.writer-managed-memory")
+                .hasMessageContaining("sink.writer-memory");
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testManagedMemoryMustBePositive(boolean managedAllocator) throws 
Exception {
+        FileStoreTable table = createTable("0 b", managedAllocator);
+        assertThatThrownBy(() -> 
write(StreamExecutionEnvironment.getExecutionEnvironment(), table))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("sink.writer-managed-memory")
+                .hasMessageContaining("positive");
+    }
+
+    @Test
+    void testExplicitCommitterResourcesArePreserved() throws Exception {
+        Options options = new Options();
+        options.setString("sink.committer-memory", "64 mb");
+        options.setString("sink.committer-cpu", "0.25");
+        FileStoreTable table = createTable("128 mb", 
true).copy(options.toMap());
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        write(env, table);
+        StreamGraph graph = env.getStreamGraph();
+        String group =
+                graph.getStreamNodes().stream()
+                        .filter(n -> n.getOperatorName().startsWith("Global 
Committer"))
+                        .findFirst()
+                        .get()
+                        .getSlotSharingGroup();
+        ResourceProfile resources = 
graph.getSlotSharingGroupResource(group).get();
+        
assertThat(resources.getTaskHeapMemory()).isEqualTo(MemorySize.ofMebiBytes(64));
+        assertThat(resources.getManagedMemory()).isEqualTo(MemorySize.ZERO);
+        
assertThat(resources.getCpuCores().getValue().doubleValue()).isEqualTo(0.25);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testWriterUsesManagedBudgetFromJobGraph(boolean primaryKey) throws 
Exception {
+        FileStoreTable table = createTable("4 mb", true, primaryKey);
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        DataStream<Committable> written = write(env, table);
+        JobVertex vertex = writerVertex(env.getStreamGraph());
+        long managedBytes =
+                
vertex.getSlotSharingGroup().getResourceProfile().getManagedMemory().getBytes();
+        assertThat(managedBytes).isEqualTo(4L * 1024 * 1024);
+        StreamConfig config = new StreamConfig(vertex.getConfiguration());
+        double fraction =
+                config.getManagedMemoryFractionOperatorUseCaseOfSlot(
+                        ManagedMemoryUseCase.OPERATOR,
+                        new Configuration(),
+                        new Configuration(),
+                        getClass().getClassLoader());
+        assertThat(fraction).isEqualTo(1.0);
+        OneInputStreamOperatorFactory<InternalRow, Committable> factory =
+                (OneInputStreamOperatorFactory<InternalRow, Committable>)
+                        ((OneInputTransformation<InternalRow, Committable>)
+                                        written.getTransformation())
+                                .getOperatorFactory();
+        try (MockEnvironment environment =
+                        
MockEnvironment.builder().setManagedMemorySize(managedBytes).build();
+                OneInputStreamOperatorTestHarness<InternalRow, Committable> 
harness =
+                        new OneInputStreamOperatorTestHarness<>(factory, 
environment)) {
+            // The harness declares STATE_BACKEND by default; the generated 
writer group does not.
+            harness.getStreamConfig().setStateBackendUsesManagedMemory(false);
+            harness.getStreamConfig()
+                    .setManagedMemoryFractionOperatorOfUseCase(
+                            ManagedMemoryUseCase.OPERATOR, fraction);
+            harness.setup(new CommittableTypeInfo().createSerializer(new 
ExecutionConfig()));
+            harness.open();
+            harness.processElement(GenericRow.of(1, 1L), 1);
+            PrepareCommitOperator<?, ?> operator =
+                    (PrepareCommitOperator<?, ?>) 
harness.getOneInputOperator();
+            
assertThat(operator.memoryPoolFactory.totalBufferSize()).isEqualTo(managedBytes);
+            
assertThat(operator.memoryPoolFactory.usedBufferSize()).isPositive();
+            harness.prepareSnapshotPreBarrier(1);
+            assertThat(harness.extractOutputValues()).isNotEmpty();
+        }
+    }
+
+    private FileStoreTable createTable(String managedMemory, boolean 
managedAllocator)
+            throws Exception {
+        return createTable(managedMemory, managedAllocator, false);
+    }
+
+    private FileStoreTable createTable(
+            String managedMemory, boolean managedAllocator, boolean 
primaryKey) throws Exception {
+        Options options = new Options();
+        options.setString("path", tablePath.toString());
+        options.setString("bucket", "1");
+        options.setString("bucket-key", "a");
+        options.setString("sink.writer-cpu", "2.5");
+        options.setString("sink.writer-memory", "512 mb");
+        options.setString("sink.writer-managed-memory", managedMemory);
+        options.setString("sink.use-managed-memory-allocator", 
String.valueOf(managedAllocator));
+        options.setString("write-buffer-for-append", "true");
+        Schema.Builder schema =
+                Schema.newBuilder()
+                        .column("a", DataTypes.INT())
+                        .column("b", DataTypes.BIGINT())
+                        .options(options.toMap());
+        if (primaryKey) {
+            schema.primaryKey("a");
+        }
+        new FileSystemSchemaManager(LocalFileIO.create(), 
tablePath).createTable(schema.build());
+        return FileStoreTableFactory.create(LocalFileIO.create(), options);
+    }
+
+    private DataStream<Committable> write(StreamExecutionEnvironment env, 
FileStoreTable table) {
+        DataStream<InternalRow> source =
+                env.fromData(InternalRow.class, GenericRow.of(1, 1L))
+                        .name("upstream")
+                        .slotSharingGroup("main");
+        FlinkSink<InternalRow> sink = new FixedBucketSink(table, null);
+        DataStream<Committable> written = sink.doWrite(source, "test-user", 2);
+        sink.doCommit(written, "test-user");
+        return written;
+    }
+
+    private JobVertex writerVertex(StreamGraph graph) {
+        for (JobVertex vertex : graph.getJobGraph().getVertices()) {
+            if (vertex.getName().startsWith("Writer")) {
+                return vertex;
+            }
+        }
+        throw new AssertionError("Writer vertex not found");
+    }
+}

Reply via email to