This is an automated email from the ASF dual-hosted git repository.

sijie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pulsar.git


The following commit(s) were added to refs/heads/master by this push:
     new b6f93f0  InputStream to read from S3 (#1768)
b6f93f0 is described below

commit b6f93f0b4aa4fa96c419bc84150719cd262899ae
Author: Ivan Kelly <[email protected]>
AuthorDate: Tue May 15 13:59:10 2018 +0200

    InputStream to read from S3 (#1768)
    
    * InputStream to read from S3
    
    Reads from S3 in configurable chunks. Interface allows caller to seek
    to an arbitrary position.
    
    Master Issue: #1511
    
    * Make s3mock use anon credentials to avoid going to S3
    
    Otherwise it uses the default chain which contacts S3. This is
    expensive and time consuming.
    
    * Added read(byte[], int, int)
    
    and other review comments
---
 .../broker/s3offload/S3BackedInputStream.java      |  27 ++++
 .../s3offload/impl/S3BackedInputStreamImpl.java    | 133 ++++++++++++++++
 .../broker/s3offload/S3BackedInputStreamTest.java  | 177 +++++++++++++++++++++
 .../apache/pulsar/broker/s3offload/S3TestBase.java |   3 +
 4 files changed, 340 insertions(+)

diff --git 
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/s3offload/S3BackedInputStream.java
 
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/s3offload/S3BackedInputStream.java
new file mode 100644
index 0000000..28e6855
--- /dev/null
+++ 
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/s3offload/S3BackedInputStream.java
@@ -0,0 +1,27 @@
+/**
+ * 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.pulsar.broker.s3offload;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+public abstract class S3BackedInputStream extends InputStream {
+    public abstract void seek(long position);
+    public abstract void seekForward(long position) throws IOException;
+}
diff --git 
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/s3offload/impl/S3BackedInputStreamImpl.java
 
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/s3offload/impl/S3BackedInputStreamImpl.java
new file mode 100644
index 0000000..0c5e3df
--- /dev/null
+++ 
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/s3offload/impl/S3BackedInputStreamImpl.java
@@ -0,0 +1,133 @@
+/**
+ * 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.pulsar.broker.s3offload.impl;
+
+import com.amazonaws.AmazonClientException;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.GetObjectRequest;
+import com.amazonaws.services.s3.model.S3Object;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.PooledByteBufAllocator;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+import org.apache.pulsar.broker.s3offload.S3BackedInputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class S3BackedInputStreamImpl extends S3BackedInputStream {
+    private static final Logger log = 
LoggerFactory.getLogger(S3BackedInputStreamImpl.class);
+
+    private final AmazonS3 s3client;
+    private final String bucket;
+    private final String key;
+    private final ByteBuf buffer;
+    private final long objectLen;
+    private final int bufferSize;
+
+    private long cursor;
+
+    public S3BackedInputStreamImpl(AmazonS3 s3client, String bucket, String 
key,
+                                   long objectLen, int bufferSize) {
+        this.s3client = s3client;
+        this.bucket = bucket;
+        this.key = key;
+        this.buffer = PooledByteBufAllocator.DEFAULT.buffer(bufferSize, 
bufferSize);
+        this.objectLen = objectLen;
+        this.bufferSize = bufferSize;
+        this.cursor = 0;
+    }
+
+    /**
+     * Refill the buffered input if it is empty.
+     * @return true if there are bytes to read, false otherwise
+     */
+    private boolean refillBufferIfNeeded() throws IOException {
+        if (buffer.readableBytes() == 0) {
+            if (cursor >= objectLen) {
+                return false;
+            }
+            long startRange = cursor;
+            long endRange = Math.min(cursor + bufferSize - 1,
+                                     objectLen - 1);
+            GetObjectRequest req = new GetObjectRequest(bucket, key)
+                .withRange(startRange, endRange);
+            log.debug("Reading range {}-{} from {}/{}", startRange, endRange, 
bucket, key);
+            try (S3Object obj = s3client.getObject(req)) {
+                Long[] range = obj.getObjectMetadata().getContentRange();
+                long bytesRead = range[1] - range[0] + 1;
+
+                buffer.clear();
+                InputStream s = obj.getObjectContent();
+                int bytesToCopy = (int)bytesRead;
+                while (bytesToCopy > 0) {
+                    bytesToCopy -= buffer.writeBytes(s, bytesToCopy);
+                }
+                cursor += buffer.readableBytes();
+            } catch (AmazonClientException e) {
+                throw new IOException("Error reading from S3", e);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (refillBufferIfNeeded()) {
+            return buffer.readUnsignedByte();
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (refillBufferIfNeeded()) {
+            int bytesToRead = Math.min(len, buffer.readableBytes());
+            buffer.readBytes(b, off, bytesToRead);
+            return bytesToRead;
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public void seek(long position) {
+        log.debug("Seeking to {} on {}/{}, current position {}", position, 
bucket, key, cursor);
+        this.cursor = position;
+        buffer.clear();
+    }
+
+    @Override
+    public void seekForward(long position) throws IOException {
+        if (position >= cursor) {
+            seek(position);
+        } else {
+            throw new IOException(String.format("Error seeking, new position 
%d < current position %d",
+                                                position, cursor));
+        }
+    }
+
+    @Override
+    public void close() {
+        buffer.release();
+    }
+}
diff --git 
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/s3offload/S3BackedInputStreamTest.java
 
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/s3offload/S3BackedInputStreamTest.java
new file mode 100644
index 0000000..1155c2b
--- /dev/null
+++ 
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/s3offload/S3BackedInputStreamTest.java
@@ -0,0 +1,177 @@
+/**
+ * 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.pulsar.broker.s3offload;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.apache.pulsar.broker.s3offload.impl.S3BackedInputStreamImpl;
+
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@Slf4j
+class S3BackedInputStreamTest extends S3TestBase {
+    class RandomInputStream extends InputStream {
+        final Random r;
+        int bytesRemaining;
+
+        RandomInputStream(int seed, int bytesRemaining) {
+            this.r = new Random(seed);
+            this.bytesRemaining = bytesRemaining;
+        }
+
+        @Override
+        public int read() {
+            if (bytesRemaining-- > 0) {
+                return r.nextInt() & 0xFF;
+            } else {
+                return -1;
+            }
+        }
+    }
+
+    private void assertStreamsMatch(InputStream a, InputStream b) throws 
Exception {
+        int ret = 0;
+        while (ret >= 0) {
+            ret = a.read();
+            Assert.assertEquals(ret, b.read());
+        }
+        Assert.assertEquals(-1, a.read());
+        Assert.assertEquals(-1, b.read());
+    }
+
+    private void assertStreamsMatchByBytes(InputStream a, InputStream b) 
throws Exception {
+        byte[] bytesA = new byte[100];
+        byte[] bytesB = new byte[100];
+
+        int retA = 0;
+        while (retA >= 0) {
+            retA = a.read(bytesA, 0, 100);
+            int retB = b.read(bytesB, 0, 100);
+            Assert.assertEquals(retA, retB);
+            Assert.assertEquals(bytesA, bytesB);
+        }
+    }
+
+    @Test
+    public void testReadingFullObject() throws Exception {
+        String objectKey = "foobar";
+        int objectSize = 12345;
+        RandomInputStream toWrite = new RandomInputStream(0, objectSize);
+        RandomInputStream toCompare = new RandomInputStream(0, objectSize);
+
+        ObjectMetadata metadata = new ObjectMetadata();
+        metadata.setContentLength(objectSize);
+        s3client.putObject(BUCKET, objectKey, toWrite, metadata);
+
+        S3BackedInputStream toTest = new S3BackedInputStreamImpl(s3client, 
BUCKET, objectKey, objectSize, 1000);
+        assertStreamsMatch(toTest, toCompare);
+    }
+
+    @Test
+    public void testReadingFullObjectByBytes() throws Exception {
+        String objectKey = "foobar";
+        int objectSize = 12345;
+        RandomInputStream toWrite = new RandomInputStream(0, objectSize);
+        RandomInputStream toCompare = new RandomInputStream(0, objectSize);
+
+        ObjectMetadata metadata = new ObjectMetadata();
+        metadata.setContentLength(objectSize);
+        s3client.putObject(BUCKET, objectKey, toWrite, metadata);
+
+        S3BackedInputStream toTest = new S3BackedInputStreamImpl(s3client, 
BUCKET, objectKey, objectSize, 1000);
+        assertStreamsMatchByBytes(toTest, toCompare);
+    }
+
+    @Test(expectedExceptions = IOException.class)
+    public void testErrorOnS3Read() throws Exception {
+        S3BackedInputStream toTest = new S3BackedInputStreamImpl(s3client, 
BUCKET, "doesn't exist", 1234, 1000);
+        toTest.read();
+    }
+
+
+    @Test
+    public void testSeek() throws Exception {
+        String objectKey = "foobar";
+        int objectSize = 12345;
+        RandomInputStream toWrite = new RandomInputStream(0, objectSize);
+
+        Map<Integer, InputStream> seeks = new HashMap<>();
+        Random r = new Random(12345);
+        for (int i = 0; i < 20; i++) {
+            int seek = r.nextInt(objectSize+1);
+            RandomInputStream stream = new RandomInputStream(0, objectSize);
+            stream.skip(seek);
+            seeks.put(seek, stream);
+        }
+
+        ObjectMetadata metadata = new ObjectMetadata();
+        metadata.setContentLength(objectSize);
+        s3client.putObject(BUCKET, objectKey, toWrite, metadata);
+
+        S3BackedInputStream toTest = new S3BackedInputStreamImpl(s3client, 
BUCKET, objectKey, objectSize, 1000);
+        for (Map.Entry<Integer, InputStream> e : seeks.entrySet()) {
+            toTest.seek(e.getKey());
+            assertStreamsMatch(toTest, e.getValue());
+        }
+    }
+
+    @Test
+    public void testSeekForward() throws Exception {
+        String objectKey = "foobar";
+        int objectSize = 12345;
+        RandomInputStream toWrite = new RandomInputStream(0, objectSize);
+
+        ObjectMetadata metadata = new ObjectMetadata();
+        metadata.setContentLength(objectSize);
+        s3client.putObject(BUCKET, objectKey, toWrite, metadata);
+
+        S3BackedInputStream toTest = new S3BackedInputStreamImpl(s3client, 
BUCKET, objectKey, objectSize, 1000);
+
+        // seek forward to middle
+        long middle = objectSize/2;
+        toTest.seekForward(middle);
+
+        try {
+            long before = middle - objectSize/4;
+            toTest.seekForward(before);
+            Assert.fail("Shound't be able to seek backwards");
+        } catch (IOException ioe) {
+            // correct
+        }
+
+        long after = middle + objectSize/4;
+        RandomInputStream toCompare = new RandomInputStream(0, objectSize);
+        toCompare.skip(after);
+
+        toTest.seekForward(after);
+        assertStreamsMatch(toTest, toCompare);
+    }
+}
diff --git 
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/s3offload/S3TestBase.java
 
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/s3offload/S3TestBase.java
index e3b994b..2c65c0c 100644
--- 
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/s3offload/S3TestBase.java
+++ 
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/s3offload/S3TestBase.java
@@ -18,6 +18,8 @@
  */
 package org.apache.pulsar.broker.s3offload;
 
+import com.amazonaws.auth.AnonymousAWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
 import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration;
 import com.amazonaws.services.s3.AmazonS3;
 import com.amazonaws.services.s3.AmazonS3ClientBuilder;
@@ -46,6 +48,7 @@ public class S3TestBase {
         } else {
             s3client = AmazonS3ClientBuilder.standard()
                 .withEndpointConfiguration(new 
EndpointConfiguration(s3endpoint, "foobar"))
+                .withCredentials(new AWSStaticCredentialsProvider(new 
AnonymousAWSCredentials()))
                 .withPathStyleAccessEnabled(true).build();
         }
 

-- 
To stop receiving notification emails like this one, please contact
[email protected].

Reply via email to