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 2017/10/05 14:06:59 UTC

[06/14] flink git commit: [FLINK-7068][blob] change BlobService sub-classes for permanent and transient BLOBs

http://git-wip-us.apache.org/repos/asf/flink/blob/071e27f7/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheDeleteTest.java
----------------------------------------------------------------------
diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheDeleteTest.java b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheDeleteTest.java
new file mode 100644
index 0000000..20b0152
--- /dev/null
+++ b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheDeleteTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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.runtime.blob;
+
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.configuration.BlobServerOptions;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.runtime.concurrent.FutureUtils;
+import org.apache.flink.util.FlinkException;
+import org.apache.flink.util.OperatingSystem;
+import org.apache.flink.util.TestLogger;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.apache.flink.runtime.blob.BlobServerDeleteTest.delete;
+import static org.apache.flink.runtime.blob.BlobServerGetTest.verifyDeleted;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.put;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.verifyContents;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Tests how DELETE requests behave.
+ */
+public class BlobCacheDeleteTest extends TestLogger {
+
+	private final Random rnd = new Random();
+
+	@Rule
+	public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+	@Test
+	public void testDeleteTransient1() throws IOException {
+		testDeleteTransient(null, new JobID());
+	}
+
+	@Test
+	public void testDeleteTransient2() throws IOException {
+		testDeleteTransient(new JobID(), null);
+	}
+
+	@Test
+	public void testDeleteTransient3() throws IOException {
+		testDeleteTransient(null, null);
+	}
+
+	@Test
+	public void testDeleteTransient4() throws IOException {
+		testDeleteTransient(new JobID(), new JobID());
+	}
+
+	/**
+	 * Uploads a (different) byte array for each of the given jobs and verifies that deleting one of
+	 * them (via the {@link BlobCache}) does not influence the other.
+	 *
+	 * @param jobId1
+	 * 		first job id
+	 * @param jobId2
+	 * 		second job id
+	 */
+	private void testDeleteTransient(@Nullable JobID jobId1, @Nullable JobID jobId2)
+			throws IOException {
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+			byte[] data2 = Arrays.copyOf(data, data.length);
+			data2[0] ^= 1;
+
+			// put first BLOB
+			BlobKey key1 = put(server, jobId1, data, false);
+			assertNotNull(key1);
+
+			// put two more BLOBs (same key, other key) for another job ID
+			BlobKey key2a = put(server, jobId2, data, false);
+			assertNotNull(key2a);
+			assertEquals(key1, key2a);
+			BlobKey key2b = put(server, jobId2, data2, false);
+			assertNotNull(key2b);
+
+			// issue a DELETE request
+			assertTrue(delete(cache, jobId1, key1));
+
+			verifyDeleted(cache, jobId1, key1, false);
+			verifyDeleted(server, jobId1, key1, false);
+			// deleting a one BLOB should not affect another BLOB, even with the same key if job IDs are different
+			if ((jobId1 == null && jobId2 != null) || (jobId1 != null && !jobId1.equals(jobId2))) {
+				verifyContents(server, jobId2, key2a, data, false);
+			}
+			verifyContents(server, jobId2, key2b, data2, false);
+
+			// delete first file of second job
+			assertTrue(delete(cache, jobId2, key2a));
+			verifyDeleted(cache, jobId2, key2a, false);
+			verifyDeleted(server, jobId2, key2a, false);
+			verifyContents(server, jobId2, key2b, data2, false);
+
+			// delete second file of second job
+			assertTrue(delete(cache, jobId2, key2a));
+			verifyDeleted(cache, jobId2, key2a, false);
+			verifyDeleted(server, jobId2, key2a, false);
+			verifyContents(server, jobId2, key2b, data2, false);
+		}
+	}
+
+	@Test
+	public void testDeleteTransientAlreadyDeletedNoJob() throws IOException {
+		testDeleteTransientAlreadyDeleted(null);
+	}
+
+	@Test
+	public void testDeleteTransientAlreadyDeletedForJob() throws IOException {
+		testDeleteTransientAlreadyDeleted(new JobID());
+	}
+
+	/**
+	 * Uploads a byte array for the given job and verifies that deleting it (via the {@link
+	 * BlobCache}) does not fail independent of whether the file exists.
+	 *
+	 * @param jobId
+	 * 		job id
+	 */
+	private void testDeleteTransientAlreadyDeleted(@Nullable final JobID jobId) throws IOException {
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+
+			// put BLOB
+			BlobKey key = put(server, jobId, data, false);
+			assertNotNull(key);
+
+			File blobFile = server.getStorageLocation(jobId, key);
+			assertTrue(blobFile.delete());
+
+			// DELETE operation should not fail if file is already deleted
+			assertTrue(delete(cache, jobId, key));
+			verifyDeleted(cache, jobId, key, false);
+
+			// one more delete call that should not fail
+			assertTrue(delete(cache, jobId, key));
+			verifyDeleted(cache, jobId, key, false);
+		}
+	}
+
+	@Test
+	public void testDeleteTransientLocalFailsNoJob() throws IOException {
+		testDeleteTransientLocalFails(null);
+	}
+
+	@Test
+	public void testDeleteTransientLocalFailsForJob() throws IOException {
+		testDeleteTransientLocalFails(new JobID());
+	}
+
+	/**
+	 * Uploads a byte array for the given job and verifies that a delete operation (via the {@link
+	 * BlobCache}) does not fail even if the file is not deletable locally, e.g. via restricting
+	 * the permissions.
+	 *
+	 * @param jobId
+	 * 		job id
+	 */
+	private void testDeleteTransientLocalFails(@Nullable final JobID jobId) throws IOException {
+		assumeTrue(!OperatingSystem.isWindows()); //setWritable doesn't work on Windows.
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		File blobFile = null;
+		File directory = null;
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			try {
+				byte[] data = new byte[2000000];
+				rnd.nextBytes(data);
+
+				// put BLOB
+				BlobKey key = put(server, jobId, data, false);
+				assertNotNull(key);
+
+				// access from cache once to have it available there
+				verifyContents(cache, jobId, key, data, false);
+
+				blobFile = cache.getTransientBlobStore().getStorageLocation(jobId, key);
+				directory = blobFile.getParentFile();
+
+				assertTrue(blobFile.setWritable(false, false));
+				assertTrue(directory.setWritable(false, false));
+
+				// issue a DELETE request
+				assertFalse(delete(cache, jobId, key));
+				verifyDeleted(server, jobId, key, false);
+
+				// the file should still be there on both cache and server
+				verifyContents(cache, jobId, key, data, false);
+			} finally {
+				if (blobFile != null && directory != null) {
+					//noinspection ResultOfMethodCallIgnored
+					blobFile.setWritable(true, false);
+					//noinspection ResultOfMethodCallIgnored
+					directory.setWritable(true, false);
+				}
+			}
+		}
+	}
+
+	@Test
+	public void testDeleteTransientRemoteFailsNoJob() throws IOException {
+		testDeleteTransientRemoteFails(null);
+	}
+
+	@Test
+	public void testDeleteTransientRemoteFailsForJob() throws IOException {
+		testDeleteTransientRemoteFails(new JobID());
+	}
+
+	/**
+	 * Uploads a byte array for the given job and verifies that a delete operation (via the {@link
+	 * BlobCache}) does not fail even if the file is not deletable on the {@link BlobServer}, e.g.
+	 * via restricting the permissions.
+	 *
+	 * @param jobId
+	 * 		job id
+	 */
+	private void testDeleteTransientRemoteFails(@Nullable final JobID jobId) throws IOException {
+		assumeTrue(!OperatingSystem.isWindows()); //setWritable doesn't work on Windows.
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		File blobFile = null;
+		File directory = null;
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			try {
+				byte[] data = new byte[2000000];
+				rnd.nextBytes(data);
+
+				// put BLOB
+				BlobKey key = put(server, jobId, data, false);
+				assertNotNull(key);
+
+				// access from cache once to have it available there
+				verifyContents(cache, jobId, key, data, false);
+
+				blobFile = server.getStorageLocation(jobId, key);
+				directory = blobFile.getParentFile();
+
+				assertTrue(blobFile.setWritable(false, false));
+				assertTrue(directory.setWritable(false, false));
+
+				// issue a DELETE request
+				assertFalse(delete(cache, jobId, key));
+				File blobFileAtCache = cache.getTransientBlobStore().getStorageLocation(jobId, key);
+				assertFalse(blobFileAtCache.exists());
+
+				// the file should still be there on the server
+				verifyContents(server, jobId, key, data, false);
+				// ... and may be retrieved by the cache
+				verifyContents(cache, jobId, key, data, false);
+			} finally {
+				if (blobFile != null && directory != null) {
+					//noinspection ResultOfMethodCallIgnored
+					blobFile.setWritable(true, false);
+					//noinspection ResultOfMethodCallIgnored
+					directory.setWritable(true, false);
+				}
+			}
+		}
+	}
+
+	@Test
+	public void testConcurrentDeleteOperationsNoJob() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentDeleteOperations(null);
+	}
+
+	@Test
+	public void testConcurrentDeleteOperationsForJob() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentDeleteOperations(new JobID());
+	}
+
+	/**
+	 * [FLINK-6020] Tests that concurrent delete operations don't interfere with each other.
+	 *
+	 * <p>Note: This test checks that there cannot be two threads which have checked whether a given
+	 * blob file exist and then one of them fails deleting it. Without the introduced lock, this
+	 * situation should rarely happen and make this test fail. Thus, if this test should become
+	 * "unstable", then the delete atomicity is most likely broken.
+	 *
+	 * @param jobId
+	 * 		job ID to use (or <tt>null</tt> if job-unrelated)
+	 */
+	private void testConcurrentDeleteOperations(@Nullable final JobID jobId)
+			throws IOException, InterruptedException, ExecutionException {
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		final int concurrentDeleteOperations = 3;
+		final ExecutorService executor = Executors.newFixedThreadPool(concurrentDeleteOperations);
+
+		final List<CompletableFuture<Void>> deleteFutures = new ArrayList<>(concurrentDeleteOperations);
+
+		final byte[] data = {1, 2, 3};
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			final BlobKey blobKey = put(server, jobId, data, false);
+
+			assertTrue(server.getStorageLocation(jobId, blobKey).exists());
+
+			for (int i = 0; i < concurrentDeleteOperations; i++) {
+				CompletableFuture<Void> deleteFuture = CompletableFuture
+					.supplyAsync(
+						() -> {
+							try {
+								assertTrue(delete(cache, jobId, blobKey));
+								assertFalse(cache.getTransientBlobStore().getStorageLocation(jobId, blobKey).exists());
+								assertFalse(server.getStorageLocation(jobId, blobKey).exists());
+								return null;
+							} catch (IOException e) {
+								throw new CompletionException(new FlinkException(
+									"Could not upload blob.", e));
+							}
+						}, executor);
+
+				deleteFutures.add(deleteFuture);
+			}
+
+			CompletableFuture<Void> waitFuture = FutureUtils.waitForAll(deleteFutures);
+
+			// make sure all delete operation have completed successfully
+			// in case of no lock, one of the delete operations should eventually fail
+			waitFuture.get();
+
+			assertFalse(server.getStorageLocation(jobId, blobKey).exists());
+
+		} finally {
+			executor.shutdownNow();
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/flink/blob/071e27f7/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheGetTest.java
----------------------------------------------------------------------
diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheGetTest.java b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheGetTest.java
new file mode 100644
index 0000000..52adc4d
--- /dev/null
+++ b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheGetTest.java
@@ -0,0 +1,490 @@
+/*
+ * 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.runtime.blob;
+
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.configuration.BlobServerOptions;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.runtime.concurrent.FutureUtils;
+import org.apache.flink.util.FlinkException;
+import org.apache.flink.util.OperatingSystem;
+import org.apache.flink.util.TestLogger;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.AccessDeniedException;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.apache.flink.runtime.blob.BlobClientTest.validateGetAndClose;
+import static org.apache.flink.runtime.blob.BlobServerGetTest.get;
+import static org.apache.flink.runtime.blob.BlobServerGetTest.verifyDeleted;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.put;
+import static org.apache.flink.runtime.blob.BlobUtils.JOB_DIR_PREFIX;
+import static org.apache.flink.runtime.blob.BlobUtils.NO_JOB_DIR_PREFIX;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests how failing GET requests behave in the presence of failures when used with a {@link
+ * BlobCache}.
+ *
+ * <p>Successful GET requests are tested in conjunction wit the PUT requests.
+ */
+public class BlobCacheGetTest extends TestLogger {
+
+	private final Random rnd = new Random();
+
+	@Rule
+	public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+	@Rule
+	public final ExpectedException exception = ExpectedException.none();
+
+	@Test
+	public void testGetFailsDuringLookup1() throws IOException {
+		testGetFailsDuringLookup(null, new JobID(), false);
+	}
+
+	@Test
+	public void testGetFailsDuringLookup2() throws IOException {
+		testGetFailsDuringLookup(new JobID(), new JobID(), false);
+	}
+
+	@Test
+	public void testGetFailsDuringLookup3() throws IOException {
+		testGetFailsDuringLookup(new JobID(), null, false);
+	}
+
+	@Test
+	public void testGetFailsDuringLookupHa() throws IOException {
+		testGetFailsDuringLookup(new JobID(), new JobID(), true);
+	}
+
+	/**
+	 * Checks the correct result if a GET operation fails during the lookup of the file.
+	 *
+	 * @param jobId1 first job ID or <tt>null</tt> if job-unrelated
+	 * @param jobId2 second job ID different to <tt>jobId1</tt>
+	 */
+	private void testGetFailsDuringLookup(final JobID jobId1, final JobID jobId2, boolean highAvailability)
+		throws IOException {
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+
+			// put content addressable (like libraries)
+			BlobKey key = put(server, jobId1, data, highAvailability);
+			assertNotNull(key);
+
+			// delete file to make sure that GET requests fail
+			File blobFile = server.getStorageLocation(jobId1, key);
+			assertTrue(blobFile.delete());
+
+			// issue a GET request that fails
+			verifyDeleted(cache, jobId1, key, highAvailability);
+
+			// add the same data under a second jobId
+			BlobKey key2 = put(server, jobId2, data, highAvailability);
+			assertNotNull(key);
+			assertEquals(key, key2);
+
+			// request for jobId2 should succeed
+			get(cache, jobId2, key, highAvailability);
+			// request for jobId1 should still fail
+			verifyDeleted(cache, jobId1, key, highAvailability);
+
+			// delete on cache, try to retrieve again
+			if (highAvailability) {
+				blobFile = cache.getPermanentBlobStore().getStorageLocation(jobId2, key);
+			} else {
+				blobFile = cache.getTransientBlobStore().getStorageLocation(jobId2, key);
+			}
+			assertTrue(blobFile.delete());
+			get(cache, jobId2, key, highAvailability);
+
+			// delete on cache and server, verify that it is not accessible anymore
+			if (highAvailability) {
+				blobFile = cache.getPermanentBlobStore().getStorageLocation(jobId2, key);
+			} else {
+				blobFile = cache.getTransientBlobStore().getStorageLocation(jobId2, key);
+			}
+			assertTrue(blobFile.delete());
+			blobFile = server.getStorageLocation(jobId2, key);
+			assertTrue(blobFile.delete());
+			verifyDeleted(cache, jobId2, key, highAvailability);
+		}
+	}
+
+	@Test
+	public void testGetFailsIncomingNoJob() throws IOException {
+		testGetFailsIncoming(null, false);
+	}
+
+	@Test
+	public void testGetFailsIncomingForJob() throws IOException {
+		testGetFailsIncoming(new JobID(), false);
+	}
+
+	@Test
+	public void testGetFailsIncomingForJobHa() throws IOException {
+		testGetFailsIncoming(new JobID(), true);
+	}
+
+	/**
+	 * Retrieves a BLOB via a {@link BlobCache} which cannot create incoming files. File transfers
+	 * should fail.
+	 *
+	 * @param jobId
+	 * 		job id
+	 * @param highAvailability
+	 * 		whether to retrieve a permanent blob (<tt>true</tt>, via {@link
+	 * 		PermanentBlobCache#getHAFile(JobID, BlobKey)}) or not (<tt>false</tt>, via {@link
+	 * 		TransientBlobCache#getFile(JobID, BlobKey)})
+	 */
+	private void testGetFailsIncoming(@Nullable final JobID jobId, boolean highAvailability)
+			throws IOException {
+		assumeTrue(!OperatingSystem.isWindows()); //setWritable doesn't work on Windows.
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		File tempFileDir = null;
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			// store the data on the server
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+			BlobKey blobKey = put(server, jobId, data, highAvailability);
+
+			// make sure the blob cache cannot create any files in its storage dir
+			if (highAvailability) {
+				tempFileDir =
+					cache.getPermanentBlobStore().createTemporaryFilename().getParentFile();
+			} else {
+				tempFileDir =
+					cache.getTransientBlobStore().createTemporaryFilename().getParentFile();
+			}
+			assertTrue(tempFileDir.setExecutable(true, false));
+			assertTrue(tempFileDir.setReadable(true, false));
+			assertTrue(tempFileDir.setWritable(false, false));
+
+			// request the file from the server via the cache
+			exception.expect(IOException.class);
+			exception.expectMessage("Failed to fetch BLOB ");
+
+			try {
+				get(cache, jobId, blobKey, highAvailability);
+			} finally {
+				HashSet<String> expectedDirs = new HashSet<>();
+				expectedDirs.add("incoming");
+				if (jobId != null) {
+					// only the incoming and job directory should exist (no job directory!)
+					expectedDirs.add(JOB_DIR_PREFIX + jobId);
+					File storageDir = tempFileDir.getParentFile();
+					String[] actualDirs = storageDir.list();
+					assertNotNull(actualDirs);
+					assertEquals(expectedDirs, new HashSet<>(Arrays.asList(actualDirs)));
+
+					// job directory should be empty
+					File jobDir = new File(tempFileDir.getParentFile(), JOB_DIR_PREFIX + jobId);
+					assertArrayEquals(new String[] {}, jobDir.list());
+				} else {
+					// only the incoming and no_job directory should exist (no job directory!)
+					expectedDirs.add(NO_JOB_DIR_PREFIX);
+					File storageDir = tempFileDir.getParentFile();
+					String[] actualDirs = storageDir.list();
+					assertNotNull(actualDirs);
+					assertEquals(expectedDirs, new HashSet<>(Arrays.asList(actualDirs)));
+
+					// no_job directory should be empty
+					File noJobDir = new File(tempFileDir.getParentFile(), NO_JOB_DIR_PREFIX);
+					assertArrayEquals(new String[] {}, noJobDir.list());
+				}
+			}
+		} finally {
+			// set writable again to make sure we can remove the directory
+			if (tempFileDir != null) {
+				//noinspection ResultOfMethodCallIgnored
+				tempFileDir.setWritable(true, false);
+			}
+		}
+	}
+
+	@Test
+	public void testGetFailsStoreNoJob() throws IOException {
+		testGetFailsStore(null, false);
+	}
+
+	@Test
+	public void testGetFailsStoreForJob() throws IOException {
+		testGetFailsStore(new JobID(), false);
+	}
+
+	@Test
+	public void testGetFailsStoreForJobHa() throws IOException {
+		testGetFailsStore(new JobID(), true);
+	}
+
+	/**
+	 * Retrieves a BLOB via a {@link BlobCache} which cannot create the final storage file. File
+	 * transfers should fail.
+	 *
+	 * @param jobId
+	 * 		job id
+	 * @param highAvailability
+	 * 		whether to retrieve a permanent blob (<tt>true</tt>, via {@link
+	 * 		PermanentBlobCache#getHAFile(JobID, BlobKey)}) or not (<tt>false</tt>, via {@link
+	 * 		TransientBlobCache#getFile(JobID, BlobKey)})
+	 */
+	private void testGetFailsStore(@Nullable final JobID jobId, boolean highAvailability)
+			throws IOException {
+		assumeTrue(!OperatingSystem.isWindows()); //setWritable doesn't work on Windows.
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		File jobStoreDir = null;
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			// store the data on the server
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+			BlobKey blobKey = put(server, jobId, data, highAvailability);
+
+			// make sure the blob cache cannot create any files in its storage dir
+			if (highAvailability) {
+				jobStoreDir = cache.getPermanentBlobStore().getStorageLocation(jobId, new BlobKey())
+					.getParentFile();
+			} else {
+				jobStoreDir = cache.getTransientBlobStore().getStorageLocation(jobId, new BlobKey())
+					.getParentFile();
+			}
+			assertTrue(jobStoreDir.setExecutable(true, false));
+			assertTrue(jobStoreDir.setReadable(true, false));
+			assertTrue(jobStoreDir.setWritable(false, false));
+
+			// request the file from the server via the cache
+			exception.expect(AccessDeniedException.class);
+
+			try {
+				get(cache, jobId, blobKey, highAvailability);
+			} finally {
+				// there should be no remaining incoming files
+				File incomingFileDir = new File(jobStoreDir.getParent(), "incoming");
+				assertArrayEquals(new String[] {}, incomingFileDir.list());
+
+				// there should be no files in the job directory
+				assertArrayEquals(new String[] {}, jobStoreDir.list());
+			}
+		} finally {
+			// set writable again to make sure we can remove the directory
+			if (jobStoreDir != null) {
+				//noinspection ResultOfMethodCallIgnored
+				jobStoreDir.setWritable(true, false);
+			}
+		}
+	}
+
+	/**
+	 * Retrieves a BLOB from the HA store to a {@link BlobServer} whose HA store does not contain
+	 * the file. File transfers should fail.
+	 */
+	@Test
+	public void testGetFailsHaStoreForJobHa() throws IOException {
+		final JobID jobId = new JobID();
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			// store the data on the server (and blobStore), remove from local server store
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+			BlobKey blobKey = put(server, jobId, data, true);
+			assertTrue(server.getStorageLocation(jobId, blobKey).delete());
+
+			File tempFileDir = server.createTemporaryFilename().getParentFile();
+
+			// request the file from the server via the cache
+			exception.expect(IOException.class);
+			exception.expectMessage("Failed to fetch BLOB ");
+
+			try {
+				get(cache, jobId, blobKey, true);
+			} finally {
+				HashSet<String> expectedDirs = new HashSet<>();
+				expectedDirs.add("incoming");
+				expectedDirs.add(JOB_DIR_PREFIX + jobId);
+				// only the incoming and job directory should exist (no job directory!)
+				File storageDir = tempFileDir.getParentFile();
+				String[] actualDirs = storageDir.list();
+				assertNotNull(actualDirs);
+				assertEquals(expectedDirs, new HashSet<>(Arrays.asList(actualDirs)));
+
+				// job directory should be empty
+				File jobDir = new File(tempFileDir.getParentFile(), JOB_DIR_PREFIX + jobId);
+				assertArrayEquals(new String[] {}, jobDir.list());
+			}
+		}
+	}
+
+	/**
+	 * FLINK-6020
+	 *
+	 * <p>Tests that concurrent get operations don't concurrently access the BlobStore to download a blob.
+	 */
+	@Test
+	public void testConcurrentGetOperationsNoJob() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentGetOperations(null, false, false);
+	}
+
+	@Test
+	public void testConcurrentGetOperationsForJob() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentGetOperations(new JobID(), false, false);
+	}
+
+	@Test
+	public void testConcurrentGetOperationsForJobHa() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentGetOperations(new JobID(), true, false);
+	}
+
+	@Test
+	public void testConcurrentGetOperationsForJobHa2() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentGetOperations(new JobID(), true, true);
+	}
+
+	/**
+	 * [FLINK-6020] Tests that concurrent get operations don't concurrently access the BlobStore to
+	 * download a blob.
+	 *
+	 * @param jobId
+	 * 		job ID to use (or <tt>null</tt> if job-unrelated)
+	 * @param highAvailability
+	 * 		whether to use permanent (<tt>true</tt>) or transient BLOBs (<tt>false</tt>)
+	 * @param cacheAccessesHAStore
+	 * 		whether the cache has access to the {@link BlobServer}'s HA store or not
+	 */
+	private void testConcurrentGetOperations(final JobID jobId, final boolean highAvailability,
+			final boolean cacheAccessesHAStore)
+			throws IOException, InterruptedException, ExecutionException {
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		final BlobStore blobStoreServer = mock(BlobStore.class);
+		final BlobStore blobStoreCache = mock(BlobStore.class);
+
+		final int numberConcurrentGetOperations = 3;
+		final List<CompletableFuture<File>> getOperations = new ArrayList<>(numberConcurrentGetOperations);
+
+		final byte[] data = {1, 2, 3, 4, 99, 42};
+
+		MessageDigest md = BlobUtils.createMessageDigest();
+
+		// create the correct blob key by hashing our input data
+		final BlobKey blobKey = new BlobKey(md.digest(data));
+
+		final ExecutorService executor = Executors.newFixedThreadPool(numberConcurrentGetOperations);
+
+		try (
+			final BlobServer server = new BlobServer(config, blobStoreServer);
+			final BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, cacheAccessesHAStore ? blobStoreServer : blobStoreCache)) {
+
+			server.start();
+
+			// upload data first
+			assertEquals(blobKey, put(server, jobId, data, highAvailability));
+
+			// now try accessing it concurrently (only HA mode will be able to retrieve it from HA store!)
+			for (int i = 0; i < numberConcurrentGetOperations; i++) {
+				CompletableFuture<File> getOperation = CompletableFuture
+					.supplyAsync(
+						() -> {
+							try {
+								File file = get(cache, jobId, blobKey, highAvailability);
+								// check that we have read the right data
+								validateGetAndClose(new FileInputStream(file), data);
+								return file;
+							} catch (IOException e) {
+								throw new CompletionException(new FlinkException(
+									"Could not upload blob.", e));
+							}
+						}, executor);
+
+				getOperations.add(getOperation);
+			}
+
+			FutureUtils.ConjunctFuture<Collection<File>> filesFuture = FutureUtils.combineAll(getOperations);
+
+			// wait until all operations have completed and check that no exception was thrown
+			filesFuture.get();
+		} finally {
+			executor.shutdownNow();
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/flink/blob/071e27f7/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCachePutTest.java
----------------------------------------------------------------------
diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCachePutTest.java b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCachePutTest.java
new file mode 100644
index 0000000..7066b95
--- /dev/null
+++ b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCachePutTest.java
@@ -0,0 +1,808 @@
+/*
+ * 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.runtime.blob;
+
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.configuration.BlobServerOptions;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.core.fs.Path;
+import org.apache.flink.core.testutils.CheckedThread;
+import org.apache.flink.runtime.concurrent.FutureUtils;
+import org.apache.flink.util.FlinkException;
+import org.apache.flink.util.OperatingSystem;
+import org.apache.flink.util.TestLogger;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import javax.annotation.Nullable;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Supplier;
+
+import static org.apache.flink.runtime.blob.BlobServerPutTest.BlockingInputStream;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.ChunkedInputStream;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.put;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.verifyContents;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for successful and failing PUT operations against the BLOB server,
+ * and successful GET operations.
+ */
+public class BlobCachePutTest extends TestLogger {
+
+	private final Random rnd = new Random();
+
+	@Rule
+	public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+	@Rule
+	public final ExpectedException exception = ExpectedException.none();
+
+	// --- concurrency tests for utility methods which could fail during the put operation ---
+
+	/**
+	 * Checked thread that calls {@link TransientBlobCache#getStorageLocation(JobID, BlobKey)}.
+	 */
+	public static class TransientBlobCacheGetStorageLocation extends CheckedThread {
+		private final TransientBlobCache cache;
+		private final JobID jobId;
+		private final BlobKey key;
+
+		TransientBlobCacheGetStorageLocation(
+				TransientBlobCache cache, @Nullable JobID jobId, BlobKey key) {
+			this.cache = cache;
+			this.jobId = jobId;
+			this.key = key;
+		}
+
+		@Override
+		public void go() throws Exception {
+			cache.getStorageLocation(jobId, key);
+		}
+	}
+
+	/**
+	 * Checked thread that calls {@link PermanentBlobCache#getStorageLocation(JobID, BlobKey)}.
+	 */
+	public static class PermanentBlobCacheGetStorageLocation extends CheckedThread {
+		private final PermanentBlobCache cache;
+		private final JobID jobId;
+		private final BlobKey key;
+
+		PermanentBlobCacheGetStorageLocation(PermanentBlobCache cache, JobID jobId, BlobKey key) {
+			this.cache = cache;
+			this.jobId = jobId;
+			this.key = key;
+		}
+
+		@Override
+		public void go() throws Exception {
+			cache.getStorageLocation(jobId, key);
+		}
+	}
+
+	/**
+	 * Tests concurrent calls to {@link TransientBlobCache#getStorageLocation(JobID, BlobKey)}.
+	 */
+	@Test
+	public void testTransientBlobCacheGetStorageLocationConcurrentNoJob() throws Exception {
+		testTransientBlobCacheGetStorageLocationConcurrent(null);
+	}
+
+	/**
+	 * Tests concurrent calls to {@link TransientBlobCache#getStorageLocation(JobID, BlobKey)}.
+	 */
+	@Test
+	public void testTransientBlobCacheGetStorageLocationConcurrentForJob() throws Exception {
+		testTransientBlobCacheGetStorageLocationConcurrent(new JobID());
+	}
+
+	private void testTransientBlobCacheGetStorageLocationConcurrent(
+			@Nullable final JobID jobId) throws Exception {
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (BlobServer server = new BlobServer(config, new VoidBlobStore());
+			final TransientBlobCache cache = new TransientBlobCache(
+				new InetSocketAddress("localhost", server.getPort()), config)) {
+
+			server.start();
+
+			BlobKey key = new BlobKey();
+			CheckedThread[] threads = new CheckedThread[] {
+				new TransientBlobCacheGetStorageLocation(cache, jobId, key),
+				new TransientBlobCacheGetStorageLocation(cache, jobId, key),
+				new TransientBlobCacheGetStorageLocation(cache, jobId, key)
+			};
+			checkedThreadSimpleTest(threads);
+		}
+	}
+
+	/**
+	 * Tests concurrent calls to {@link PermanentBlobCache#getStorageLocation(JobID, BlobKey)}.
+	 */
+	@Test
+	public void testPermanentBlobCacheGetStorageLocationConcurrentForJob() throws Exception {
+		final JobID jobId = new JobID();
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (BlobServer server = new BlobServer(config, new VoidBlobStore());
+			final PermanentBlobCache cache = new PermanentBlobCache(
+				new InetSocketAddress("localhost", server.getPort()), config,
+				new VoidBlobStore())) {
+
+			server.start();
+
+			BlobKey key = new BlobKey();
+			CheckedThread[] threads = new CheckedThread[] {
+				new PermanentBlobCacheGetStorageLocation(cache, jobId, key),
+				new PermanentBlobCacheGetStorageLocation(cache, jobId, key),
+				new PermanentBlobCacheGetStorageLocation(cache, jobId, key)
+			};
+			checkedThreadSimpleTest(threads);
+		}
+	}
+
+	/**
+	 * Helper method to first start all threads and then wait for their completion.
+	 *
+	 * @param threads threads to use
+	 * @throws Exception exceptions that are thrown from the threads
+	 */
+	private void checkedThreadSimpleTest(CheckedThread[] threads)
+		throws Exception {
+
+		// start all threads
+		for (CheckedThread t: threads) {
+			t.start();
+		}
+
+		// wait for thread completion and check exceptions
+		for (CheckedThread t: threads) {
+			t.sync();
+		}
+	}
+
+	// --------------------------------------------------------------------------------------------
+
+	@Test
+	public void testPutBufferSuccessfulGet1() throws IOException {
+		testPutBufferSuccessfulGet(null, null, false);
+	}
+
+	@Test
+	public void testPutBufferSuccessfulGet2() throws IOException {
+		testPutBufferSuccessfulGet(null, new JobID(), false);
+	}
+
+	@Test
+	public void testPutBufferSuccessfulGet3() throws IOException {
+		testPutBufferSuccessfulGet(new JobID(), new JobID(), false);
+	}
+
+	@Test
+	public void testPutBufferSuccessfulGet4() throws IOException {
+		testPutBufferSuccessfulGet(new JobID(), null, false);
+	}
+
+	@Test
+	public void testPutBufferSuccessfulGetHa() throws IOException {
+		testPutBufferSuccessfulGet(new JobID(), new JobID(), true);
+	}
+
+	/**
+	 * Uploads two byte arrays for different jobs into the server via the {@link BlobCache}. File
+	 * transfers should be successful.
+	 *
+	 * @param jobId1
+	 * 		first job id
+	 * @param jobId2
+	 * 		second job id
+	 * @param highAvailability
+	 * 		whether to upload a permanent blob (<tt>true</tt>, via {@link
+	 * 		BlobClient#uploadJarFiles(InetSocketAddress, Configuration, JobID, List)}) or not
+	 * 		(<tt>false</tt>, via {@link TransientBlobCache#put(JobID, byte[])}
+	 */
+	private void testPutBufferSuccessfulGet(
+			@Nullable JobID jobId1, @Nullable JobID jobId2, boolean highAvailability)
+			throws IOException {
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+			byte[] data2 = Arrays.copyOfRange(data, 10, 54);
+
+			// put data for jobId1 and verify
+			BlobKey key1a = put(cache, jobId1, data, highAvailability);
+			assertNotNull(key1a);
+
+			BlobKey key1b = put(cache, jobId1, data2, highAvailability);
+			assertNotNull(key1b);
+
+			// files should be available on the server
+			verifyContents(server, jobId1, key1a, data, highAvailability);
+			verifyContents(server, jobId1, key1b, data2, highAvailability);
+
+			// now put data for jobId2 and verify that both are ok
+			BlobKey key2a = put(cache, jobId2, data, highAvailability);
+			assertNotNull(key2a);
+			assertEquals(key1a, key2a);
+
+			BlobKey key2b = put(cache, jobId2, data2, highAvailability);
+			assertNotNull(key2b);
+			assertEquals(key1b, key2b);
+
+			// verify the accessibility and the BLOB contents
+			verifyContents(server, jobId1, key1a, data, highAvailability);
+			verifyContents(server, jobId1, key1b, data2, highAvailability);
+			verifyContents(server, jobId2, key2a, data, highAvailability);
+			verifyContents(server, jobId2, key2b, data2, highAvailability);
+		}
+	}
+
+	// --------------------------------------------------------------------------------------------
+
+	@Test
+	public void testPutStreamSuccessfulGet1() throws IOException {
+		testPutStreamSuccessfulGet(null, null);
+	}
+
+	@Test
+	public void testPutStreamSuccessfulGet2() throws IOException {
+		testPutStreamSuccessfulGet(null, new JobID());
+	}
+
+	@Test
+	public void testPutStreamSuccessfulGet3() throws IOException {
+		testPutStreamSuccessfulGet(new JobID(), new JobID());
+	}
+
+	@Test
+	public void testPutStreamSuccessfulGet4() throws IOException {
+		testPutStreamSuccessfulGet(new JobID(), null);
+	}
+
+	/**
+	 * Uploads two file streams for different jobs into the server via the {@link BlobCache}. File
+	 * transfers should be successful.
+	 *
+	 * <p>Note that high-availability uploads of streams is currently only possible at the {@link
+	 * BlobServer}.
+	 *
+	 * @param jobId1
+	 * 		first job id
+	 * @param jobId2
+	 * 		second job id
+	 */
+	private void testPutStreamSuccessfulGet(@Nullable JobID jobId1, @Nullable JobID jobId2)
+			throws IOException {
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+			byte[] data2 = Arrays.copyOfRange(data, 10, 54);
+
+			// put data for jobId1 and verify
+			BlobKey key1a = put(cache, jobId1, new ByteArrayInputStream(data), false);
+			assertNotNull(key1a);
+
+			BlobKey key1b = put(cache, jobId1, new ByteArrayInputStream(data2), false);
+			assertNotNull(key1b);
+
+			// files should be available on the server
+			verifyContents(server, jobId1, key1a, data, false);
+			verifyContents(server, jobId1, key1b, data2, false);
+
+			// now put data for jobId2 and verify that both are ok
+			BlobKey key2a = put(cache, jobId2, new ByteArrayInputStream(data), false);
+			assertNotNull(key2a);
+			assertEquals(key1a, key2a);
+
+			BlobKey key2b = put(cache, jobId2, new ByteArrayInputStream(data2), false);
+			assertNotNull(key2b);
+			assertEquals(key1b, key2b);
+
+			// verify the accessibility and the BLOB contents
+			verifyContents(server, jobId1, key1a, data, false);
+			verifyContents(server, jobId1, key1b, data2, false);
+			verifyContents(server, jobId2, key2a, data, false);
+			verifyContents(server, jobId2, key2b, data2, false);
+		}
+	}
+
+	// --------------------------------------------------------------------------------------------
+
+	@Test
+	public void testPutChunkedStreamSuccessfulGet1() throws IOException {
+		testPutChunkedStreamSuccessfulGet(null, null);
+	}
+
+	@Test
+	public void testPutChunkedStreamSuccessfulGet2() throws IOException {
+		testPutChunkedStreamSuccessfulGet(null, new JobID());
+	}
+
+	@Test
+	public void testPutChunkedStreamSuccessfulGet3() throws IOException {
+		testPutChunkedStreamSuccessfulGet(new JobID(), new JobID());
+	}
+
+	@Test
+	public void testPutChunkedStreamSuccessfulGet4() throws IOException {
+		testPutChunkedStreamSuccessfulGet(new JobID(), null);
+	}
+
+	/**
+	 * Uploads two chunked file streams for different jobs into the server via the {@link
+	 * BlobCache}. File transfers should be successful.
+	 *
+	 * <p>Note that high-availability uploads of streams is currently only possible at the {@link
+	 * BlobServer}.
+	 *
+	 * @param jobId1
+	 * 		first job id
+	 * @param jobId2
+	 * 		second job id
+	 */
+	private void testPutChunkedStreamSuccessfulGet(
+			@Nullable JobID jobId1, @Nullable JobID jobId2) throws IOException {
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+			byte[] data2 = Arrays.copyOfRange(data, 10, 54);
+
+			// put data for jobId1 and verify
+			BlobKey key1a = put(cache, jobId1, new ChunkedInputStream(data, 19), false);
+			assertNotNull(key1a);
+
+			BlobKey key1b = put(cache, jobId1, new ChunkedInputStream(data2, 19), false);
+			assertNotNull(key1b);
+
+			// files should be available on the server
+			verifyContents(server, jobId1, key1a, data, false);
+			verifyContents(server, jobId1, key1b, data2, false);
+
+			// now put data for jobId2 and verify that both are ok
+			BlobKey key2a = put(cache, jobId2, new ChunkedInputStream(data, 19), false);
+			assertNotNull(key2a);
+			assertEquals(key1a, key2a);
+
+			BlobKey key2b = put(cache, jobId2, new ChunkedInputStream(data2, 19), false);
+			assertNotNull(key2b);
+			assertEquals(key1b, key2b);
+
+			// verify the accessibility and the BLOB contents
+			verifyContents(server, jobId1, key1a, data, false);
+			verifyContents(server, jobId1, key1b, data2, false);
+			verifyContents(server, jobId2, key2a, data, false);
+			verifyContents(server, jobId2, key2b, data2, false);
+		}
+	}
+
+	// --------------------------------------------------------------------------------------------
+
+	@Test
+	public void testPutBufferFailsNoJob() throws IOException {
+		testPutBufferFails(null, false);
+	}
+
+	@Test
+	public void testPutBufferFailsForJob() throws IOException {
+		testPutBufferFails(new JobID(), false);
+	}
+
+	@Test
+	public void testPutBufferFailsForJobHa() throws IOException {
+		testPutBufferFails(new JobID(), true);
+	}
+
+	/**
+	 * Uploads a byte array to a server which cannot create any files via the {@link BlobCache}.
+	 * File transfers should fail.
+	 *
+	 * @param jobId
+	 * 		job id
+	 * @param highAvailability
+	 * 		whether to upload a permanent blob (<tt>true</tt>, via {@link
+	 * 		BlobClient#uploadJarFiles(InetSocketAddress, Configuration, JobID, List)}) or not
+	 * 		(<tt>false</tt>, via {@link TransientBlobCache#put(JobID, byte[])}
+	 */
+	private void testPutBufferFails(@Nullable final JobID jobId, boolean highAvailability)
+			throws IOException {
+		assumeTrue(!OperatingSystem.isWindows()); //setWritable doesn't work on Windows.
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		File tempFileDir = null;
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			// make sure the blob server cannot create any files in its storage dir
+			tempFileDir = server.createTemporaryFilename().getParentFile().getParentFile();
+			assertTrue(tempFileDir.setExecutable(true, false));
+			assertTrue(tempFileDir.setReadable(true, false));
+			assertTrue(tempFileDir.setWritable(false, false));
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+
+			// upload the file to the server via the cache
+			exception.expect(IOException.class);
+			exception.expectMessage("PUT operation failed: ");
+
+			put(cache, jobId, data, highAvailability);
+
+		} finally {
+			// set writable again to make sure we can remove the directory
+			if (tempFileDir != null) {
+				//noinspection ResultOfMethodCallIgnored
+				tempFileDir.setWritable(true, false);
+			}
+		}
+	}
+
+	@Test
+	public void testPutBufferFailsIncomingNoJob() throws IOException {
+		testPutBufferFailsIncoming(null, false);
+	}
+
+	@Test
+	public void testPutBufferFailsIncomingForJob() throws IOException {
+		testPutBufferFailsIncoming(new JobID(), false);
+	}
+
+	@Test
+	public void testPutBufferFailsIncomingForJobHa() throws IOException {
+		testPutBufferFailsIncoming(new JobID(), true);
+	}
+
+	/**
+	 * Uploads a byte array to a server which cannot create incoming files via the {@link
+	 * BlobCache}. File transfers should fail.
+	 *
+	 * @param jobId
+	 * 		job id
+	 * @param highAvailability
+	 * 		whether to upload a permanent blob (<tt>true</tt>, via {@link
+	 * 		BlobClient#uploadJarFiles(InetSocketAddress, Configuration, JobID, List)}) or not
+	 * 		(<tt>false</tt>, via {@link TransientBlobCache#put(JobID, byte[])}
+	 */
+	private void testPutBufferFailsIncoming(@Nullable final JobID jobId, boolean highAvailability)
+			throws IOException {
+		assumeTrue(!OperatingSystem.isWindows()); //setWritable doesn't work on Windows.
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		File tempFileDir = null;
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			// make sure the blob server cannot create any files in its storage dir
+			tempFileDir = server.createTemporaryFilename().getParentFile();
+			assertTrue(tempFileDir.setExecutable(true, false));
+			assertTrue(tempFileDir.setReadable(true, false));
+			assertTrue(tempFileDir.setWritable(false, false));
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+
+			// upload the file to the server via the cache
+			exception.expect(IOException.class);
+			exception.expectMessage("PUT operation failed: ");
+
+			try {
+				put(cache, jobId, data, highAvailability);
+			} finally {
+				File storageDir = tempFileDir.getParentFile();
+				// only the incoming directory should exist (no job directory!)
+				assertArrayEquals(new String[] {"incoming"}, storageDir.list());
+			}
+		} finally {
+			// set writable again to make sure we can remove the directory
+			if (tempFileDir != null) {
+				//noinspection ResultOfMethodCallIgnored
+				tempFileDir.setWritable(true, false);
+			}
+		}
+	}
+
+	@Test
+	public void testPutBufferFailsStoreNoJob() throws IOException {
+		testPutBufferFailsStore(null, false);
+	}
+
+	@Test
+	public void testPutBufferFailsStoreForJob() throws IOException {
+		testPutBufferFailsStore(new JobID(), false);
+	}
+
+	@Test
+	public void testPutBufferFailsStoreForJobHa() throws IOException {
+		testPutBufferFailsStore(new JobID(), true);
+	}
+
+	/**
+	 * Uploads a byte array to a server which cannot create files via the {@link BlobCache}. File
+	 * transfers should fail.
+	 *
+	 * @param jobId
+	 * 		job id
+	 * @param highAvailability
+	 * 		whether to upload a permanent blob (<tt>true</tt>, via {@link
+	 * 		BlobClient#uploadJarFiles(InetSocketAddress, Configuration, JobID, List)}) or not
+	 * 		(<tt>false</tt>, via {@link TransientBlobCache#put(JobID, byte[])}
+	 */
+	private void testPutBufferFailsStore(@Nullable final JobID jobId, boolean highAvailability)
+			throws IOException {
+		assumeTrue(!OperatingSystem.isWindows()); //setWritable doesn't work on Windows.
+
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		File jobStoreDir = null;
+		try (
+			BlobServer server = new BlobServer(config, new VoidBlobStore());
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
+
+			server.start();
+
+			// make sure the blob server cannot create any files in its storage dir
+			jobStoreDir = server.getStorageLocation(jobId, new BlobKey()).getParentFile();
+			assertTrue(jobStoreDir.setExecutable(true, false));
+			assertTrue(jobStoreDir.setReadable(true, false));
+			assertTrue(jobStoreDir.setWritable(false, false));
+
+			byte[] data = new byte[2000000];
+			rnd.nextBytes(data);
+
+			// upload the file to the server via the cache
+			exception.expect(IOException.class);
+			exception.expectMessage("PUT operation failed: ");
+
+			try {
+				put(cache, jobId, data, highAvailability);
+			} finally {
+				// there should be no remaining incoming files
+				File incomingFileDir = new File(jobStoreDir.getParent(), "incoming");
+				assertArrayEquals(new String[] {}, incomingFileDir.list());
+
+				// there should be no files in the job directory
+				assertArrayEquals(new String[] {}, jobStoreDir.list());
+			}
+		} finally {
+			// set writable again to make sure we can remove the directory
+			if (jobStoreDir != null) {
+				//noinspection ResultOfMethodCallIgnored
+				jobStoreDir.setWritable(true, false);
+			}
+		}
+	}
+
+	@Test
+	public void testConcurrentPutOperationsNoJob() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentPutOperations(null, false);
+	}
+
+	@Test
+	public void testConcurrentPutOperationsForJob() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentPutOperations(new JobID(), false);
+	}
+
+	@Test
+	public void testConcurrentPutOperationsForJobHa() throws IOException, ExecutionException, InterruptedException {
+		testConcurrentPutOperations(new JobID(), true);
+	}
+
+	/**
+	 * [FLINK-6020]
+	 * Tests that concurrent put operations will only upload the file once to the {@link BlobStore}
+	 * and that the files are not corrupt at any time.
+	 *
+	 * @param jobId
+	 * 		job ID to use (or <tt>null</tt> if job-unrelated)
+	 * @param highAvailability
+	 * 		whether to use permanent (<tt>true</tt>) or transient BLOBs (<tt>false</tt>)
+	 */
+	private void testConcurrentPutOperations(
+			@Nullable final JobID jobId, final boolean highAvailability)
+			throws IOException, InterruptedException, ExecutionException {
+		final Configuration config = new Configuration();
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+
+		final BlobStore blobStoreServer = mock(BlobStore.class);
+		final BlobStore blobStoreCache = mock(BlobStore.class);
+
+		int concurrentPutOperations = 2;
+		int dataSize = 1024;
+
+		final CountDownLatch countDownLatch = new CountDownLatch(concurrentPutOperations);
+		final byte[] data = new byte[dataSize];
+
+		final List<Path> jars;
+		if (highAvailability) {
+			// implement via JAR file upload instead:
+			File tmpFile = temporaryFolder.newFile();
+			FileUtils.writeByteArrayToFile(tmpFile, data);
+			jars = Collections.singletonList(new Path(tmpFile.getAbsolutePath()));
+		} else {
+			jars = null;
+		}
+
+		Collection<CompletableFuture<BlobKey>> allFutures = new ArrayList<>(concurrentPutOperations);
+
+		ExecutorService executor = Executors.newFixedThreadPool(concurrentPutOperations);
+
+		try (
+			final BlobServer server = new BlobServer(config, blobStoreServer);
+			final BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, blobStoreCache)) {
+
+			server.start();
+
+			// for highAvailability
+			final InetSocketAddress serverAddress =
+				new InetSocketAddress("localhost", server.getPort());
+			// uploading HA BLOBs works on BlobServer only (and, for now, via the BlobClient)
+
+			for (int i = 0; i < concurrentPutOperations; i++) {
+				final Supplier<BlobKey> callable;
+				if (highAvailability) {
+					// cannot use a blocking stream here (upload only possible via files)
+					callable =
+						() -> {
+							try {
+								List<BlobKey> keys =
+									BlobClient.uploadJarFiles(serverAddress, config, jobId, jars);
+								assertEquals(1, keys.size());
+								BlobKey uploadedKey = keys.get(0);
+								// check the uploaded file's contents (concurrently)
+								verifyContents(server, jobId, uploadedKey, data, true);
+								return uploadedKey;
+							} catch (IOException e) {
+								throw new CompletionException(new FlinkException(
+									"Could not upload blob.", e));
+							}
+						};
+
+				} else {
+					callable =
+						() -> {
+							try {
+								BlockingInputStream inputStream =
+									new BlockingInputStream(countDownLatch, data);
+								BlobKey uploadedKey = put(cache, jobId, inputStream, false);
+								// check the uploaded file's contents (concurrently)
+								verifyContents(server, jobId, uploadedKey, data, false);
+								return uploadedKey;
+							} catch (IOException e) {
+								throw new CompletionException(new FlinkException(
+									"Could not upload blob.", e));
+							}
+					};
+				}
+				CompletableFuture<BlobKey> putFuture = CompletableFuture
+					.supplyAsync(callable, executor);
+
+				allFutures.add(putFuture);
+			}
+
+			FutureUtils.ConjunctFuture<Collection<BlobKey>> conjunctFuture = FutureUtils.combineAll(allFutures);
+
+			// wait until all operations have completed and check that no exception was thrown
+			Collection<BlobKey> blobKeys = conjunctFuture.get();
+
+			Iterator<BlobKey> blobKeyIterator = blobKeys.iterator();
+
+			assertTrue(blobKeyIterator.hasNext());
+
+			BlobKey blobKey = blobKeyIterator.next();
+
+			// make sure that all blob keys are the same
+			while (blobKeyIterator.hasNext()) {
+				assertEquals(blobKey, blobKeyIterator.next());
+			}
+
+			// check the uploaded file's contents
+			verifyContents(server, jobId, blobKey, data, highAvailability);
+
+			// check that we only uploaded the file once to the blob store
+			if (highAvailability) {
+				verify(blobStoreServer, times(1)).put(any(File.class), eq(jobId), eq(blobKey));
+			} else {
+				// can't really verify much in the other cases other than that the put operations should
+				// work and not corrupt files
+				verify(blobStoreServer, times(0)).put(any(File.class), eq(jobId), eq(blobKey));
+			}
+			// caches must not access the blob store (they are not allowed to write there)
+			verify(blobStoreCache, times(0)).put(any(File.class), eq(jobId), eq(blobKey));
+		} finally {
+			executor.shutdownNow();
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/flink/blob/071e27f7/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRecoveryTest.java
----------------------------------------------------------------------
diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRecoveryTest.java b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRecoveryTest.java
new file mode 100644
index 0000000..9f462c2
--- /dev/null
+++ b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRecoveryTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.runtime.blob;
+
+import org.apache.flink.api.common.JobID;
+import org.apache.flink.configuration.BlobServerOptions;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.CoreOptions;
+import org.apache.flink.configuration.HighAvailabilityOptions;
+import org.apache.flink.core.fs.FileSystem;
+import org.apache.flink.core.fs.Path;
+import org.apache.flink.runtime.jobmanager.HighAvailabilityMode;
+import org.apache.flink.util.TestLogger;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Random;
+
+import static org.apache.flink.runtime.blob.BlobServerGetTest.verifyDeleted;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.put;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.verifyContents;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the recovery of files of a {@link BlobCache} from a HA store.
+ */
+public class BlobCacheRecoveryTest extends TestLogger {
+
+	@Rule
+	public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+	/**
+	 * Tests that with {@link HighAvailabilityMode#ZOOKEEPER} distributed JARs are recoverable from any
+	 * participating BlobServer.
+	 */
+	@Test
+	public void testBlobCacheRecovery() throws Exception {
+		Configuration config = new Configuration();
+		config.setString(HighAvailabilityOptions.HA_MODE, "ZOOKEEPER");
+		config.setString(CoreOptions.STATE_BACKEND, "FILESYSTEM");
+		config.setString(BlobServerOptions.STORAGE_DIRECTORY, temporaryFolder.newFolder().getAbsolutePath());
+		config.setString(HighAvailabilityOptions.HA_STORAGE_PATH, temporaryFolder.newFolder().getPath());
+
+		BlobStoreService blobStoreService = null;
+
+		try {
+			blobStoreService = BlobUtils.createBlobStoreFromConfig(config);
+
+			testBlobCacheRecovery(config, blobStoreService);
+		} finally {
+			if (blobStoreService != null) {
+				blobStoreService.closeAndCleanupAllData();
+			}
+		}
+	}
+
+	/**
+	 * Helper to test that the {@link BlobServer} recovery from its HA store works.
+	 *
+	 * <p>Uploads two BLOBs to one {@link BlobServer} via a {@link BlobCache} and expects a second
+	 * {@link BlobCache} to be able to retrieve them from a second {@link BlobServer} that is
+	 * configured with the same HA store.
+	 *
+	 * @param config
+	 * 		blob server configuration (including HA settings like {@link HighAvailabilityOptions#HA_STORAGE_PATH}
+	 * 		and {@link HighAvailabilityOptions#HA_CLUSTER_ID}) used to set up <tt>blobStore</tt>
+	 * @param blobStore
+	 * 		shared HA blob store to use
+	 *
+	 * @throws IOException
+	 * 		in case of failures
+	 */
+	public static void testBlobCacheRecovery(
+			final Configuration config, final BlobStore blobStore) throws IOException {
+
+		final String clusterId = config.getString(HighAvailabilityOptions.HA_CLUSTER_ID);
+		String storagePath = config.getString(HighAvailabilityOptions.HA_STORAGE_PATH) + "/" + clusterId;
+		Random rand = new Random();
+
+		try (
+			BlobServer server0 = new BlobServer(config, blobStore);
+			BlobServer server1 = new BlobServer(config, blobStore);
+			// use VoidBlobStore as the HA store to force download from each server's HA store
+			BlobCache cache0 = new BlobCache(
+				new InetSocketAddress("localhost", server0.getPort()), config,
+				new VoidBlobStore());
+			BlobCache cache1 = new BlobCache(
+				new InetSocketAddress("localhost", server1.getPort()), config,
+				new VoidBlobStore())) {
+
+			server0.start();
+			server1.start();
+
+			// Random data
+			byte[] expected = new byte[1024];
+			rand.nextBytes(expected);
+			byte[] expected2 = Arrays.copyOfRange(expected, 32, 288);
+
+			BlobKey[] keys = new BlobKey[2];
+			BlobKey nonHAKey;
+
+			// Put job-related HA data
+			JobID[] jobId = new JobID[] { new JobID(), new JobID() };
+			keys[0] = put(cache0, jobId[0], expected, true); // Request 1
+			keys[1] = put(cache0, jobId[1], expected2, true); // Request 2
+
+			// put non-HA data
+			nonHAKey = put(cache0, jobId[0], expected2, false);
+			assertNotEquals(keys[0], nonHAKey);
+			assertEquals(keys[1], nonHAKey);
+
+			// check that the storage directory exists
+			final Path blobServerPath = new Path(storagePath, "blob");
+			FileSystem fs = blobServerPath.getFileSystem();
+			assertTrue("Unknown storage dir: " + blobServerPath, fs.exists(blobServerPath));
+
+			// Verify HA requests from cache1 (connected to server1) with no immediate access to the file
+			verifyContents(cache1, jobId[0], keys[0], expected, true);
+			verifyContents(cache1, jobId[1], keys[1], expected2, true);
+
+			// Verify non-HA file is not accessible from server1
+			verifyDeleted(cache1, jobId[0], nonHAKey, true);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/flink/blob/071e27f7/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRetriesTest.java
----------------------------------------------------------------------
diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRetriesTest.java b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRetriesTest.java
index 0060ccb..9f8bfe3 100644
--- a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRetriesTest.java
+++ b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheRetriesTest.java
@@ -28,13 +28,13 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
-import java.io.File;
+import javax.annotation.Nullable;
+
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.InetSocketAddress;
-import java.net.URL;
 
-import static org.junit.Assert.*;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.verifyContents;
+import static org.junit.Assert.fail;
 
 /**
  * Unit tests for the blob cache retrying the connection to the server.
@@ -54,7 +54,7 @@ public class BlobCacheRetriesTest extends TestLogger {
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 
-		testBlobFetchRetries(config, new VoidBlobStore(), null);
+		testBlobFetchRetries(config, new VoidBlobStore(), null, false);
 	}
 
 	/**
@@ -67,16 +67,7 @@ public class BlobCacheRetriesTest extends TestLogger {
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 
-		testBlobFetchRetries(config, new VoidBlobStore(), new JobID());
-	}
-
-	/**
-	 * A test where the connection fails twice and then the get operation succeeds
-	 * (with high availability set, job-unrelated blob).
-	 */
-	@Test
-	public void testBlobNoJobFetchRetriesHa() throws IOException {
-		testBlobFetchRetriesHa(null);
+		testBlobFetchRetries(config, new VoidBlobStore(), new JobID(), false);
 	}
 
 	/**
@@ -85,10 +76,6 @@ public class BlobCacheRetriesTest extends TestLogger {
 	 */
 	@Test
 	public void testBlobFetchRetriesHa() throws IOException {
-		testBlobFetchRetriesHa(new JobID());
-	}
-
-	private void testBlobFetchRetriesHa(final JobID jobId) throws IOException {
 		final Configuration config = new Configuration();
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
@@ -101,7 +88,7 @@ public class BlobCacheRetriesTest extends TestLogger {
 		try {
 			blobStoreService = BlobUtils.createBlobStoreFromConfig(config);
 
-			testBlobFetchRetries(config, blobStoreService, jobId);
+			testBlobFetchRetries(config, blobStoreService, new JobID(), true);
 		} finally {
 			if (blobStoreService != null) {
 				blobStoreService.closeAndCleanupAllData();
@@ -118,50 +105,23 @@ public class BlobCacheRetriesTest extends TestLogger {
 	 * 		set compared to this one)
 	 */
 	private static void testBlobFetchRetries(
-			final Configuration config, final BlobStore blobStore, final JobID jobId)
-			throws IOException {
-		final byte[] data = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
+			final Configuration config, final BlobStore blobStore, @Nullable final JobID jobId,
+			boolean highAvailability) throws IOException {
 
-		BlobServer server = null;
-		BlobCache cache = null;
-		try {
+		final byte[] data = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
 
-			server = new TestingFailingBlobServer(config, blobStore, 2);
+		try (
+			BlobServer server = new TestingFailingBlobServer(config, blobStore, 2);
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
 
-			final InetSocketAddress
-				serverAddress = new InetSocketAddress("localhost", server.getPort());
+			server.start();
 
 			// upload some blob
-			BlobClient blobClient = null;
-			BlobKey key;
-			try {
-				blobClient = new BlobClient(serverAddress, config);
-
-				key = blobClient.put(jobId, data);
-			}
-			finally {
-				if (blobClient != null) {
-					blobClient.close();
-				}
-			}
-
-			cache = new BlobCache(serverAddress, config, new VoidBlobStore());
+			final BlobKey key = BlobServerPutTest.put(server, jobId, data, highAvailability);
 
 			// trigger a download - it should fail the first two times, but retry, and succeed eventually
-			File file = jobId == null ? cache.getFile(key) : cache.getFile(jobId, key);
-			URL url = file.toURI().toURL();
-			try (InputStream is = url.openStream()) {
-				byte[] received = new byte[data.length];
-				assertEquals(data.length, is.read(received));
-				assertArrayEquals(data, received);
-			}
-		} finally {
-			if (cache != null) {
-				cache.close();
-			}
-			if (server != null) {
-				server.close();
-			}
+			verifyContents(cache, jobId, key, data, highAvailability);
 		}
 	}
 
@@ -175,7 +135,7 @@ public class BlobCacheRetriesTest extends TestLogger {
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 
-		testBlobFetchWithTooManyFailures(config, new VoidBlobStore(), null);
+		testBlobFetchWithTooManyFailures(config, new VoidBlobStore(), null, false);
 	}
 
 	/**
@@ -188,28 +148,15 @@ public class BlobCacheRetriesTest extends TestLogger {
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 
-		testBlobFetchWithTooManyFailures(config, new VoidBlobStore(), new JobID());
+		testBlobFetchWithTooManyFailures(config, new VoidBlobStore(), new JobID(), false);
 	}
 
 	/**
-	 * A test where the connection fails twice and then the get operation succeeds
-	 * (with high availability set, job-unrelated blob).
-	 */
-	@Test
-	public void testBlobNoJobFetchWithTooManyFailuresHa() throws IOException {
-		testBlobFetchWithTooManyFailuresHa(null);
-	}
-
-	/**
-	 * A test where the connection fails twice and then the get operation succeeds
+	 * A test where the connection fails too often and eventually fails the GET request
 	 * (with high availability set, job-related blob).
 	 */
 	@Test
 	public void testBlobForJobFetchWithTooManyFailuresHa() throws IOException {
-		testBlobFetchWithTooManyFailuresHa(new JobID());
-	}
-
-	private void testBlobFetchWithTooManyFailuresHa(final JobID jobId) throws IOException {
 		final Configuration config = new Configuration();
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
@@ -222,7 +169,7 @@ public class BlobCacheRetriesTest extends TestLogger {
 		try {
 			blobStoreService = BlobUtils.createBlobStoreFromConfig(config);
 
-			testBlobFetchWithTooManyFailures(config, blobStoreService, jobId);
+			testBlobFetchWithTooManyFailures(config, blobStoreService, new JobID(), true);
 		} finally {
 			if (blobStoreService != null) {
 				blobStoreService.closeAndCleanupAllData();
@@ -239,55 +186,29 @@ public class BlobCacheRetriesTest extends TestLogger {
 	 * 		set compared to this one)
 	 */
 	private static void testBlobFetchWithTooManyFailures(
-		final Configuration config, final BlobStore blobStore, final JobID jobId)
-			throws IOException {
-		final byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
+			final Configuration config, final BlobStore blobStore, @Nullable final JobID jobId,
+			boolean highAvailability) throws IOException {
 
-		BlobServer server = null;
-		BlobCache cache = null;
-		try {
+		final byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
 
-			server = new TestingFailingBlobServer(config, blobStore, 10);
+		try (
+			BlobServer server = new TestingFailingBlobServer(config, blobStore, 0, 10);
+			BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+				config, new VoidBlobStore())) {
 
-			final InetSocketAddress
-				serverAddress = new InetSocketAddress("localhost", server.getPort());
+			server.start();
 
 			// upload some blob
-			BlobClient blobClient = null;
-			BlobKey key;
-			try {
-				blobClient = new BlobClient(serverAddress, config);
-
-				key = blobClient.put(jobId, data);
-			}
-			finally {
-				if (blobClient != null) {
-					blobClient.close();
-				}
-			}
-
-			cache = new BlobCache(serverAddress, config, new VoidBlobStore());
+			final BlobKey key = BlobServerPutTest.put(server, jobId, data, highAvailability);
 
 			// trigger a download - it should fail eventually
 			try {
-				if (jobId == null) {
-					cache.getFile(key);
-				} else {
-					cache.getFile(jobId, key);
-				}
+				verifyContents(cache, jobId, key, data, highAvailability);
 				fail("This should fail");
 			}
 			catch (IOException e) {
 				// as we expected
 			}
 		}
-		finally {
-			if (cache != null) {
-				cache.close();
-			}
-			if (server != null) {
-				server.close();
-			}
-		}
 	}
 }

http://git-wip-us.apache.org/repos/asf/flink/blob/071e27f7/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheSuccessTest.java
----------------------------------------------------------------------
diff --git a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheSuccessTest.java b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheSuccessTest.java
index 1216be2..7a69804 100644
--- a/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheSuccessTest.java
+++ b/flink-runtime/src/test/java/org/apache/flink/runtime/blob/BlobCacheSuccessTest.java
@@ -22,22 +22,20 @@ import org.apache.flink.api.common.JobID;
 import org.apache.flink.configuration.BlobServerOptions;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.configuration.HighAvailabilityOptions;
-import org.apache.flink.util.Preconditions;
 import org.apache.flink.util.TestLogger;
 
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
-import java.io.File;
+import javax.annotation.Nullable;
+
 import java.io.IOException;
 import java.net.InetSocketAddress;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Arrays;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.put;
+import static org.apache.flink.runtime.blob.BlobServerPutTest.verifyContents;
 
 /**
  * This class contains unit tests for the {@link BlobCache}.
@@ -57,7 +55,7 @@ public class BlobCacheSuccessTest extends TestLogger {
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 
-		uploadFileGetTest(config, null, false, false);
+		uploadFileGetTest(config, null, false, false, false);
 	}
 
 	/**
@@ -70,76 +68,40 @@ public class BlobCacheSuccessTest extends TestLogger {
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 
-		uploadFileGetTest(config, new JobID(), false, false);
-	}
-
-	/**
-	 * BlobCache is configured in HA mode and the cache can download files from
-	 * the file system directly and does not need to download BLOBs from the
-	 * BlobServer. Using job-unrelated BLOBs.
-	 */
-	@Test
-	public void testBlobNoJobCacheHa() throws IOException {
-		testBlobCacheHa(null);
+		uploadFileGetTest(config, new JobID(), false, false, false);
 	}
 
 	/**
 	 * BlobCache is configured in HA mode and the cache can download files from
 	 * the file system directly and does not need to download BLOBs from the
-	 * BlobServer. Using job-related BLOBs.
+	 * BlobServer which remains active after the BLOB upload. Using job-related BLOBs.
 	 */
 	@Test
 	public void testBlobForJobCacheHa() throws IOException {
-		testBlobCacheHa(new JobID());
-	}
-
-	private void testBlobCacheHa(final JobID jobId) throws IOException {
 		Configuration config = new Configuration();
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 		config.setString(HighAvailabilityOptions.HA_MODE, "ZOOKEEPER");
 		config.setString(HighAvailabilityOptions.HA_STORAGE_PATH,
 			temporaryFolder.newFolder().getPath());
-		uploadFileGetTest(config, jobId, true, true);
-	}
 
-	/**
-	 * BlobCache is configured in HA mode and the cache can download files from
-	 * the file system directly and does not need to download BLOBs from the
-	 * BlobServer. Using job-unrelated BLOBs.
-	 */
-	@Test
-	public void testBlobNoJobCacheHa2() throws IOException {
-		testBlobCacheHa2(null);
+		uploadFileGetTest(config, new JobID(), true, true, true);
 	}
 
 	/**
 	 * BlobCache is configured in HA mode and the cache can download files from
 	 * the file system directly and does not need to download BLOBs from the
-	 * BlobServer. Using job-related BLOBs.
+	 * BlobServer which is shut down after the BLOB upload. Using job-related BLOBs.
 	 */
 	@Test
 	public void testBlobForJobCacheHa2() throws IOException {
-		testBlobCacheHa2(new JobID());
-	}
-
-	private void testBlobCacheHa2(JobID jobId) throws IOException {
 		Configuration config = new Configuration();
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 		config.setString(HighAvailabilityOptions.HA_MODE, "ZOOKEEPER");
 		config.setString(HighAvailabilityOptions.HA_STORAGE_PATH,
 			temporaryFolder.newFolder().getPath());
-		uploadFileGetTest(config, jobId, false, true);
-	}
-
-	/**
-	 * BlobCache is configured in HA mode but the cache itself cannot access the
-	 * file system and thus needs to download BLOBs from the BlobServer. Using job-unrelated BLOBs.
-	 */
-	@Test
-	public void testBlobNoJobCacheHaFallback() throws IOException {
-		testBlobCacheHaFallback(null);
+		uploadFileGetTest(config, new JobID(), false, true, true);
 	}
 
 	/**
@@ -148,17 +110,14 @@ public class BlobCacheSuccessTest extends TestLogger {
 	 */
 	@Test
 	public void testBlobForJobCacheHaFallback() throws IOException {
-		testBlobCacheHaFallback(new JobID());
-	}
-
-	private void testBlobCacheHaFallback(final JobID jobId) throws IOException {
 		Configuration config = new Configuration();
 		config.setString(BlobServerOptions.STORAGE_DIRECTORY,
 			temporaryFolder.newFolder().getAbsolutePath());
 		config.setString(HighAvailabilityOptions.HA_MODE, "ZOOKEEPER");
 		config.setString(HighAvailabilityOptions.HA_STORAGE_PATH,
 			temporaryFolder.newFolder().getPath());
-		uploadFileGetTest(config, jobId, false, false);
+
+		uploadFileGetTest(config, new JobID(), false, false, true);
 	}
 
 	/**
@@ -174,101 +133,61 @@ public class BlobCacheSuccessTest extends TestLogger {
 	 * @param cacheHasAccessToFs
 	 * 		whether the cache should have access to a shared <tt>HA_STORAGE_PATH</tt> (only useful with
 	 * 		HA mode)
+	 * @param highAvailability
+	 * 		whether to use HA BLOB upload and download methods
 	 */
-	private void uploadFileGetTest(final Configuration config, JobID jobId, boolean shutdownServerAfterUpload,
-			boolean cacheHasAccessToFs) throws IOException {
-		Preconditions.checkArgument(!shutdownServerAfterUpload || cacheHasAccessToFs);
+	private void uploadFileGetTest(
+			final Configuration config, @Nullable JobID jobId, boolean shutdownServerAfterUpload,
+			boolean cacheHasAccessToFs, boolean highAvailability) throws IOException {
 
-		// First create two BLOBs and upload them to BLOB server
-		final byte[] buf = new byte[128];
-		final List<BlobKey> blobKeys = new ArrayList<BlobKey>(2);
-
-		BlobServer blobServer = null;
-		BlobCache blobCache = null;
-		BlobStoreService blobStoreService = null;
-		try {
-			final Configuration cacheConfig = new Configuration(config);
+		final Configuration cacheConfig = new Configuration(config);
+		cacheConfig.setString(BlobServerOptions.STORAGE_DIRECTORY,
+			temporaryFolder.newFolder().getAbsolutePath());
+		if (!cacheHasAccessToFs) {
+			// make sure the cache cannot access the HA store directly
 			cacheConfig.setString(BlobServerOptions.STORAGE_DIRECTORY,
 				temporaryFolder.newFolder().getAbsolutePath());
-			if (!cacheHasAccessToFs) {
-				// make sure the cache cannot access the HA store directly
-				cacheConfig.setString(BlobServerOptions.STORAGE_DIRECTORY,
-					temporaryFolder.newFolder().getAbsolutePath());
-				cacheConfig.setString(HighAvailabilityOptions.HA_STORAGE_PATH,
-					temporaryFolder.newFolder().getPath() + "/does-not-exist");
-			}
-
-			blobStoreService = BlobUtils.createBlobStoreFromConfig(cacheConfig);
-
-			// Start the BLOB server
-			blobServer = new BlobServer(config, blobStoreService);
-			final InetSocketAddress serverAddress = new InetSocketAddress(blobServer.getPort());
+			cacheConfig.setString(HighAvailabilityOptions.HA_STORAGE_PATH,
+				temporaryFolder.newFolder().getPath() + "/does-not-exist");
+		}
 
-			// Upload BLOBs
-			BlobClient blobClient = null;
-			try {
+		// First create two BLOBs and upload them to BLOB server
+		final byte[] data = new byte[128];
+		byte[] data2 = Arrays.copyOf(data, data.length);
+		data2[0] ^= 1;
 
-				blobClient = new BlobClient(serverAddress, config);
+		BlobStoreService blobStoreService = null;
 
-				blobKeys.add(blobClient.put(jobId, buf));
-				buf[0] = 1; // Make sure the BLOB key changes
-				blobKeys.add(blobClient.put(jobId, buf));
-			} finally {
-				if (blobClient != null) {
-					blobClient.close();
-				}
-			}
+		try {
+			blobStoreService = BlobUtils.createBlobStoreFromConfig(cacheConfig);
+			try (
+				BlobServer server = new BlobServer(config, blobStoreService);
+				BlobCache cache = new BlobCache(new InetSocketAddress("localhost", server.getPort()),
+					cacheConfig, blobStoreService)) {
 
-			if (shutdownServerAfterUpload) {
-				// Now, shut down the BLOB server, the BLOBs must still be accessible through the cache.
-				blobServer.close();
-				blobServer = null;
-			}
+				server.start();
 
-			blobCache = new BlobCache(serverAddress, cacheConfig, blobStoreService);
+				// Upload BLOBs
+				BlobKey key1 = put(server, jobId, data, highAvailability);
+				BlobKey key2 = put(server, jobId, data2, highAvailability);
 
-			for (BlobKey blobKey : blobKeys) {
-				if (jobId == null) {
-					blobCache.getFile(blobKey);
-				} else {
-					blobCache.getFile(jobId, blobKey);
+				if (shutdownServerAfterUpload) {
+					// Now, shut down the BLOB server, the BLOBs must still be accessible through the cache.
+					server.close();
 				}
-			}
 
-			if (blobServer != null) {
-				// Now, shut down the BLOB server, the BLOBs must still be accessible through the cache.
-				blobServer.close();
-				blobServer = null;
-			}
+				verifyContents(cache, jobId, key1, data, highAvailability);
+				verifyContents(cache, jobId, key2, data2, highAvailability);
 
-			final File[] files = new File[blobKeys.size()];
+				if (shutdownServerAfterUpload) {
+					// Now, shut down the BLOB server, the BLOBs must still be accessible through the cache.
+					server.close();
 
-			for(int i = 0; i < blobKeys.size(); i++){
-				if (jobId == null) {
-					files[i] = blobCache.getFile(blobKeys.get(i));
-				} else {
-					files[i] = blobCache.getFile(jobId, blobKeys.get(i));
+					verifyContents(cache, jobId, key1, data, highAvailability);
+					verifyContents(cache, jobId, key2, data2, highAvailability);
 				}
 			}
-
-			// Verify the result
-			assertEquals(blobKeys.size(), files.length);
-
-			for (final File file : files) {
-				assertNotNull(file);
-
-				assertTrue(file.exists());
-				assertEquals(buf.length, file.length());
-			}
 		} finally {
-			if (blobServer != null) {
-				blobServer.close();
-			}
-
-			if(blobCache != null){
-				blobCache.close();
-			}
-
 			if (blobStoreService != null) {
 				blobStoreService.closeAndCleanupAllData();
 			}