You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by tr...@apache.org on 2020/11/08 17:21:54 UTC

[flink] 04/04: [FLINK-19693] ITCases for Approximate Local Recovery

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

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

commit 0b3f15ee598aa2b55706a4c3a30e2e529ac8f651
Author: Yuan Mei <yu...@gmail.com>
AuthorDate: Sun Nov 8 17:54:51 2020 +0800

    [FLINK-19693] ITCases for Approximate Local Recovery
---
 .../ApproximateLocalRecoveryDownstreamITCase.java  | 336 +++++++++++++++++++++
 1 file changed, 336 insertions(+)

diff --git a/flink-tests/src/test/java/org/apache/flink/test/checkpointing/ApproximateLocalRecoveryDownstreamITCase.java b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/ApproximateLocalRecoveryDownstreamITCase.java
new file mode 100644
index 0000000..e89d9d9
--- /dev/null
+++ b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/ApproximateLocalRecoveryDownstreamITCase.java
@@ -0,0 +1,336 @@
+/*
+ * 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.checkpointing;
+
+import org.apache.flink.api.common.functions.RichMapFunction;
+import org.apache.flink.api.common.restartstrategy.RestartStrategies;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.JobManagerOptions;
+import org.apache.flink.configuration.MemorySize;
+import org.apache.flink.configuration.TaskManagerOptions;
+import org.apache.flink.runtime.state.KeyGroupRangeAssignment;
+import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration;
+import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
+import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
+import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.test.util.SuccessException;
+import org.apache.flink.util.TestLogger;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.Timeout;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.apache.flink.test.util.TestUtils.tryExecute;
+
+/**
+ * To test approximate downstream failover.
+ *
+ * <p>If a task fails, all its downstream tasks restart, including itself.
+ * - Test the failed task can reconnect successfully.
+ * - Test partial records are cleaned up correctly.
+ * - Test only downstream are tasks restart.
+ */
+
+public class ApproximateLocalRecoveryDownstreamITCase extends TestLogger {
+	private static final int BUFFER_SIZE = 4096;
+
+	@Rule
+	public MiniClusterWithClientResource cluster = new MiniClusterWithClientResource(
+		new MiniClusterResourceConfiguration.Builder()
+			.setConfiguration(createConfig())
+			.setNumberTaskManagers(4)
+			.setNumberSlotsPerTaskManager(1)
+			.build());
+
+	@Rule
+	public final Timeout timeout = Timeout.millis(300000L);
+
+	/**
+	 * Test the following topology.
+	 * <pre>
+	 *     (source1/1) -----> (map1/1) -----> (sink1/1)
+	 * </pre>
+	 * (map1/1) fails, (map1/1) and (sink1/1) restart
+	 */
+	@Test
+	public void localTaskFailureRecoveryThreeTasks() throws Exception {
+		final int failAfterElements = 150;
+		final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+		env
+			.setParallelism(1)
+			.setBufferTimeout(0)
+			.setMaxParallelism(128)
+			.disableOperatorChaining()
+			.setRestartStrategy(RestartStrategies.fixedDelayRestart(1, 0));
+		env.getCheckpointConfig().enableApproximateLocalRecovery(true);
+
+		env.addSource(new AppSourceFunction())
+			.slotSharingGroup("source")
+			.map(new FailingMapper<>(failAfterElements))
+			.slotSharingGroup("map")
+			.addSink(new ValidatingAtMostOnceSink(300))
+			.slotSharingGroup("sink");
+
+		FailingMapper.failedBefore = false;
+		tryExecute(env, "testThreeTasks");
+	}
+
+	/**
+	 * Test the following topology.
+	 * <pre>
+	 *     (source1/1) -----> (map1/2) -----> (sink1/1)
+	 *         |                                ^
+	 *         -------------> (map2/2) ---------|
+	 * </pre>
+	 * (map1/2) fails, (map1/2) and (sink1/1) restart
+	 */
+	@Test
+	public void localTaskFailureRecoveryTwoMapTasks() throws Exception {
+		final int failAfterElements = 20;
+		final int keyByChannelNumber = 2;
+		final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
+		env
+			.setParallelism(1)
+			.setBufferTimeout(0)
+			.disableOperatorChaining()
+			.setMaxParallelism(128)
+			.setRestartStrategy(RestartStrategies.fixedDelayRestart(1, 0));
+		env.getCheckpointConfig().enableApproximateLocalRecovery(true);
+
+		env.addSource(new AppSourceFunction(BUFFER_SIZE, env.getMaxParallelism(), keyByChannelNumber))
+			.slotSharingGroup("source")
+			.keyBy(InvertedKeyTuple::key)
+			.map(new FailingMapper<>(failAfterElements))
+			.setParallelism(keyByChannelNumber)
+			.slotSharingGroup("map")
+			.addSink(new ValidatingAtMostOnceSink(200, keyByChannelNumber))
+			.slotSharingGroup("sink");
+
+		FailingMapper.failedBefore = false;
+		tryExecute(env, "testTwoMapTasks");
+	}
+
+	/**
+	 * source function used to generate InvertedKeyTuple.
+	 *
+	 * <p>InvertedKeyTuple includes four fields:
+	 * - index: increased upon each tuple is generated.
+	 * - key: based on which the tuple to be partitioned for downstream consumption (a map, as shown in the testcase for example).
+	 * - selectedChannel: the selected channel (the selected instance of the downstream operator) the tuple to be sent to.
+	 *                    Usually, selectedChannel is decided by the hash-partition function applied on the key.
+	 *                    However in this case, we invert the process by selecting the channel to be sent first, and then
+	 *                    decide a possible key the tuple may contain. In this way, we can control exactly how many
+	 *                    tuples are sent to each instance of the downstream operator.
+	 * - longOrShortString: either long or short string used to test network transition upon reconnection.
+	 *
+	 * <p>AppSourceFunction makes sure tuples generated are evenly distributed amongst downstream operator instances.
+	 */
+	private static class AppSourceFunction extends RichParallelSourceFunction<InvertedKeyTuple> {
+		private final String shortString = "I am a very long string to test partial records hohoho hahaha ";
+		private final String longOrShortString;
+		private final int maxParallelism;
+		private final int numberOfChannels;
+		private final int[] keys;
+		private int index = 0;
+		private volatile boolean running = true;
+
+		// short-length string
+		AppSourceFunction() {
+			this.longOrShortString = shortString;
+			this.maxParallelism = 128;
+			this.numberOfChannels = 1;
+			this.keys = initKeys(numberOfChannels);
+		}
+
+		// long-length string
+		AppSourceFunction(int bufferSize, int maxParallelism, int numberOfChannels) {
+			this.maxParallelism = maxParallelism;
+			this.numberOfChannels = numberOfChannels;
+			this.keys = initKeys(numberOfChannels);
+
+			StringBuilder builder = new StringBuilder(shortString);
+			for (int i = 0; i <= 2 * bufferSize / shortString.length() + 1; i++) {
+				builder.append(shortString);
+			}
+			this.longOrShortString = builder.toString();
+		}
+
+		@Override
+		public void run(SourceContext<InvertedKeyTuple> ctx) throws Exception{
+			while (running) {
+				synchronized (ctx.getCheckpointLock()) {
+					if (index % 100 == 0) {
+						Thread.sleep(50);
+					}
+					int selectedChannel = index % numberOfChannels;
+					int key = keys[selectedChannel];
+					ctx.collect(new InvertedKeyTuple(index, key, selectedChannel, longOrShortString));
+				}
+				index++;
+			}
+		}
+
+		@Override
+		public void cancel() {
+			running = false;
+		}
+
+		// Find the first key which falls into the key group of each downstream channel
+		private int[] initKeys(int numberOfChannels) {
+			int[] keys = new int[numberOfChannels];
+
+			for (int i = 0; i < numberOfChannels; i++) {
+				int key = 0;
+				while (key < 1000 && selectedChannel(key) != i) {
+					key++;
+				}
+				assert key < 1000 : "Can not find a key within number 1000";
+				keys[i] = key;
+			}
+
+			return keys;
+		}
+
+		private int selectedChannel(int key) {
+			return KeyGroupRangeAssignment.assignKeyToParallelOperator(key, maxParallelism, numberOfChannels);
+		}
+	}
+
+	/** Fails once the first map instance reaches failCount. */
+	private static class FailingMapper<T> extends RichMapFunction<T, T>  {
+		private static final long serialVersionUID = 6334389850158703L;
+
+		private static volatile boolean failedBefore;
+
+		private final int failCount;
+		private int numElementsTotal;
+
+		private boolean failer;
+
+		FailingMapper(int failCount) {
+			this.failCount = failCount;
+		}
+
+		@Override
+		public void open(Configuration parameters) {
+			failer = getRuntimeContext().getIndexOfThisSubtask() == 0;
+		}
+
+		@Override
+		public T map(T value) throws Exception {
+			numElementsTotal++;
+
+			if (!failedBefore) {
+				Thread.sleep(10);
+
+				if (failer && numElementsTotal >= failCount) {
+					failedBefore = true;
+					throw new Exception("Artificial Test Failure");
+				}
+			}
+
+			return value;
+		}
+	}
+
+	/**
+	 * Validating sink to make sure each selectedChannel (of map) gets at least numElementsTotal elements.
+	 *
+	 * <p>Notice that the source generates tuples evenly distributed amongst downstream operator (map) instances.
+	 * Besides, the index generated is continuous and monotonic increasing as long as the source is not restarted.
+	 * Hence,
+	 * - In the case of approximate local recovery is NOT enabled, where the entire job, including source, is restarted
+	 *   after map fails, source regenerates tuples indexed from 0. Upon reach numElementsTotal,
+	 *   the maximal possible index is numElementsTotal * numberOfInputChannels - 1
+	 * - In the case of approximate local recovery is enabled, where only the downstream of the failed task restart,
+	 *   source does not regenerating tuples, the maximal possible index > numElementsTotal * numberOfInputChannels - 1
+	 */
+	private static class ValidatingAtMostOnceSink extends RichSinkFunction<InvertedKeyTuple> {
+		private static final long serialVersionUID = 1748426382527469932L;
+		private final int numElementsTotal;
+		private final int[] numElements;
+		private final Integer[] indexReachingNumElements;
+		private final int numberOfInputChannels;
+
+		ValidatingAtMostOnceSink(int numElementsTotal, int numberOfInputChannels) {
+			this.numElementsTotal = numElementsTotal;
+			this.numberOfInputChannels = numberOfInputChannels;
+			this.numElements = new int[numberOfInputChannels];
+			this.indexReachingNumElements = new Integer[numberOfInputChannels];
+		}
+
+		ValidatingAtMostOnceSink(int numElementsTotal) {
+			this.numElementsTotal = numElementsTotal;
+			this.numberOfInputChannels = 1;
+			this.numElements = new int[numberOfInputChannels];
+			this.indexReachingNumElements = new Integer[numberOfInputChannels];
+		}
+
+		@Override
+		public void invoke(InvertedKeyTuple tuple, Context ctx) throws Exception {
+			assert tuple.selectedChannel < numberOfInputChannels;
+			numElements[tuple.selectedChannel]++;
+
+			boolean allReachNumElementsTotal = true;
+			for (int i = 0; i < numberOfInputChannels; i++) {
+				if (numElements[i] == numElementsTotal) {
+					indexReachingNumElements[i] = tuple.index;
+				} else if (numElements[i] < numElementsTotal) {
+					allReachNumElementsTotal = false;
+				}
+			}
+			if (allReachNumElementsTotal) {
+				assert Collections.max(Arrays.asList(indexReachingNumElements)) >= numElementsTotal * numberOfInputChannels;
+				throw new SuccessException();
+			}
+		}
+	}
+
+	private static Configuration createConfig() {
+		Configuration config = new Configuration();
+		config.setString(JobManagerOptions.SCHEDULING_STRATEGY, "legacy");
+		config.set(TaskManagerOptions.MEMORY_SEGMENT_SIZE, MemorySize.parse(Integer.toString(BUFFER_SIZE)));
+
+		return config;
+	}
+
+	private static class InvertedKeyTuple {
+		int index;
+		/** Key based on which the tuple is partitioned. */
+		int key;
+		/** The selected channel of this tuple after key-partitioned. */
+		int selectedChannel;
+		String longOrShortString;
+
+		InvertedKeyTuple(int index, int key, int selectedChannel, String longOrShortString) {
+			this.index = index;
+			this.key = key;
+			this.selectedChannel = selectedChannel;
+			this.longOrShortString = longOrShortString;
+		}
+
+		int key() {
+			return key;
+		}
+	}
+}