work around DynECT api and session limitations

This commit is contained in:
adriancole 2013-02-19 01:28:40 -08:00
parent e2f18eb64a
commit 0d6fbce68a
6 changed files with 214 additions and 6 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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)