mirror of https://github.com/apache/jclouds.git
work around DynECT api and session limitations
This commit is contained in:
parent
e2f18eb64a
commit
0d6fbce68a
|
@ -61,6 +61,11 @@
|
|||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.mockwebserver</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jclouds.driver</groupId>
|
||||
<artifactId>jclouds-slf4j</artifactId>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.jclouds.dynect.v3;
|
||||
|
||||
import static org.jclouds.Constants.PROPERTY_MAX_REDIRECTS;
|
||||
import static org.jclouds.Constants.PROPERTY_RETRY_DELAY_START;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Properties;
|
||||
|
@ -51,8 +52,9 @@ public class DynECTProviderMetadata extends BaseProviderMetadata {
|
|||
|
||||
public static Properties defaultProperties() {
|
||||
Properties properties = new Properties();
|
||||
// job polling occurs via redirect loop
|
||||
properties.setProperty(PROPERTY_MAX_REDIRECTS, "20");
|
||||
// job polling occurs via redirect loop and can take a while
|
||||
properties.setProperty(PROPERTY_MAX_REDIRECTS, "100");
|
||||
properties.setProperty(PROPERTY_RETRY_DELAY_START, "200");
|
||||
return properties;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,10 +18,22 @@
|
|||
*/
|
||||
package org.jclouds.dynect.v3.config;
|
||||
|
||||
import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
|
||||
import static org.jclouds.rest.config.BinderUtils.bindClientAndAsyncClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.jclouds.Constants;
|
||||
import org.jclouds.concurrent.SingleThreaded;
|
||||
import org.jclouds.dynect.v3.DynECTApi;
|
||||
import org.jclouds.dynect.v3.DynECTAsyncApi;
|
||||
import org.jclouds.dynect.v3.features.SessionApi;
|
||||
|
@ -29,13 +41,26 @@ import org.jclouds.dynect.v3.features.SessionAsyncApi;
|
|||
import org.jclouds.dynect.v3.features.ZoneApi;
|
||||
import org.jclouds.dynect.v3.features.ZoneAsyncApi;
|
||||
import org.jclouds.dynect.v3.filters.SessionManager;
|
||||
import org.jclouds.dynect.v3.handlers.DynECTErrorHandler;
|
||||
import org.jclouds.dynect.v3.handlers.GetJobRedirectionRetryHandler;
|
||||
import org.jclouds.http.HttpErrorHandler;
|
||||
import org.jclouds.http.HttpResponse;
|
||||
import org.jclouds.http.HttpRetryHandler;
|
||||
import org.jclouds.http.HttpUtils;
|
||||
import org.jclouds.http.IOExceptionRetryHandler;
|
||||
import org.jclouds.http.annotation.ClientError;
|
||||
import org.jclouds.http.annotation.Redirection;
|
||||
import org.jclouds.http.annotation.ServerError;
|
||||
import org.jclouds.http.handlers.DelegatingErrorHandler;
|
||||
import org.jclouds.http.handlers.DelegatingRetryHandler;
|
||||
import org.jclouds.http.handlers.RedirectionRetryHandler;
|
||||
import org.jclouds.http.internal.HttpWire;
|
||||
import org.jclouds.http.internal.JavaUrlHttpCommandExecutorService;
|
||||
import org.jclouds.io.ContentMetadataCodec;
|
||||
import org.jclouds.rest.ConfiguresRestClient;
|
||||
import org.jclouds.rest.config.RestClientModule;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
/**
|
||||
|
@ -44,6 +69,8 @@ import com.google.common.collect.ImmutableMap;
|
|||
* @author Adrian Cole
|
||||
*/
|
||||
@ConfiguresRestClient
|
||||
// only one job at a time or error "This session already has a job running"
|
||||
@SingleThreaded
|
||||
public class DynECTRestClientModule extends RestClientModule<DynECTApi, DynECTAsyncApi> {
|
||||
|
||||
public static final Map<Class<?>, Class<?>> DELEGATE_MAP = ImmutableMap.<Class<?>, Class<?>> builder()
|
||||
|
@ -54,6 +81,13 @@ public class DynECTRestClientModule extends RestClientModule<DynECTApi, DynECTAs
|
|||
super(DELEGATE_MAP);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bindErrorHandlers() {
|
||||
bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(DynECTErrorHandler.class);
|
||||
bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(DynECTErrorHandler.class);
|
||||
bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(DynECTErrorHandler.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bindRetryHandlers() {
|
||||
bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(SessionManager.class);
|
||||
|
@ -67,6 +101,39 @@ public class DynECTRestClientModule extends RestClientModule<DynECTApi, DynECTAs
|
|||
super.configure();
|
||||
// Bind apis that are used directly vs via DynECTApi
|
||||
bindClientAndAsyncClient(binder(), SessionApi.class, SessionAsyncApi.class);
|
||||
|
||||
// dynect returns the following as a 200.
|
||||
// {"status": "failure", "data": {}, "job_id": 274509427, "msgs":
|
||||
// [{"INFO": "token: This session already has a job running", "SOURCE":
|
||||
// "API-B", "ERR_CD": "OPERATION_FAILED", "LVL": "ERROR"}]}
|
||||
bind(JavaUrlHttpCommandExecutorService.class).to(SillyRabbit200sAreForSuccess.class);
|
||||
}
|
||||
|
||||
@Singleton
|
||||
private static class SillyRabbit200sAreForSuccess extends JavaUrlHttpCommandExecutorService {
|
||||
|
||||
@Inject
|
||||
private SillyRabbit200sAreForSuccess(HttpUtils utils, ContentMetadataCodec contentMetadataCodec,
|
||||
@Named(Constants.PROPERTY_IO_WORKER_THREADS) ExecutorService ioExecutor,
|
||||
DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler,
|
||||
DelegatingErrorHandler errorHandler, HttpWire wire, @Named("untrusted") HostnameVerifier verifier,
|
||||
@Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider)
|
||||
throws SecurityException, NoSuchFieldException {
|
||||
super(utils, contentMetadataCodec, ioExecutor, retryHandler, ioRetryHandler, errorHandler, wire, verifier,
|
||||
untrustedSSLContextProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HttpResponse invoke(HttpURLConnection connection) throws IOException, InterruptedException {
|
||||
HttpResponse response = super.invoke(connection);
|
||||
if (response.getStatusCode() == 200) {
|
||||
byte[] data = closeClientButKeepContentStream(response);
|
||||
String message = data != null ? new String(data, "UTF-8") : null;
|
||||
if (message != null && !message.startsWith("{\"status\": \"success\"")) {
|
||||
response = response.toBuilder().statusCode(400).build();
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds 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.jclouds.dynect.v3.handlers;
|
||||
|
||||
import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream;
|
||||
import static org.jclouds.http.HttpUtils.releasePayload;
|
||||
|
||||
import org.jclouds.http.HttpCommand;
|
||||
import org.jclouds.http.HttpErrorHandler;
|
||||
import org.jclouds.http.HttpResponse;
|
||||
import org.jclouds.http.HttpResponseException;
|
||||
|
||||
/**
|
||||
* @author Adrian Cole
|
||||
*/
|
||||
public class DynECTErrorHandler implements HttpErrorHandler {
|
||||
private static final String JOB_STILL_RUNNING = "This session already has a job running";
|
||||
|
||||
public void handleError(HttpCommand command, HttpResponse response) {
|
||||
Exception exception = new HttpResponseException(command, response);
|
||||
try {
|
||||
byte[] data = closeClientButKeepContentStream(response);
|
||||
String message = data != null ? new String(data) : null;
|
||||
if (message != null) {
|
||||
exception = new HttpResponseException(command, response, message);
|
||||
if (message.indexOf(JOB_STILL_RUNNING) != -1)
|
||||
exception = new IllegalStateException(JOB_STILL_RUNNING, exception);
|
||||
} else {
|
||||
exception = new HttpResponseException(command, response);
|
||||
}
|
||||
} finally {
|
||||
releasePayload(response);
|
||||
command.setException(exception);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds 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.jclouds.dynect.v3;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.jclouds.ContextBuilder;
|
||||
import org.jclouds.rest.RestContext;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import com.google.mockwebserver.MockResponse;
|
||||
import com.google.mockwebserver.MockWebServer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Adrian Cole
|
||||
*/
|
||||
@Test
|
||||
public class DynectApiMockTest {
|
||||
|
||||
static RestContext<DynECTApi, DynECTAsyncApi> getContext(String uri) {
|
||||
return ContextBuilder.newBuilder("dynect").credentials("jclouds:joe", "letmein").endpoint(uri).build();
|
||||
}
|
||||
|
||||
String session = "{\"status\": \"success\", \"data\": {\"token\": \"FFFFFFFFFF\", \"version\": \"3.3.7\"}, \"job_id\": 254417252, \"msgs\": [{\"INFO\": \"login: Login successful\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}";
|
||||
String failure = "{\"status\": \"failure\", \"data\": {}, \"job_id\": 274509427, \"msgs\": [{\"INFO\": \"token: This session already has a job running\", \"SOURCE\": \"API-B\", \"ERR_CD\": \"OPERATION_FAILED\", \"LVL\": \"ERROR\"}]}";
|
||||
|
||||
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "This session already has a job running")
|
||||
public void test200OnFailureThrowsExceptionWithoutRetry() throws IOException, InterruptedException {
|
||||
MockWebServer server = new MockWebServer();
|
||||
server.enqueue(new MockResponse().setResponseCode(200).setBody(session));
|
||||
server.enqueue(new MockResponse().setResponseCode(200).setBody(failure));
|
||||
server.play();
|
||||
|
||||
DynECTApi api = getContext(server.getUrl("/").toString()).getApi();
|
||||
|
||||
try {
|
||||
api.getZoneApi().list();
|
||||
} finally {
|
||||
server.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,12 +20,17 @@ package org.jclouds.dynect.v3.internal;
|
|||
|
||||
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
|
||||
|
||||
import org.jclouds.dynect.v3.config.DynECTRestClientModule;
|
||||
import org.jclouds.http.HttpRequest;
|
||||
import org.jclouds.http.HttpResponse;
|
||||
import org.jclouds.http.config.SSLModule;
|
||||
import org.jclouds.io.Payload;
|
||||
import org.jclouds.io.Payloads;
|
||||
import org.jclouds.rest.ConfiguresRestClient;
|
||||
import org.jclouds.rest.internal.BaseRestApiExpectTest;
|
||||
|
||||
import com.google.inject.Module;
|
||||
|
||||
/**
|
||||
* Base class for writing DynECT Expect tests
|
||||
*
|
||||
|
@ -38,6 +43,20 @@ public class BaseDynECTExpectTest<T> extends BaseRestApiExpectTest<T> {
|
|||
credential = "letmein";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Module createModule() {
|
||||
return new TestDynECTRestClientModule();
|
||||
}
|
||||
|
||||
@ConfiguresRestClient
|
||||
private static final class TestDynECTRestClientModule extends DynECTRestClientModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
install(new SSLModule());
|
||||
super.configure();
|
||||
}
|
||||
}
|
||||
|
||||
public static Payload emptyJsonPayload() {
|
||||
Payload p = Payloads.newByteArrayPayload(new byte[] {});
|
||||
p.getContentMetadata().setContentType(APPLICATION_JSON);
|
||||
|
@ -57,10 +76,14 @@ public class BaseDynECTExpectTest<T> extends BaseRestApiExpectTest<T> {
|
|||
|
||||
protected String authToken = "FFFFFFFFFF";
|
||||
|
||||
protected HttpRequest createSession = HttpRequest.builder().method("POST")
|
||||
protected HttpRequest createSession = HttpRequest
|
||||
.builder()
|
||||
.method("POST")
|
||||
.endpoint("https://api2.dynect.net/REST/Session")
|
||||
.addHeader("API-Version", "3.3.7")
|
||||
.payload(payloadFromStringWithContentType("{\"customer_name\":\"jclouds\",\"user_name\":\"joe\",\"password\":\"letmein\"}",APPLICATION_JSON))
|
||||
.payload(
|
||||
payloadFromStringWithContentType(
|
||||
"{\"customer_name\":\"jclouds\",\"user_name\":\"joe\",\"password\":\"letmein\"}", APPLICATION_JSON))
|
||||
.build();
|
||||
|
||||
protected HttpResponse createSessionResponse = HttpResponse.builder().statusCode(200)
|
||||
|
|
Loading…
Reference in New Issue