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

roman pushed a commit to branch release-1.15
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/release-1.15 by this push:
     new e31c1b0  [FLINK-26063][runtime] Set current key before polling next PQ 
element
e31c1b0 is described below

commit e31c1b0f88cbcd3f3a720948123b262fc21e4cd5
Author: Roman Khachatryan <[email protected]>
AuthorDate: Mon Mar 14 15:38:45 2022 +0100

    [FLINK-26063][runtime] Set current key before polling next PQ element
    
    Some InternalPriorityQueue implementations need a correct key/group
    set before performing poll() or remove().
    
    In particular, ChangelogKeyGroupedPriorityQueue logs key group so that
    state changes can be re-distributed or shuffled.
    
    This change re-orders queue.poll and keyContext.setCurrentKey.
---
 .../flink/runtime/state/InternalPriorityQueue.java |   6 +
 .../api/operators/InternalTimerServiceImpl.java    |   4 +-
 .../flink/test/state/ChangelogRescalingITCase.java | 354 +++++++++++++++++++++
 3 files changed, 362 insertions(+), 2 deletions(-)

diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/InternalPriorityQueue.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/InternalPriorityQueue.java
index 25816eb..17bdd6a 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/InternalPriorityQueue.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/InternalPriorityQueue.java
@@ -39,6 +39,9 @@ public interface InternalPriorityQueue<T> {
      * Retrieves and removes the first element (w.r.t. the order) of this set, 
or returns {@code
      * null} if this set is empty.
      *
+     * <p>NOTE: Correct key (i.e. the key of the polled element) must be set 
on KeyContext before
+     * calling this method.
+     *
      * @return the first element of this ordered set, or {@code null} if this 
set is empty.
      */
     @Nullable
@@ -67,6 +70,9 @@ public interface InternalPriorityQueue<T> {
     /**
      * Removes the given element from the set, if is contained in the set.
      *
+     * <p>NOTE: Correct key (i.e. the key of the polled element) must be set 
on KeyContext before
+     * calling this method.
+     *
      * @param toRemove the element to remove.
      * @return <code>true</code> if the operation changed the head element or 
if it is unclear if
      *     the head element changed. Only returns <code>false</code> if the 
head element was not
diff --git 
a/flink-streaming-java/src/main/java/org/apache/flink/streaming/api/operators/InternalTimerServiceImpl.java
 
b/flink-streaming-java/src/main/java/org/apache/flink/streaming/api/operators/InternalTimerServiceImpl.java
index a9977d7..b2cf4c6 100644
--- 
a/flink-streaming-java/src/main/java/org/apache/flink/streaming/api/operators/InternalTimerServiceImpl.java
+++ 
b/flink-streaming-java/src/main/java/org/apache/flink/streaming/api/operators/InternalTimerServiceImpl.java
@@ -279,8 +279,8 @@ public class InternalTimerServiceImpl<K, N> implements 
InternalTimerService<N> {
         InternalTimer<K, N> timer;
 
         while ((timer = processingTimeTimersQueue.peek()) != null && 
timer.getTimestamp() <= time) {
-            processingTimeTimersQueue.poll();
             keyContext.setCurrentKey(timer.getKey());
+            processingTimeTimersQueue.poll();
             triggerTarget.onProcessingTime(timer);
         }
 
@@ -297,8 +297,8 @@ public class InternalTimerServiceImpl<K, N> implements 
InternalTimerService<N> {
         InternalTimer<K, N> timer;
 
         while ((timer = eventTimeTimersQueue.peek()) != null && 
timer.getTimestamp() <= time) {
-            eventTimeTimersQueue.poll();
             keyContext.setCurrentKey(timer.getKey());
+            eventTimeTimersQueue.poll();
             triggerTarget.onEventTime(timer);
         }
     }
diff --git 
a/flink-tests/src/test/java/org/apache/flink/test/state/ChangelogRescalingITCase.java
 
b/flink-tests/src/test/java/org/apache/flink/test/state/ChangelogRescalingITCase.java
new file mode 100644
index 0000000..85a30c8
--- /dev/null
+++ 
b/flink-tests/src/test/java/org/apache/flink/test/state/ChangelogRescalingITCase.java
@@ -0,0 +1,354 @@
+/*
+ * 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.flink.test.state;
+
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
+import org.apache.flink.api.connector.source.ReaderOutput;
+import org.apache.flink.api.connector.source.SourceReader;
+import org.apache.flink.api.connector.source.SourceReaderContext;
+import org.apache.flink.api.connector.source.lib.NumberSequenceSource;
+import org.apache.flink.api.connector.source.lib.util.IteratorSourceReader;
+import org.apache.flink.api.connector.source.lib.util.IteratorSourceSplit;
+import org.apache.flink.changelog.fs.FsStateChangelogStorageFactory;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.MemorySize;
+import org.apache.flink.core.io.InputStatus;
+import org.apache.flink.runtime.jobgraph.JobGraph;
+import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration;
+import org.apache.flink.streaming.api.CheckpointingMode;
+import org.apache.flink.streaming.api.datastream.DataStreamUtils;
+import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.functions.sink.DiscardingSink;
+import 
org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
+import 
org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows;
+import org.apache.flink.streaming.api.windowing.time.Time;
+import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.util.Collector;
+import org.apache.flink.util.TestLogger;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.time.Duration;
+import java.util.Iterator;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import static 
org.apache.flink.changelog.fs.FsStateChangelogOptions.PREEMPTIVE_PERSIST_THRESHOLD;
+import static 
org.apache.flink.configuration.CheckpointingOptions.CHECKPOINTS_DIRECTORY;
+import static 
org.apache.flink.configuration.CheckpointingOptions.CHECKPOINT_STORAGE;
+import static 
org.apache.flink.configuration.CheckpointingOptions.FS_SMALL_FILE_THRESHOLD;
+import static 
org.apache.flink.configuration.CheckpointingOptions.LOCAL_RECOVERY;
+import static org.apache.flink.configuration.CoreOptions.DEFAULT_PARALLELISM;
+import static org.apache.flink.configuration.PipelineOptions.OBJECT_REUSE;
+import static 
org.apache.flink.configuration.RestartStrategyOptions.RESTART_STRATEGY;
+import static org.apache.flink.configuration.StateBackendOptions.STATE_BACKEND;
+import static 
org.apache.flink.configuration.StateChangelogOptions.ENABLE_STATE_CHANGE_LOG;
+import static 
org.apache.flink.configuration.StateChangelogOptions.PERIODIC_MATERIALIZATION_INTERVAL;
+import static 
org.apache.flink.configuration.TaskManagerOptions.BUFFER_DEBLOAT_ENABLED;
+import static 
org.apache.flink.runtime.jobgraph.SavepointRestoreSettings.forPath;
+import static 
org.apache.flink.runtime.testutils.CommonTestUtils.waitForAllTaskRunning;
+import static 
org.apache.flink.streaming.api.environment.CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION;
+import static 
org.apache.flink.streaming.api.environment.ExecutionCheckpointingOptions.ALIGNED_CHECKPOINT_TIMEOUT;
+import static 
org.apache.flink.streaming.api.environment.ExecutionCheckpointingOptions.CHECKPOINTING_INTERVAL;
+import static 
org.apache.flink.streaming.api.environment.ExecutionCheckpointingOptions.CHECKPOINTING_MODE;
+import static 
org.apache.flink.streaming.api.environment.ExecutionCheckpointingOptions.ENABLE_UNALIGNED;
+import static 
org.apache.flink.streaming.api.environment.ExecutionCheckpointingOptions.EXTERNALIZED_CHECKPOINT;
+import static 
org.apache.flink.test.util.TestUtils.getMostRecentCompletedCheckpoint;
+import static 
org.apache.flink.test.util.TestUtils.getMostRecentCompletedCheckpointMaybe;
+import static org.apache.flink.util.Preconditions.checkArgument;
+
+/**
+ * Tests rescaling with Changelog enabled and with timers in state. It uses an 
adaptation of a
+ * ChangelogTestProgram that aims to generate the highest load possible while 
still allowing
+ * checkpointing. For that, it uses rate-limited FLIP-27 source and Unaligned 
checkpoints.
+ */
+@RunWith(Parameterized.class)
+public class ChangelogRescalingITCase extends TestLogger {
+    /** The rate at which events will be generated by the source. */
+    private static final int EVENTS_PER_SECOND_PER_READER = 100;
+    /** Payload size of each event generated randomly. */
+    private static final int PAYLOAD_SIZE = 1000;
+    /** Size of (ProcessingTime) windows. */
+    private static final Time WINDOW_SIZE = Time.milliseconds(100);
+    /** Slide size. */
+    private static final Time WINDOW_SLIDE = Time.milliseconds(10);
+    /** Time to Accumulate some timer delete operations. */
+    private static final int ACCUMULATE_TIME_MILLIS = 5_000;
+
+    @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Parameters(name = "Rescale {0} -> {1}")
+    public static Object[] parameters() {
+        return new Object[][] {new Object[] {6, 4}, new Object[] {4, 6}};
+    }
+
+    private final int parallelism1;
+    private final int parallelism2;
+
+    private MiniClusterWithClientResource cluster;
+
+    public ChangelogRescalingITCase(int parallelism1, int parallelism2) {
+        this.parallelism1 = parallelism1;
+        this.parallelism2 = parallelism2;
+    }
+
+    @Before
+    public void before() throws Exception {
+        Configuration configuration = new Configuration();
+        FsStateChangelogStorageFactory.configure(configuration, 
temporaryFolder.newFolder());
+        cluster =
+                new MiniClusterWithClientResource(
+                        new MiniClusterResourceConfiguration.Builder()
+                                .setConfiguration(configuration)
+                                
.setNumberSlotsPerTaskManager(Math.max(parallelism1, parallelism2))
+                                .build());
+        cluster.before();
+    }
+
+    @After
+    public void after() {
+        if (cluster != null) {
+            cluster.after();
+            cluster = null;
+        }
+    }
+
+    @Test
+    public void test() throws Exception {
+        // before rescale
+        File cpDir1 = temporaryFolder.newFolder();
+        JobID jobID1 = submit(configureJob(parallelism1, cpDir1), graph -> {});
+
+        Thread.sleep(ACCUMULATE_TIME_MILLIS);
+        File cpLocation = checkpointAndCancel(jobID1, cpDir1);
+
+        // rescale and checkpoint to verify
+        JobID jobID2 =
+                submit(
+                        configureJob(parallelism2, 
temporaryFolder.newFolder()),
+                        graph ->
+                                graph.setSavepointRestoreSettings(
+                                        
forPath(cpLocation.toURI().toString())));
+        waitForAllTaskRunning(cluster.getMiniCluster(), jobID2, true);
+        cluster.getClusterClient().cancel(jobID2).get();
+    }
+
+    private JobID submit(Configuration conf, Consumer<JobGraph> updateGraph)
+            throws InterruptedException, ExecutionException {
+        JobGraph jobGraph = createJobGraph(conf);
+        updateGraph.accept(jobGraph);
+        return cluster.getClusterClient().submitJob(jobGraph).get();
+    }
+
+    private JobGraph createJobGraph(Configuration conf) {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment(conf);
+        SingleOutputStreamOperator<TestEvent> map =
+                env.fromSource(
+                                new ThrottlingNumberSequenceSource(
+                                        0, Long.MAX_VALUE, 
EVENTS_PER_SECOND_PER_READER),
+                                WatermarkStrategy.noWatermarks(),
+                                "Sequence Source")
+                        .keyBy(ChangelogRescalingITCase::key)
+                        .map(
+                                el -> {
+                                    // Thread.sleep(100); // don't block 
barriers
+                                    byte[] bytes = new byte[PAYLOAD_SIZE];
+                                    
ThreadLocalRandom.current().nextBytes(bytes);
+                                    return new TestEvent(el, bytes);
+                                });
+        DataStreamUtils.reinterpretAsKeyedStream(map, e -> key(e.id))
+                .window(SlidingProcessingTimeWindows.of(WINDOW_SIZE, 
WINDOW_SLIDE))
+                .process(
+                        new ProcessWindowFunction<TestEvent, String, Long, 
TimeWindow>() {
+                            @Override
+                            public void process(
+                                    Long key,
+                                    ProcessWindowFunction<TestEvent, String, 
Long, TimeWindow>
+                                                    .Context
+                                            context,
+                                    Iterable<TestEvent> elements,
+                                    Collector<String> out) {}
+                        })
+                .addSink(new DiscardingSink<>());
+
+        return env.getStreamGraph().getJobGraph();
+    }
+
+    private static long key(Long num) {
+        return num % 1000;
+    }
+
+    private Configuration configureJob(int parallelism, File cpDir) {
+        Configuration conf = new Configuration();
+
+        conf.set(EXTERNALIZED_CHECKPOINT, RETAIN_ON_CANCELLATION);
+        conf.set(DEFAULT_PARALLELISM, parallelism);
+        conf.set(ENABLE_STATE_CHANGE_LOG, true);
+        conf.set(CHECKPOINTING_MODE, CheckpointingMode.EXACTLY_ONCE);
+        conf.set(CHECKPOINTING_INTERVAL, Duration.ofMillis(10));
+        conf.set(CHECKPOINT_STORAGE, "filesystem");
+        conf.set(CHECKPOINTS_DIRECTORY, cpDir.toURI().toString());
+        conf.set(STATE_BACKEND, "hashmap");
+        conf.set(LOCAL_RECOVERY, false); // not supported by changelog
+        // tune changelog
+        conf.set(PREEMPTIVE_PERSIST_THRESHOLD, MemorySize.ofMebiBytes(10));
+        conf.set(PERIODIC_MATERIALIZATION_INTERVAL, Duration.ofMinutes(3));
+        // tune flink
+        conf.set(FS_SMALL_FILE_THRESHOLD, MemorySize.ofMebiBytes(1));
+        conf.set(OBJECT_REUSE, true);
+
+        conf.set(ENABLE_UNALIGNED, true); // speedup
+        conf.set(ALIGNED_CHECKPOINT_TIMEOUT, Duration.ZERO); // prevent 
randomization
+        conf.set(BUFFER_DEBLOAT_ENABLED, false); // prevent randomization
+        conf.set(RESTART_STRATEGY, "none"); // not expecting any failures
+
+        return conf;
+    }
+
+    private static final class TestEvent implements Serializable {
+        private final long id;
+
+        @SuppressWarnings({"FieldCanBeLocal", "unused"})
+        private final byte[] payload;
+
+        private TestEvent(long id, byte[] payload) {
+            this.id = id;
+            this.payload = payload;
+        }
+    }
+
+    private static class ThrottlingNumberSequenceSource extends 
NumberSequenceSource {
+        private final int numbersPerSecond;
+
+        public ThrottlingNumberSequenceSource(long from, long to, int 
numbersPerSecondPerReader) {
+            super(from, to);
+            this.numbersPerSecond = numbersPerSecondPerReader;
+        }
+
+        @Override
+        public SourceReader<Long, NumberSequenceSplit> createReader(
+                SourceReaderContext readerContext) {
+            return new ThrottlingIteratorSourceReader<>(
+                    readerContext, new SourceRateLimiter(numbersPerSecond));
+        }
+    }
+
+    private static class ThrottlingIteratorSourceReader<
+                    E, IterT extends Iterator<E>, SplitT extends 
IteratorSourceSplit<E, IterT>>
+            extends IteratorSourceReader<E, IterT, SplitT> {
+        private final SourceRateLimiter rateLimiter;
+
+        public ThrottlingIteratorSourceReader(
+                SourceReaderContext context, SourceRateLimiter rateLimiter) {
+            super(context);
+            this.rateLimiter = rateLimiter;
+        }
+
+        @Override
+        public InputStatus pollNext(ReaderOutput<E> output) {
+            if (rateLimiter.request()) {
+                return super.pollNext(output);
+            } else {
+                return InputStatus.NOTHING_AVAILABLE;
+            }
+        }
+    }
+
+    private static final class SourceRateLimiter {
+        private final AtomicBoolean newTokensAdded = new AtomicBoolean(false);
+        private final int tokensToAdd;
+        private int tokensAvailable;
+
+        public SourceRateLimiter(int tokensPerSecond) {
+            this(
+                    tokensPerSecond < 10 ? 1000 : 100,
+                    tokensPerSecond < 10 ? tokensPerSecond : tokensPerSecond / 
10);
+        }
+
+        public SourceRateLimiter(int intervalMs, int tokensToAdd) {
+            checkArgument(intervalMs > 0);
+            checkArgument(tokensToAdd > 0);
+            this.tokensToAdd = tokensToAdd;
+            this.tokensAvailable = tokensToAdd;
+            new Timer("source-limiter", true)
+                    .scheduleAtFixedRate(
+                            new TimerTask() {
+                                @Override
+                                public void run() {
+                                    newTokensAdded.set(true); // "catch up" is 
ok
+                                }
+                            },
+                            intervalMs,
+                            intervalMs);
+        }
+
+        public boolean request() {
+            if (tokensAvailable == 0 && newTokensAdded.compareAndSet(true, 
false)) {
+                tokensAvailable = tokensToAdd;
+            }
+            if (tokensAvailable > 0) {
+                tokensAvailable--;
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    private File checkpointAndCancel(JobID jobID, File cpDir)
+            throws IOException, InterruptedException, ExecutionException {
+        while (!getMostRecentCompletedCheckpointMaybe(cpDir).isPresent()) {
+            checkStatus(jobID);
+            Thread.sleep(50);
+        }
+        cluster.getClusterClient().cancel(jobID).get();
+        checkStatus(jobID);
+        return getMostRecentCompletedCheckpoint(cpDir);
+    }
+
+    private void checkStatus(JobID jobID) throws InterruptedException, 
ExecutionException {
+        if 
(cluster.getClusterClient().getJobStatus(jobID).get().isGloballyTerminalState())
 {
+            cluster.getClusterClient()
+                    .requestJobResult(jobID)
+                    .get()
+                    .getSerializedThrowable()
+                    .ifPresent(
+                            serializedThrowable -> {
+                                throw new 
RuntimeException(serializedThrowable);
+                            });
+        }
+    }
+}

Reply via email to