diff --git a/LICENSE-commons-codec.txt b/3rd-party-licenses/LICENSE-commons-codec.txt similarity index 100% rename from LICENSE-commons-codec.txt rename to 3rd-party-licenses/LICENSE-commons-codec.txt diff --git a/LICENSE-httpclient.txt b/3rd-party-licenses/LICENSE-httpclient.txt similarity index 100% rename from LICENSE-httpclient.txt rename to 3rd-party-licenses/LICENSE-httpclient.txt diff --git a/LICENSE-httpcore.txt b/3rd-party-licenses/LICENSE-httpcore.txt similarity index 100% rename from LICENSE-httpcore.txt rename to 3rd-party-licenses/LICENSE-httpcore.txt diff --git a/LICENSE-jdom2.txt b/3rd-party-licenses/LICENSE-jdom2.txt similarity index 100% rename from LICENSE-jdom2.txt rename to 3rd-party-licenses/LICENSE-jdom2.txt diff --git a/LICENSE-jersey-apache-client4.txt b/3rd-party-licenses/LICENSE-jersey-apache-client4.txt similarity index 100% rename from LICENSE-jersey-apache-client4.txt rename to 3rd-party-licenses/LICENSE-jersey-apache-client4.txt diff --git a/LICENSE-jersey-client.txt b/3rd-party-licenses/LICENSE-jersey-client.txt similarity index 100% rename from LICENSE-jersey-client.txt rename to 3rd-party-licenses/LICENSE-jersey-client.txt diff --git a/LICENSE-jersey-core.txt b/3rd-party-licenses/LICENSE-jersey-core.txt similarity index 100% rename from LICENSE-jersey-core.txt rename to 3rd-party-licenses/LICENSE-jersey-core.txt diff --git a/LICENSE-log4j.txt b/3rd-party-licenses/LICENSE-log4j.txt similarity index 100% rename from LICENSE-log4j.txt rename to 3rd-party-licenses/LICENSE-log4j.txt diff --git a/LICENSE-object-transform.txt b/3rd-party-licenses/LICENSE-object-transform.txt similarity index 100% rename from LICENSE-object-transform.txt rename to 3rd-party-licenses/LICENSE-object-transform.txt diff --git a/LICENSE-smart-client.txt b/3rd-party-licenses/LICENSE-smart-client.txt similarity index 100% rename from LICENSE-smart-client.txt rename to 3rd-party-licenses/LICENSE-smart-client.txt diff --git a/build.gradle b/build.gradle index 70531be4..83196c19 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ buildscript { apply from: "$commonBuildDir/ecs-publish.gradle" dependencies { - compile 'com.emc.ecs:smart-client:2.0.2', + compile 'com.emc.ecs:smart-client:2.0.3', 'com.emc.ecs:object-transform:1.0.2', 'org.jdom:jdom2:2.0.6' testCompile 'junit:junit:4.12' diff --git a/src/main/java/com/emc/object/ObjectConfig.java b/src/main/java/com/emc/object/ObjectConfig.java index ffcb3e4d..d17544a5 100644 --- a/src/main/java/com/emc/object/ObjectConfig.java +++ b/src/main/java/com/emc/object/ObjectConfig.java @@ -64,7 +64,6 @@ public abstract class ObjectConfig> { private String secretKey; private long serverClockSkew; private String userAgent = DEFAULT_USER_AGENT; - private EncryptionConfig encryptionConfig; private boolean geoPinningEnabled = false; private Map properties = new HashMap(); @@ -111,7 +110,6 @@ public ObjectConfig(ObjectConfig other) { this.secretKey = other.secretKey; this.serverClockSkew = other.serverClockSkew; this.userAgent = other.userAgent; - if (other.encryptionConfig != null) this.encryptionConfig = new EncryptionConfig(other.encryptionConfig); this.geoPinningEnabled = other.geoPinningEnabled; } @@ -266,12 +264,18 @@ public void setUserAgent(String userAgent) { this.userAgent = userAgent; } + /** + * @deprecated (2.0.3) always returns null (see {@link #setEncryptionConfig(EncryptionConfig)}) + */ public EncryptionConfig getEncryptionConfig() { - return encryptionConfig; + return null; } + /** + * @deprecated (2.0.3) this method does nothing. EncryptionConfig instance should be passed to the constructor of + * an encryption client + */ public void setEncryptionConfig(EncryptionConfig encryptionConfig) { - this.encryptionConfig = encryptionConfig; } public boolean isGeoPinningEnabled() { @@ -344,6 +348,10 @@ public T withUserAgent(String userAgent) { return (T) this; } + /** + * @deprecated (2.0.3) this method does nothing. EncryptionConfig instance should be passed to the constructor of + * an encryption client + */ @SuppressWarnings("unchecked") public T withEncryptionConfig(EncryptionConfig encryptionConfig) { setEncryptionConfig(encryptionConfig); @@ -375,7 +383,6 @@ public String toString() { ", secretKey='" + secretKey + '\'' + ", serverClockSkew=" + serverClockSkew + ", userAgent='" + userAgent + '\'' + - ", encryptionConfig=" + encryptionConfig + ", geoPinningEnabled=" + geoPinningEnabled + ", properties=" + properties + '}'; diff --git a/src/main/java/com/emc/object/s3/S3Client.java b/src/main/java/com/emc/object/s3/S3Client.java index b91499a9..9e0ca68e 100644 --- a/src/main/java/com/emc/object/s3/S3Client.java +++ b/src/main/java/com/emc/object/s3/S3Client.java @@ -45,8 +45,13 @@ */ public interface S3Client { /** - * Always call .shutdown() when finished with a client to ensure that any attached resources and background processes - * are released/terminated (i.e. polling threads for host list providers) + * Always call .destroy() when finished with a client to ensure that any attached resources and background processes + * are released/terminated (i.e. polling threads, host list providers and connection pools) + */ + void destroy(); + + /** + * @deprecated (2.0.3) use destroy() instead */ void shutdown(); @@ -125,7 +130,8 @@ public interface S3Client { void setBucketCors(String bucketName, CorsConfiguration corsConfiguration); /** - * Retrieves the CORS configuration for bucketName + * Retrieves the CORS configuration for bucketName. If no CORS configuration exists for the specified + * bucket, null is returned * * @see CorsConfiguration */ @@ -144,7 +150,8 @@ public interface S3Client { void setBucketLifecycle(String bucketName, LifecycleConfiguration lifecycleConfiguration); /** - * Retrieves the lifecycle configuration for bucketName + * Retrieves the lifecycle configuration for bucketName. If no lifecycle exists for the specified + * bucket, null is returned * * @see LifecycleConfiguration */ diff --git a/src/main/java/com/emc/object/s3/S3Config.java b/src/main/java/com/emc/object/s3/S3Config.java index 669c5882..9b481409 100644 --- a/src/main/java/com/emc/object/s3/S3Config.java +++ b/src/main/java/com/emc/object/s3/S3Config.java @@ -61,6 +61,9 @@ public class S3Config extends ObjectConfig { public static final int DEFAULT_HTTP_PORT = 9020; public static final int DEFAULT_HTTPS_PORT = 9021; + public static final int DEFAULT_INITIAL_RETRY_DELAY = 1000; // ms + public static final int DEFAULT_RETRY_LIMIT = 3; + public static final int DEFAULT_RETRY_BUFFER_SIZE = 2 * 1024 * 1024; protected static int defaultPort(Protocol protocol) { if (protocol == Protocol.HTTP) return DEFAULT_HTTP_PORT; @@ -72,6 +75,10 @@ protected static int defaultPort(Protocol protocol) { protected boolean useVHost = false; protected boolean signNamespace = true; protected boolean checksumEnabled = true; + protected boolean retryEnabled = true; + protected int initialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY; + protected int retryLimit = DEFAULT_RETRY_LIMIT; + protected int retryBufferSize = DEFAULT_RETRY_BUFFER_SIZE; /** * External load balancer constructor (no smart-client). @@ -105,6 +112,7 @@ public S3Config(S3Config other) { this.useVHost = other.useVHost; this.signNamespace = other.signNamespace; this.checksumEnabled = other.checksumEnabled; + this.retryEnabled = other.retryEnabled; } @Override @@ -153,6 +161,41 @@ public void setChecksumEnabled(boolean checksumEnabled) { this.checksumEnabled = checksumEnabled; } + public boolean isRetryEnabled() { + return retryEnabled; + } + + /** + * Set to false to disable automatic retry of (retriable) requests + */ + public void setRetryEnabled(boolean retryEnabled) { + this.retryEnabled = retryEnabled; + } + + public int getInitialRetryDelay() { + return initialRetryDelay; + } + + public void setInitialRetryDelay(int initialRetryDelay) { + this.initialRetryDelay = initialRetryDelay; + } + + public int getRetryLimit() { + return retryLimit; + } + + public void setRetryLimit(int retryLimit) { + this.retryLimit = retryLimit; + } + + public int getRetryBufferSize() { + return retryBufferSize; + } + + public void setRetryBufferSize(int retryBufferSize) { + this.retryBufferSize = retryBufferSize; + } + public S3Config withUseVHost(boolean useVHost) { setUseVHost(useVHost); return this; @@ -168,6 +211,26 @@ public S3Config withChecksumEnabled(boolean checksumEnabled) { return this; } + public S3Config withRetryEnabled(boolean retryEnabled) { + setRetryEnabled(retryEnabled); + return this; + } + + public S3Config withInitialRetryDelay(int initialRetryDelay) { + setInitialRetryDelay(initialRetryDelay); + return this; + } + + public S3Config withRetryLimit(int retryLimit) { + setRetryLimit(retryLimit); + return this; + } + + public S3Config withRetryBufferSize(int retryBufferSize) { + setRetryBufferSize(retryBufferSize); + return this; + } + @Override public String toString() { return "S3Config{" + diff --git a/src/main/java/com/emc/object/s3/bean/CorsRule.java b/src/main/java/com/emc/object/s3/bean/CorsRule.java index 525ef6c5..743784e7 100644 --- a/src/main/java/com/emc/object/s3/bean/CorsRule.java +++ b/src/main/java/com/emc/object/s3/bean/CorsRule.java @@ -140,4 +140,35 @@ public CorsRule withAllowedHeaders(List allowedHeaders) { public CorsRule withAllowedHeaders(String... allowedHeaders) { return withAllowedHeaders(Arrays.asList(allowedHeaders)); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CorsRule)) return false; + + CorsRule corsRule = (CorsRule) o; + + if (id != null ? !id.equals(corsRule.id) : corsRule.id != null) return false; + if (allowedMethods != null ? !allowedMethods.equals(corsRule.allowedMethods) : corsRule.allowedMethods != null) + return false; + if (allowedOrigins != null ? !allowedOrigins.equals(corsRule.allowedOrigins) : corsRule.allowedOrigins != null) + return false; + if (maxAgeSeconds != null ? !maxAgeSeconds.equals(corsRule.maxAgeSeconds) : corsRule.maxAgeSeconds != null) + return false; + if (exposeHeaders != null ? !exposeHeaders.equals(corsRule.exposeHeaders) : corsRule.exposeHeaders != null) + return false; + return !(allowedHeaders != null ? !allowedHeaders.equals(corsRule.allowedHeaders) : corsRule.allowedHeaders != null); + + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (allowedMethods != null ? allowedMethods.hashCode() : 0); + result = 31 * result + (allowedOrigins != null ? allowedOrigins.hashCode() : 0); + result = 31 * result + (maxAgeSeconds != null ? maxAgeSeconds.hashCode() : 0); + result = 31 * result + (exposeHeaders != null ? exposeHeaders.hashCode() : 0); + result = 31 * result + (allowedHeaders != null ? allowedHeaders.hashCode() : 0); + return result; + } } diff --git a/src/main/java/com/emc/object/s3/bean/LifecycleConfiguration.java b/src/main/java/com/emc/object/s3/bean/LifecycleConfiguration.java index 8db0312c..d51fc927 100644 --- a/src/main/java/com/emc/object/s3/bean/LifecycleConfiguration.java +++ b/src/main/java/com/emc/object/s3/bean/LifecycleConfiguration.java @@ -29,6 +29,7 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @XmlRootElement(name = "LifecycleConfiguration") @@ -43,4 +44,14 @@ public List getRules() { public void setRules(List rules) { this.rules = rules; } + + public LifecycleConfiguration withRules(List rules) { + setRules(rules); + return this; + } + + public LifecycleConfiguration withRules(LifecycleRule... rules) { + setRules(Arrays.asList(rules)); + return this; + } } diff --git a/src/main/java/com/emc/object/s3/jersey/CodecFilter.java b/src/main/java/com/emc/object/s3/jersey/CodecFilter.java index eb16bf04..8fc7f30a 100644 --- a/src/main/java/com/emc/object/s3/jersey/CodecFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/CodecFilter.java @@ -32,6 +32,8 @@ import com.emc.rest.smart.SizeOverrideWriter; import com.sun.jersey.api.client.*; import com.sun.jersey.api.client.filter.ClientFilter; +import org.apache.log4j.LogMF; +import org.apache.log4j.Logger; import javax.ws.rs.core.MultivaluedMap; import java.io.IOException; @@ -41,6 +43,8 @@ import java.util.Set; public class CodecFilter extends ClientFilter { + private static final Logger l4j = Logger.getLogger(CodecFilter.class); + private CodecChain encodeChain; private Map codecProperties; @@ -56,8 +60,16 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio Boolean encode = (Boolean) request.getProperties().get(RestUtil.PROPERTY_ENCODE_ENTITY); if (encode != null && encode) { - // we don't know what the size will be; this will turn on chunked encoding in the apache client - SizeOverrideWriter.setEntitySize(-1L); + // if encoded size is predictable and we know the original size, we can set a content-length and avoid chunked encoding + Long originalSize = SizeOverrideWriter.getEntitySize(); + if (encodeChain.isSizePredictable() && originalSize != null) { + long encodedSize = encodeChain.getEncodedSize(originalSize); + LogMF.debug(l4j, "updating content-length for encoded data (original: {0}, encoded: {1})", originalSize, encodedSize); + SizeOverrideWriter.setEntitySize(encodedSize); + } else { + // we don't know what the size will be; this will turn on chunked encoding in the apache client + SizeOverrideWriter.setEntitySize(-1L); + } // wrap output stream with encryptor request.setAdapter(new EncryptAdapter(request.getAdapter(), userMeta)); diff --git a/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java b/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java index 126a58f7..e3269ee3 100644 --- a/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java +++ b/src/main/java/com/emc/object/s3/jersey/GeoPinningFilter.java @@ -34,6 +34,7 @@ import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.ClientFilter; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.log4j.Logger; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -44,6 +45,8 @@ * the path to extract the object key) */ public class GeoPinningFilter extends ClientFilter { + private static final Logger l4j = Logger.getLogger(GeoPinningFilter.class); + private ObjectConfig objectConfig; public GeoPinningFilter(ObjectConfig objectConfig) { @@ -83,9 +86,13 @@ public ClientResponse handle(ClientRequest request) throws ClientHandlerExceptio if (vdc.isHealthy()) healthyVdcs.add(vdc); } - int geoPinIndex = getGeoPinIndex(getGeoId(request, bucketName), healthyVdcs.size()); + if (healthyVdcs.isEmpty()) { + l4j.warn("there are no healthy VDCs!"); + } else { + int geoPinIndex = getGeoPinIndex(getGeoId(request, bucketName), healthyVdcs.size()); - request.getProperties().put(GeoPinningRule.PROP_GEO_PINNED_VDC, healthyVdcs.get(geoPinIndex)); + request.getProperties().put(GeoPinningRule.PROP_GEO_PINNED_VDC, healthyVdcs.get(geoPinIndex)); + } } return getNext().handle(request); diff --git a/src/main/java/com/emc/object/s3/jersey/RetryFilter.java b/src/main/java/com/emc/object/s3/jersey/RetryFilter.java new file mode 100644 index 00000000..0a08fd02 --- /dev/null +++ b/src/main/java/com/emc/object/s3/jersey/RetryFilter.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2015, EMC Corporation. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * + Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + The name of EMC Corporation may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.emc.object.s3.jersey; + +import com.emc.object.s3.S3Config; +import com.emc.object.s3.S3Exception; +import com.sun.jersey.api.client.ClientHandlerException; +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.filter.ClientFilter; +import org.apache.log4j.LogMF; +import org.apache.log4j.Logger; + +import java.io.IOException; +import java.io.InputStream; + +public class RetryFilter extends ClientFilter { + private static final Logger l4j = Logger.getLogger(RetryFilter.class); + + private S3Config s3Config; + + public RetryFilter(S3Config s3Config) { + this.s3Config = s3Config; + } + + @Override + public ClientResponse handle(ClientRequest clientRequest) throws ClientHandlerException { + int retryCount = 0; + InputStream entityStream = null; + if (clientRequest.getEntity() instanceof InputStream) entityStream = (InputStream) clientRequest.getEntity(); + while (true) { + try { + // if using an InputStream, mark the stream so we can rewind it in case of an error + if (entityStream != null && entityStream.markSupported()) + entityStream.mark(s3Config.getRetryBufferSize()); + + return getNext().handle(clientRequest); + } catch (RuntimeException orig) { + Throwable t = orig; + + // in this case, the exception was wrapped by Jersey + if (t instanceof ClientHandlerException) t = t.getCause(); + + if (t instanceof S3Exception) { + S3Exception se = (S3Exception) t; + + // retry all 50x errors except 501 (not implemented) + if (se.getHttpCode() < 500 || se.getHttpCode() == 501) throw orig; + + // retry all IO exceptions + } else if (!(t instanceof IOException)) throw orig; + + // only retry retryLimit times + if (++retryCount > s3Config.getRetryLimit()) throw orig; + + // attempt to reset InputStream + if (entityStream != null) { + try { + if (!entityStream.markSupported()) throw new IOException("stream does not support mark/reset"); + entityStream.reset(); + } catch (IOException e) { + l4j.warn("could not reset entity stream for retry: " + e); + throw orig; + } + } + + // wait for retry delay + if (s3Config.getInitialRetryDelay() > 0) { + int retryDelay = s3Config.getInitialRetryDelay() * (int) Math.pow(2, retryCount - 1); + try { + LogMF.debug(l4j, "waiting {0}ms before retry", retryDelay); + Thread.sleep(retryDelay); + } catch (InterruptedException e) { + l4j.warn("interrupted while waiting to retry: " + e.getMessage()); + } + } + + LogMF.info(l4j, "error received in response [{0}], retrying ({1} of {2})...", t, retryCount, s3Config.getRetryLimit()); + } + } + } +} diff --git a/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java b/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java index 7fc7012b..31d83f00 100644 --- a/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java +++ b/src/main/java/com/emc/object/s3/jersey/S3EncryptionClient.java @@ -110,7 +110,11 @@ public class S3EncryptionClient extends S3JerseyClient { private EncryptionConfig encryptionConfig; public S3EncryptionClient(S3Config s3Config, EncryptionConfig encryptionConfig) { - super(s3Config); + this(s3Config, null, encryptionConfig); + } + + public S3EncryptionClient(S3Config s3Config, ClientHandler clientHandler, EncryptionConfig encryptionConfig) { + super(s3Config, clientHandler); this.encryptionConfig = encryptionConfig; // create an encode chain based on parameters diff --git a/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java b/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java index a1e93ba6..c937b638 100644 --- a/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java +++ b/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java @@ -35,7 +35,6 @@ import com.emc.object.s3.request.*; import com.emc.object.util.RestUtil; import com.emc.rest.smart.LoadBalancer; -import com.emc.rest.smart.PollingDaemon; import com.emc.rest.smart.SmartClientFactory; import com.emc.rest.smart.SmartConfig; import com.emc.rest.smart.ecs.EcsHostListProvider; @@ -43,7 +42,6 @@ import com.sun.jersey.api.client.ClientHandler; import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientResponse; -import org.apache.log4j.Logger; import java.io.InputStream; import java.io.StringReader; @@ -126,8 +124,6 @@ * */ public class S3JerseyClient extends AbstractJerseyClient implements S3Client { - private static final Logger l4j = Logger.getLogger(S3JerseyClient.class); - protected S3Config s3Config; protected Client client; protected LoadBalancer loadBalancer; @@ -187,6 +183,10 @@ public S3JerseyClient(S3Config config, ClientHandler clientHandler) { // S.C. - GEO-PINNING if (s3Config.isGeoPinningEnabled()) loadBalancer.withVetoRules(new GeoPinningRule()); + // S.C. - RETRY CONFIG + if (s3Config.isRetryEnabled()) + smartConfig.setProperty(SmartClientFactory.DISABLE_APACHE_RETRY, Boolean.TRUE); + // S.C. - CLIENT CREATION // create a load-balancing jersey client if (clientHandler == null) { @@ -198,6 +198,7 @@ public S3JerseyClient(S3Config config, ClientHandler clientHandler) { // jersey filters client.addFilter(new ErrorFilter()); + if (s3Config.isRetryEnabled()) client.addFilter(new RetryFilter(s3Config)); // replaces the apache retry handler if (s3Config.isChecksumEnabled()) client.addFilter(new ChecksumFilter()); client.addFilter(new AuthorizationFilter(s3Config)); client.addFilter(new BucketFilter(s3Config)); @@ -208,17 +209,33 @@ public S3JerseyClient(S3Config config, ClientHandler clientHandler) { @Override protected void finalize() throws Throwable { try { - shutdown(); + destroy(); } finally { super.finalize(); // make sure we call super.finalize() no matter what! } } + /** + * @deprecated (2.0.3) use destroy() instead + */ @Override public void shutdown() { - l4j.debug("terminating polling daemon"); - PollingDaemon pollingDaemon = (PollingDaemon) client.getProperties().get(PollingDaemon.PROPERTY_KEY); - if (pollingDaemon != null) pollingDaemon.terminate(); + destroy(); + } + + /** + * Destroy the client. Any system resources associated with the client + * will be cleaned up. + *

+ * This method must be called when there are not responses pending otherwise + * undefined behavior will occur. + *

+ * The client must not be reused after this method is called otherwise + * undefined behavior will occur. + */ + @Override + public void destroy() { + SmartClientFactory.destroy(client); } public LoadBalancer getLoadBalancer() { @@ -304,7 +321,12 @@ public void setBucketCors(String bucketName, CorsConfiguration corsConfiguration @Override public CorsConfiguration getBucketCors(String bucketName) { ObjectRequest request = new GenericBucketRequest(Method.GET, bucketName, "cors"); - return executeRequest(client, request, CorsConfiguration.class); + try { + return executeRequest(client, request, CorsConfiguration.class); + } catch (S3Exception e) { + if ("NoSuchCORSConfiguration".equals(e.getErrorCode())) return null; + throw e; + } } @Override @@ -322,7 +344,12 @@ public void setBucketLifecycle(String bucketName, LifecycleConfiguration lifecyc @Override public LifecycleConfiguration getBucketLifecycle(String bucketName) { ObjectRequest request = new GenericBucketRequest(Method.GET, bucketName, "lifecycle"); - return executeRequest(client, request, LifecycleConfiguration.class); + try { + return executeRequest(client, request, LifecycleConfiguration.class); + } catch (S3Exception e) { + if ("NoSuchBucketPolicy".equals(e.getErrorCode())) return null; + throw e; + } } @Override diff --git a/src/test/java/com/emc/object/s3/AbstractS3ClientTest.java b/src/test/java/com/emc/object/s3/AbstractS3ClientTest.java index dab1e046..c01c652d 100644 --- a/src/test/java/com/emc/object/s3/AbstractS3ClientTest.java +++ b/src/test/java/com/emc/object/s3/AbstractS3ClientTest.java @@ -59,7 +59,7 @@ public void dumpLBStats() { @After public void shutdownClient() { - if (client != null) client.shutdown(); + if (client != null) client.destroy(); } @Override diff --git a/src/test/java/com/emc/object/s3/RetryFilterTest.java b/src/test/java/com/emc/object/s3/RetryFilterTest.java new file mode 100644 index 00000000..600bcb95 --- /dev/null +++ b/src/test/java/com/emc/object/s3/RetryFilterTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2015, EMC Corporation. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * + Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + The name of EMC Corporation may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.emc.object.s3; + +import com.emc.object.s3.jersey.S3JerseyClient; +import com.emc.object.s3.request.PutObjectRequest; +import com.sun.jersey.api.client.ClientHandlerException; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +public class RetryFilterTest extends AbstractS3ClientTest { + protected S3Config s3Config; + + @Override + protected String getTestBucketPrefix() { + return "s3-retry-test"; + } + + @Override + protected void initClient() throws Exception { + client = new S3JerseyClient(createS3Config()); + s3Config = ((S3JerseyClient) client).getS3Config(); + } + + @Test + public void testRetryFilter() throws Exception { + int retryLimit = 3; + final String flagMessage = "XXXXX"; + + S3ObjectMetadata metadata = new S3ObjectMetadata().withContentLength(1).withContentType("text/plain"); + PutObjectRequest request = new PutObjectRequest(getTestBucket(), "foo", + new RetryInputStream(s3Config, flagMessage)).withObjectMetadata(metadata); + try { + client.putObject(request); + Assert.fail("Retried more than retryLimit times"); + } catch (ClientHandlerException e) { + Assert.assertEquals("Wrong exception thrown", flagMessage, e.getCause().getMessage()); + } + + s3Config.setRetryLimit(retryLimit + 1); + + request = new PutObjectRequest(getTestBucket(), "foo", + new RetryInputStream(s3Config, flagMessage)).withObjectMetadata(metadata); + client.putObject(request); + byte[] content = client.readObject(getTestBucket(), "foo", byte[].class); + Assert.assertEquals("Content wrong size", 1, content.length); + Assert.assertEquals("Wrong content", (byte) 65, content[0]); + + try { + request = new PutObjectRequest(getTestBucket(), "foo", + new RetryInputStream(null, null) { + @Override + public int read() throws IOException { + switch (callCount++) { + case 0: + throw new S3Exception("should not retry", 400); + case 1: + return 65; + } + return -1; + } + }).withObjectMetadata(metadata); + client.putObject(request); + Assert.fail("HTTP 400 was retried and should not be"); + } catch (ClientHandlerException e) { + Assert.assertEquals("Wrong http code", 400, ((S3Exception) e.getCause()).getHttpCode()); + } + + try { + request = new PutObjectRequest(getTestBucket(), "foo", + new RetryInputStream(null, null) { + @Override + public int read() throws IOException { + switch (callCount++) { + case 0: + throw new S3Exception("should not retry", 501); + case 1: + return 65; + } + return -1; + } + }).withObjectMetadata(metadata); + client.putObject(request); + Assert.fail("HTTP 501 was retried and should not be"); + } catch (ClientHandlerException e) { + Assert.assertEquals("Wrong http code", 501, ((S3Exception) e.getCause()).getHttpCode()); + } + + try { + request = new PutObjectRequest(getTestBucket(), "foo", + new RetryInputStream(null, null) { + @Override + public int read() throws IOException { + switch (callCount++) { + case 0: + throw new RuntimeException(flagMessage); + case 1: + return 65; + } + return -1; + } + }).withObjectMetadata(metadata); + client.putObject(request); + Assert.fail("RuntimeException was retried and should not be"); + } catch (ClientHandlerException e) { + Assert.assertEquals("Wrong exception message", flagMessage, e.getCause().getMessage()); + } + } + + private class RetryInputStream extends InputStream { + protected int callCount = 0; + private long now; + private long lastTime; + private S3Config s3Config; + private String flagMessage; + + public RetryInputStream(S3Config s3Config, String flagMessage) { + this.s3Config = s3Config; + this.flagMessage = flagMessage; + } + + @Override + public int read() throws IOException { + int retryDelay = s3Config.getInitialRetryDelay() * (int) Math.pow(2, callCount - 1); + switch (callCount++) { + case 0: + lastTime = System.currentTimeMillis(); + throw new S3Exception("foo", 500); + case 1: + now = System.currentTimeMillis(); + Assert.assertTrue("Retry delay for 1st error was not honored", now - lastTime >= retryDelay); + lastTime = now; + throw new S3Exception("bar", 503); + case 2: + now = System.currentTimeMillis(); + Assert.assertTrue("Retry delay for 2nd error was not honored", now - lastTime >= retryDelay); + lastTime = now; + throw new IOException("baz"); + case 3: + now = System.currentTimeMillis(); + Assert.assertTrue("Retry delay for 3rd error was not honored", now - lastTime >= retryDelay); + lastTime = now; + throw new S3Exception(flagMessage, 500); + case 4: + return 65; + } + return -1; + } + + @Override + public synchronized void reset() throws IOException { + } + + @Override + public boolean markSupported() { + return true; + } + } +} diff --git a/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java b/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java index e0fc68c0..8ebaa664 100644 --- a/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java +++ b/src/test/java/com/emc/object/s3/S3EncryptionClientBasicTest.java @@ -368,7 +368,7 @@ public void testDeleteBucketWithObjects() throws Exception { @Ignore @Override - public void testSetBucketAcl() throws Exception { + public void testSetGetBucketAcl() throws Exception { } @Ignore @@ -378,7 +378,7 @@ public void testSetBucketAclCanned() { @Ignore @Override - public void testGetBucketCors() throws Exception { + public void testSetGetBucketCors() throws Exception { } @Ignore @@ -388,7 +388,7 @@ public void testDeleteBucketCors() throws Exception { @Ignore @Override - public void testDeleteBucketLifecycle() throws Exception { + public void testBucketLifecycle() throws Exception { } @Ignore diff --git a/src/test/java/com/emc/object/s3/S3EncryptionUrlConnectionTest.java b/src/test/java/com/emc/object/s3/S3EncryptionUrlConnectionTest.java new file mode 100644 index 00000000..62648ae1 --- /dev/null +++ b/src/test/java/com/emc/object/s3/S3EncryptionUrlConnectionTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2015, EMC Corporation. + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * + Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + The name of EMC Corporation may not be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.emc.object.s3; + +import com.emc.object.s3.jersey.S3EncryptionClient; +import com.emc.object.s3.jersey.S3JerseyClient; +import com.sun.jersey.client.urlconnection.URLConnectionClientHandler; + +public class S3EncryptionUrlConnectionTest extends S3EncryptionClientBasicTest { + @Override + protected String getTestBucketPrefix() { + return "s3-url-connection-test"; + } + + @Override + public void initClient() throws Exception { + System.setProperty("http.maxConnections", "100"); + rclient = new S3JerseyClient(createS3Config(), new URLConnectionClientHandler()); + client = eclient = new S3EncryptionClient(createS3Config(), new URLConnectionClientHandler(), createEncryptionConfig()); + } +} diff --git a/src/test/java/com/emc/object/s3/S3JerseyClientTest.java b/src/test/java/com/emc/object/s3/S3JerseyClientTest.java index 2af194b9..594a97d4 100755 --- a/src/test/java/com/emc/object/s3/S3JerseyClientTest.java +++ b/src/test/java/com/emc/object/s3/S3JerseyClientTest.java @@ -128,17 +128,17 @@ public void testListBucketsReq() { ListBucketsRequest request = new ListBucketsRequest(); ListBucketsResult result = client.listBuckets(request); Assert.assertNotNull(result); - List bucketList = result.getBuckets(); - l4j.debug("There are " + Integer.toString(bucketList.size()) + " existing buckets"); - for (Bucket b: bucketList) { - l4j.debug("JMC bucket: " + b.getName()); - } + Assert.assertNotNull(result.getOwner()); + Assert.assertNotNull(result.getBuckets()); + + Bucket bucket = new Bucket(); + bucket.setName(getTestBucket()); + Assert.assertTrue(result.getBuckets().contains(bucket)); } @Test public void testBucketExists() throws Exception { Assert.assertTrue("Bucket " + getTestBucket() + " should exist but does NOT", client.bucketExists(getTestBucket())); - l4j.debug("JMC testBucketExists succeeded!!!!!!!!!!!!!!!!!!!!!!!!!"); } @Test @@ -146,6 +146,8 @@ public void testCreateBucketRequest() throws Exception { String bucketName = getTestBucket() + "-x"; CreateBucketRequest request = new CreateBucketRequest(bucketName); client.createBucket(request); + + Assert.assertTrue(client.bucketExists(bucketName)); this.cleanUpBucket(bucketName); } @@ -166,8 +168,7 @@ public void testDeleteBucket() throws Exception { @Test public void testDeleteBucketWithObjects() throws Exception { - createTestObjects(getTestBucket(), "prefix/", 5); - l4j.debug("Objects in bucket " + getTestBucket() + " have been created"); + createTestObjects("prefix/", 5); try { client.deleteBucket(getTestBucket()); Assert.fail("Test succeeds. Fail was expected. Can NOT delete bucket with existing objects"); @@ -176,23 +177,8 @@ public void testDeleteBucketWithObjects() throws Exception { } } - protected void assertSameAcl(AccessControlList acl1, AccessControlList acl2) { - Assert.assertEquals(acl1.getOwner().getId(), acl2.getOwner().getId()); - - Set gs1 = acl1.getGrants(); - Set gs2 = acl2.getGrants(); - - Assert.assertEquals(gs1.size(), gs2.size()); - Iterator grantI = acl2.getGrants().iterator(); - for (Grant g1 : acl1.getGrants()) { - Grant g2 = grantI.next(); - Assert.assertEquals(g1.getGrantee(), g2.getGrantee()); - Assert.assertEquals(g1.getPermission(), g2.getPermission()); - } - } - @Test - public void testSetBucketAcl() throws Exception { + public void testSetGetBucketAcl() throws Exception { String identity = createS3Config().getIdentity(); CanonicalUser owner = new CanonicalUser(identity, identity); AccessControlList acl = new AccessControlList(); @@ -201,144 +187,82 @@ public void testSetBucketAcl() throws Exception { client.setBucketAcl(getTestBucket(), acl); - this.assertSameAcl(acl, client.getBucketAcl(getTestBucket())); + this.assertAclEquals(acl, client.getBucketAcl(getTestBucket())); } @Ignore // TODO: blocked by STORAGE-7422 @Test - public void testSetBucketAclCanned() { - client.setBucketAcl(getTestBucket(), CannedAcl.BucketOwnerFullControl); - } + public void testSetBucketAclCanned() throws Exception { + String identity = createS3Config().getIdentity(); + CanonicalUser owner = new CanonicalUser(identity, identity); + AccessControlList acl = new AccessControlList(); + acl.setOwner(owner); + acl.addGrants(new Grant(owner, Permission.FULL_CONTROL)); - //TODO - //void setBucketAcl(SetBucketAclRequest request); - - //tested by testSetBucketAcl - //AccessControlList getBucketAcl(String bucketName); + client.setBucketAcl(getTestBucket(), CannedAcl.BucketOwnerFullControl); - //tested by testGetBucketCors - //void setBucketCors(String bucketName, CorsConfiguration corsConfiguration); + this.assertAclEquals(acl, client.getBucketAcl(getTestBucket())); + } - @Test - public void testGetBucketCors() throws Exception { - //CorsConfiguration getBucketCors(String bucketName); - - ArrayList crArr = new ArrayList(); - List crArrVerify = new ArrayList(); - - CorsRule cr0 = new CorsRule(); - cr0.setId("corsRuleTestId0"); - cr0.withAllowedOrigins("10.10.10.10"); - cr0.withAllowedMethods(CorsMethod.GET); - crArr.add(cr0); - crArrVerify.add(cr0); + public void testSetGetBucketCors() throws Exception { + CorsRule cr0 = new CorsRule().withId("corsRuleTestId0"); + cr0.withAllowedOrigins("10.10.10.10").withAllowedMethods(CorsMethod.GET); - CorsRule cr1 = new CorsRule(); - cr1.setId("corsRuleTestId1"); - cr1.withAllowedOrigins("10.10.10.10"); - cr1.withAllowedMethods(CorsMethod.GET); - crArr.add(cr1); - crArrVerify.add(cr1); + CorsRule cr1 = new CorsRule().withId("corsRuleTestId1"); + cr1.withAllowedOrigins("10.10.10.10").withAllowedMethods(CorsMethod.GET); - CorsRule cr2 = new CorsRule(); - cr2.setId("corsRuleTestId2"); - cr2.withAllowedOrigins("10.10.10.10"); - cr2.withAllowedMethods(CorsMethod.GET); - crArr.add(cr2); - crArrVerify.add(cr2); - - CorsConfiguration cc = new CorsConfiguration(); - cc.setCorsRules(crArr); + CorsRule cr2 = new CorsRule().withId("corsRuleTestId2"); + cr2.withAllowedOrigins("10.10.10.10").withAllowedMethods(CorsMethod.GET); + CorsConfiguration cc = new CorsConfiguration().withCorsRules(cr0, cr1, cr2); client.setBucketCors(getTestBucket(), cc); CorsConfiguration ccVerify = client.getBucketCors(getTestBucket()); Assert.assertNotNull("CorsConfiguration should NOT be null but is", ccVerify); - crArrVerify = ccVerify.getCorsRules(); - Assert.assertNotNull("CorsRule list should NOT be null but is", crArrVerify); - Assert.assertEquals("There are NOT the same number of CorsRule items", crArr.size(), crArrVerify.size()); - - //JMC the rules might not come back in the same order! need to change - //maybe could brute force or look into sort or rules (based upon id maybe) - int testResults = 0x0; - for (CorsRule aCrArrVerify : crArrVerify) { - if (cr0.getId().equals(aCrArrVerify.getId())) { - //Assert.assertEquals(crArr.get(i).getId(), crArrVerify.get(i).getId()); - testResults = testResults | 0x001; - } - if (cr1.getId().equals(aCrArrVerify.getId())) { - //Assert.assertEquals(crArr.get(i).getId(), crArrVerify.get(i).getId()); - testResults = testResults | 0x010; - } - if (cr1.getId().equals(aCrArrVerify.getId())) { - //Assert.assertEquals(crArr.get(i).getId(), crArrVerify.get(i).getId()); - testResults = testResults | 0x100; - } + Assert.assertEquals(cc.getCorsRules().size(), ccVerify.getCorsRules().size()); + + for (CorsRule rule : cc.getCorsRules()) { + Assert.assertTrue(ccVerify.getCorsRules().contains(rule)); } - Assert.assertEquals("Incorrect CorRules returned", 0x111, testResults); } - @Test public void testDeleteBucketCors() throws Exception { - ArrayList crArr = new ArrayList(); - - CorsRule cr = new CorsRule(); - cr.setId("corsRuleTestId"); - cr.withAllowedMethods(CorsMethod.GET).withAllowedOrigins("10.10.10.10"); - crArr.add(cr); + CorsRule cr0 = new CorsRule().withId("corsRuleTestId0"); + cr0.withAllowedOrigins("10.10.10.10").withAllowedMethods(CorsMethod.GET); - CorsConfiguration cc = new CorsConfiguration(); - cc.setCorsRules(crArr); + CorsConfiguration cc = new CorsConfiguration().withCorsRules(cr0); client.setBucketCors(getTestBucket(), cc); - CorsConfiguration ccVerify = client.getBucketCors(getTestBucket()); - Assert.assertNotNull("deleteBucketCors NOT tested ccVerify prereq is null", ccVerify); + Assert.assertNotNull(client.getBucketCors(getTestBucket())); - client.deleteBucketCors(getTestBucket());//PRIMARY TEST CALL - try { - client.getBucketCors(getTestBucket()); - Assert.fail("getting non-existing cors config should throw exception"); - } catch (S3Exception e) { - Assert.assertEquals("Wrong error code when getting non-existing cors config", "NoSuchCORSConfiguration", e.getErrorCode()); - } + client.deleteBucketCors(getTestBucket()); - //make sure the bucket still exists - Assert.assertTrue("deleteBucketCors succeeded, but bucket does not exist", client.bucketExists(getTestBucket())); + Assert.assertNull(client.getBucketCors(getTestBucket())); } - //see testDeleteBucketLifecycle - //void setBucketLifecycle(String bucketName, LifecycleConfiguration lifecycleConfiguration); - - //see testDeleteBucketLifecycle - //LifecycleConfiguration getBucketLifecycle(String bucketName); - - - //TODO @Test - public void testDeleteBucketLifecycle() throws Exception { - String bn = getTestBucket(); - LifecycleRule lcr = new LifecycleRule(); - ArrayList lcrList = new ArrayList(); - lcrList.add(lcr); + public void testBucketLifecycle() throws Exception { + Calendar end = Calendar.getInstance(); + end.add(Calendar.YEAR, 300); LifecycleConfiguration lc = new LifecycleConfiguration(); - lc.setRules(lcrList); + lc.withRules(new LifecycleRule("archive-expires-30", "archive/", LifecycleRule.Status.Enabled, 180), + new LifecycleRule("archive-disabled", "archive/", LifecycleRule.Status.Disabled, 365), + new LifecycleRule("armageddon", "", LifecycleRule.Status.Enabled, end.getTime())); - //String bucketName = createBucketAndName(); - client.setBucketLifecycle(bn, lc); - Assert.assertNotNull(client.getBucketLifecycle(bn)); - client.deleteBucketLifecycle(bn); //PRIMARY TEST CALL - try { - client.getBucketLifecycle(bn); - Assert.fail("getting non-existing bucket lifecycle should throw exception"); - } catch (S3Exception e) { - Assert.assertEquals("wrong error code for getting non-existing bucket lifecycle", "NoSuchBucketPolicy", e.getErrorCode()); + client.setBucketLifecycle(getTestBucket(), lc); + + LifecycleConfiguration lc2 = client.getBucketLifecycle(getTestBucket()); + Assert.assertNotNull(lc2); + Assert.assertEquals(lc.getRules().size(), lc2.getRules().size()); + + for (LifecycleRule rule : lc.getRules()) { + Assert.assertTrue(lc2.getRules().contains(rule)); } - //make sure the bucket still exists - Assert.assertTrue("deleteBucketLifecycle succeeded, but bucket does not exist", client.bucketExists(bn)); - //cleanUpBucket(bn); + client.deleteBucketLifecycle(getTestBucket()); + Assert.assertNull(client.getBucketLifecycle(getTestBucket())); } @Test @@ -505,20 +429,15 @@ public void testListVersionsReq() throws Exception { //Assert.assertNotNull(lvr.getTruncated()); Assert.assertNotNull(lvr.getVersions()); } - - protected void createTestObjects(String prefixWithDelim, int numObjects) throws Exception { - this.createTestObjects(getTestBucket(), prefixWithDelim, numObjects); - } - protected void createTestObjects(String bucket, String prefixWithDelim, int numObjects) throws Exception { - String objectName; + protected void createTestObjects(String prefix, int numObjects) throws Exception { + if (prefix == null) prefix = ""; - byte[] content1 = new byte[5 * 1024]; - new Random().nextBytes(content1); + byte[] content = new byte[5 * 1024]; + new Random().nextBytes(content); for(int i=0; i