/* * Copyright 2016 Grzegorz Grzybek * * Licensed 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.ops4j.pax.url.mvn; import java.io.File; import java.io.IOException; import java.util.Properties; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.maven.settings.Profile; import org.apache.maven.settings.Repository; import org.apache.maven.settings.Settings; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.ops4j.pax.url.mvn.internal.AetherBasedResolver; import org.ops4j.pax.url.mvn.internal.config.MavenConfigurationImpl; import org.ops4j.util.property.PropertiesPropertyResolver; import static org.junit.Assert.*; /** * Test cases for connection and read timeouts */ public class AetherTimeoutTest { private static Server server; private static int port; private static ExecutorService pool = Executors.newFixedThreadPool(1); @BeforeClass public static void startJetty() throws Exception { server = new Server(0); server.setHandler(new AbstractHandler() { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { try { if (request.getRequestURI().endsWith(".jar")) { // get simulated timeout value from ... version fragment String[] split = request.getRequestURI().split("[-.]"); String versionWhichIsTimeout = split[split.length - 2]; try { Thread.sleep(Integer.parseInt(versionWhichIsTimeout)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } response.setStatus(HttpServletResponse.SC_OK); response.getOutputStream().write(0x42); } else { response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } finally { baseRequest.setHandled(true); } } }); server.start(); port = server.getConnectors()[0].getLocalPort(); } @Test public void connectionTimeout() throws Exception { final MavenConfigurationImpl mavenConfiguration = basicMavenConfigurationWithTwoTimeouts(5000, 500); mavenConfiguration.setSettings(settingsWithUnresponsiveRepository()); Future<Boolean> ok = pool.submit(new ResolveArtifactTask(mavenConfiguration, 0)); try { boolean resolved = ok.get(2000, TimeUnit.MILLISECONDS); assertFalse(resolved); } catch (TimeoutException e) { fail("Should fail due to connection timeout earlier."); } } @Test public void readTimeout() throws Exception { // case 1: resolution fails because of timeout // - aether uses 500ms read timeout // - server responds with 1000ms delay // - we interrupt resolution after 2000ms in case read timeout in socket.setSoTimeout() is set to 0 MavenConfigurationImpl mavenConfiguration = basicMavenConfiguration(500); mavenConfiguration.setSettings(settingsWithJettyRepository()); Future<Boolean> task = pool.submit(new ResolveArtifactTask(mavenConfiguration, 1000)); try { boolean resolved = task.get(2000, TimeUnit.MILLISECONDS); assertFalse("Should not be resolved due to read timeout", resolved); } catch (TimeoutException e) { task.cancel(true); fail("Should fail due to socket read timeout earlier, not due to future.get() timeout."); } // case 2: resolution doesn't fail, we're cancelling the task earlier // - aether uses 6s read timeout // - server responds with 3s delay // - we interrupt resolution after 2s ensuring that Aether resolution didn't end on timeout yet mavenConfiguration = basicMavenConfiguration(6000); mavenConfiguration.setSettings(settingsWithJettyRepository()); task = pool.submit(new ResolveArtifactTask(mavenConfiguration, 3000)); boolean timedOut; try { task.get(2000, TimeUnit.MILLISECONDS); timedOut = false; fail("Task should not be completed yet"); } catch (TimeoutException e) { timedOut = true; task.cancel(true); } assertTrue("Resolution task should be interrupted", timedOut); } @AfterClass public static void stopJetty() throws Exception { server.stop(); pool.shutdown(); } private MavenConfigurationImpl basicMavenConfiguration(int timeoutInMs) { Properties properties = new Properties(); properties.setProperty("pid.localRepository", "target/" + UUID.randomUUID().toString()); properties.setProperty("pid.timeout", Integer.toString(timeoutInMs)); properties.setProperty("pid.globalChecksumPolicy", "ignore"); return new MavenConfigurationImpl(new PropertiesPropertyResolver(properties), "pid"); } private MavenConfigurationImpl basicMavenConfigurationWithTwoTimeouts(int readTimeoutInMs, int connectTimeoutInMs) { Properties properties = new Properties(); properties.setProperty("pid.localRepository", "target/" + UUID.randomUUID().toString()); properties.setProperty("pid.timeout", Integer.toString(readTimeoutInMs)); properties.setProperty("pid.socket.connectionTimeout", Integer.toString(connectTimeoutInMs)); properties.setProperty("pid.globalChecksumPolicy", "ignore"); return new MavenConfigurationImpl(new PropertiesPropertyResolver(properties), "pid"); } private Settings settingsWithUnresponsiveRepository() { Settings settings = new Settings(); Profile defaultProfile = new Profile(); defaultProfile.setId("default"); Repository repo1 = new Repository(); repo1.setId("repo1"); // see: // - https://tools.ietf.org/html/rfc5737 // - https://en.wikipedia.org/wiki/Reserved_IP_addresses repo1.setUrl("http://192.0.2.0/repository"); defaultProfile.addRepository(repo1); settings.addProfile(defaultProfile); settings.addActiveProfile("default"); return settings; } private Settings settingsWithJettyRepository() { Settings settings = new Settings(); Profile defaultProfile = new Profile(); defaultProfile.setId("default"); Repository repo1 = new Repository(); repo1.setId("repo1"); repo1.setUrl("http://localhost:" + port + "/repository"); defaultProfile.addRepository(repo1); settings.addProfile(defaultProfile); settings.addActiveProfile("default"); return settings; } /** * Task that performs Aether resolution with simulated server-side timeout */ private static class ResolveArtifactTask implements Callable<Boolean> { private final MavenConfigurationImpl mavenConfiguration; private final int expectedTimeout; /** * @param mavenConfiguration * @param expectedTimeout this parameter will be used at server side to simulate read timeout */ public ResolveArtifactTask(MavenConfigurationImpl mavenConfiguration, int expectedTimeout) { this.mavenConfiguration = mavenConfiguration; this.expectedTimeout = expectedTimeout; } @Override public Boolean call() throws Exception { AetherBasedResolver resolver = new AetherBasedResolver(mavenConfiguration); try { // version value will be used to simulate read timeout at server side File resolved = resolver.resolve("org.ops4j.pax.web", "pax-web-api", "", "jar", Integer.toString(expectedTimeout)); return resolved.isFile(); } catch (IOException e) { return false; } finally { resolver.close(); } } } }