/** * Copyright 2016 StreamSets Inc. * * Licensed under 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 com.streamsets.pipeline.lib.io.fileref; import com.google.common.util.concurrent.RateLimiter; import com.streamsets.pipeline.api.FileRef; import com.streamsets.pipeline.api.OnRecordError; import com.streamsets.pipeline.api.Stage; import com.streamsets.pipeline.sdk.ContextInfoCreator; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.mockito.internal.util.reflection.Whitebox; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Random; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; public class TestRateLimitingWrapperStream { private static final int RATE_LIMIT = new Random().nextInt(4) + 1; private File testDir; private Stage.Context context; @Before public void setup() throws Exception { testDir = new File("target", UUID.randomUUID().toString()); Assert.assertTrue(testDir.mkdirs()); FileRefTestUtil.writePredefinedTextToFile(testDir); context = ContextInfoCreator.createTargetContext("", false, OnRecordError.TO_ERROR); } @After public void tearDown() throws Exception { testDir.delete(); } private static FileRef getLocalFileRef(File testDir, double rateLimit) throws IOException { return new LocalFileRef.Builder() .filePath(FileRefTestUtil.getSourceFilePath(testDir)) .bufferSize(FileRefTestUtil.TEXT.getBytes().length / 2) .totalSizeInBytes(FileRefTestUtil.TEXT.getBytes().length) .createMetrics(false) .rateLimit(rateLimit) .build(); } private <T extends AutoCloseable> void intercept(T stream, final AtomicInteger bytesWishToBeRead, final AtomicBoolean isRateLimiterAcquired) { RateLimiter rateLimiter = (RateLimiter) Whitebox.getInternalState(stream, "rateLimiter"); Assert.assertEquals(RATE_LIMIT, rateLimiter.getRate(), 0); Mockito.doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { Object[] args = invocationOnMock.getArguments(); bytesWishToBeRead.compareAndSet(-1, (int)args[0]); return invocationOnMock.callRealMethod(); } }).when((RateLimitingWrapperStream)stream).performPreReadOperation(Mockito.anyInt()); Mockito.doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { //basically book keep that we are acquiring the rate limit. isRateLimiterAcquired.compareAndSet(false, true); return null; } }).when((RateLimitingWrapperStream)stream).acquire(Mockito.anyInt()); } private static <T extends AutoCloseable> long getRemainingStreamSize(T stream) { return (long) Whitebox.getInternalState(stream, "remainingStreamSize"); } private <T extends AutoCloseable> void checkState( T stream, long remainingFileSize, int readCalledWithBytesToBeRead, AtomicInteger bytesWishToBeRead, AtomicBoolean isRateLimiterAcquired ) { Assert.assertEquals(remainingFileSize, getRemainingStreamSize(stream)); Assert.assertEquals(readCalledWithBytesToBeRead, bytesWishToBeRead.get()); long optimizedBytesWishToBeRead = Math.min(remainingFileSize, readCalledWithBytesToBeRead); if (optimizedBytesWishToBeRead > 0) { Assert.assertTrue(isRateLimiterAcquired.get()); } } @Test(expected = IllegalArgumentException.class) public void testNegativeRateLimit() throws Exception { new RateLimitingWrapperStream<>(new ByteArrayInputStream("a".getBytes()), 1, -2); } @Test public void testIOReadParameterLess() throws Exception { FileRef fileRef = getLocalFileRef(testDir, RATE_LIMIT); long fileSize = Files.size(Paths.get(FileRefTestUtil.getSourceFilePath(testDir))); long remainingFileSize = fileSize; try (InputStream is = Mockito.spy(fileRef.createInputStream(context, InputStream.class))) { AtomicInteger bytesWishToBeRead = new AtomicInteger(-1); AtomicBoolean isRateLimiterAcquired = new AtomicBoolean(false); intercept(is, bytesWishToBeRead, isRateLimiterAcquired); Assert.assertEquals(fileSize, getRemainingStreamSize(is)); while(is.read() != -1) { remainingFileSize--; checkState(is, remainingFileSize, 1, bytesWishToBeRead, isRateLimiterAcquired); bytesWishToBeRead.set(-1); isRateLimiterAcquired.set(false); } } } @Test public void testIOReadParameterized1() throws Exception { FileRef fileRef = getLocalFileRef(testDir, RATE_LIMIT); long fileSize = Files.size(Paths.get(FileRefTestUtil.getSourceFilePath(testDir))); long remainingFileSize = fileSize; try (InputStream is = Mockito.spy(fileRef.createInputStream(context, InputStream.class))) { AtomicInteger bytesWishToBeRead = new AtomicInteger(-1); AtomicBoolean isRateLimiterAcquired = new AtomicBoolean(false); intercept(is, bytesWishToBeRead, isRateLimiterAcquired); Assert.assertEquals(fileSize, getRemainingStreamSize(is)); int bytesRead; byte[] b = new byte[10]; while( (bytesRead = is.read(b)) > 0) { remainingFileSize -= bytesRead; checkState(is, remainingFileSize, b.length, bytesWishToBeRead, isRateLimiterAcquired); bytesWishToBeRead.set(-1); isRateLimiterAcquired.set(false); } Assert.assertFalse(isRateLimiterAcquired.get()); } } @Test public void testIOReadParameterized2() throws Exception { FileRef fileRef = getLocalFileRef(testDir, RATE_LIMIT); long fileSize = Files.size(Paths.get(FileRefTestUtil.getSourceFilePath(testDir))); long remainingFileSize = fileSize; try (InputStream is = Mockito.spy(fileRef.createInputStream(context, InputStream.class))) { AtomicInteger bytesWishToBeRead = new AtomicInteger(-1); AtomicBoolean isRateLimiterAcquired = new AtomicBoolean(false); intercept(is, bytesWishToBeRead, isRateLimiterAcquired); Assert.assertEquals(fileSize, getRemainingStreamSize(is)); int bytesRead; byte[] b = new byte[10]; while( (bytesRead = is.read(b, 0, b.length)) > 0) { remainingFileSize -= bytesRead; checkState(is, remainingFileSize, b.length, bytesWishToBeRead, isRateLimiterAcquired); bytesWishToBeRead.set(-1); isRateLimiterAcquired.set(false); } Assert.assertFalse(isRateLimiterAcquired.get()); } } @Test public void testIOReadsMixed() throws Exception { FileRef fileRef = getLocalFileRef(testDir, RATE_LIMIT); long fileSize = Files.size(Paths.get(FileRefTestUtil.getSourceFilePath(testDir))); long remainingFileSize = fileSize; try (InputStream is = Mockito.spy(fileRef.createInputStream(context, InputStream.class))) { AtomicInteger bytesWishToBeRead = new AtomicInteger(-1); AtomicBoolean isRateLimiterAcquired = new AtomicBoolean(false); intercept(is, bytesWishToBeRead, isRateLimiterAcquired); Assert.assertEquals(fileSize, getRemainingStreamSize(is)); int bytesRead; while ((bytesRead= FileRefTestUtil.randomReadMethodsWithInputStream(is)) > 0) { remainingFileSize -= bytesRead; Assert.assertTrue(isRateLimiterAcquired.get()); Assert.assertEquals(remainingFileSize, getRemainingStreamSize(is)); bytesWishToBeRead.set(-1); isRateLimiterAcquired.set(false); } Assert.assertFalse(isRateLimiterAcquired.get()); } } @Test public void testNIOReadWithDirectBuffer() throws Exception { FileRef fileRef = getLocalFileRef(testDir, RATE_LIMIT); long fileSize = Files.size(Paths.get(FileRefTestUtil.getSourceFilePath(testDir))); long remainingFileSize = fileSize; try (ReadableByteChannel is = Mockito.spy(fileRef.createInputStream(context, ReadableByteChannel.class))) { AtomicInteger bytesWishToBeRead = new AtomicInteger(-1); AtomicBoolean isRateLimiterAcquired = new AtomicBoolean(false); intercept(is, bytesWishToBeRead, isRateLimiterAcquired); Assert.assertEquals(fileSize, getRemainingStreamSize(is)); ByteBuffer b = ByteBuffer.allocateDirect(10); int bytesRead; int freeSpaceInBuffer = b.remaining(); while((bytesRead = is.read(b)) > 0) { remainingFileSize -= bytesRead; checkState(is, remainingFileSize, freeSpaceInBuffer, bytesWishToBeRead, isRateLimiterAcquired); bytesWishToBeRead.set(-1); isRateLimiterAcquired.set(false); b.compact(); freeSpaceInBuffer = b.remaining(); } Assert.assertFalse(isRateLimiterAcquired.get()); } } @Test public void testNIOReadWithHeapByteBuffer() throws Exception { FileRef fileRef = getLocalFileRef(testDir, RATE_LIMIT); long fileSize = Files.size(Paths.get(FileRefTestUtil.getSourceFilePath(testDir))); long remainingFileSize = fileSize; try (ReadableByteChannel is = Mockito.spy(fileRef.createInputStream(context, ReadableByteChannel.class))) { AtomicInteger bytesWishToBeRead = new AtomicInteger(-1); AtomicBoolean isRateLimiterAcquired = new AtomicBoolean(false); intercept(is, bytesWishToBeRead, isRateLimiterAcquired); Assert.assertEquals(fileSize, getRemainingStreamSize(is)); ByteBuffer b = ByteBuffer.allocate(10); int bytesRead; int freeSpaceInBuffer = b.remaining(); while((bytesRead = is.read(b)) > 0) { remainingFileSize -= bytesRead; checkState(is, remainingFileSize, freeSpaceInBuffer, bytesWishToBeRead, isRateLimiterAcquired); bytesWishToBeRead.set(-1); isRateLimiterAcquired.set(false); b.compact(); freeSpaceInBuffer = b.remaining(); } Assert.assertFalse(isRateLimiterAcquired.get()); } } }