/*
* 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 gobblin.util.limiter;
import java.io.Closeable;
import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.testng.Assert;
import org.testng.annotations.Test;
import com.google.common.base.Optional;
import com.google.common.collect.Queues;
import com.linkedin.common.callback.Callback;
import com.linkedin.restli.client.Response;
import com.linkedin.restli.client.RestClient;
import com.linkedin.restli.client.RestLiResponseException;
import com.linkedin.restli.common.HttpStatus;
import gobblin.restli.throttling.PermitAllocation;
import gobblin.restli.throttling.PermitRequest;
import gobblin.util.ExecutorsUtils;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
public class BatchedPermitsRequesterTest {
@Test
public void testForwardingOfRequests() throws Exception {
Queue<RequestAndCallback> queue = Queues.newArrayDeque();
BatchedPermitsRequester container = BatchedPermitsRequester.builder().resourceId("resource")
.requestorIdentifier("requestor").requestSender(new TestRequestSender(queue, false)).build();
try (ParallelRequester requester = new ParallelRequester(container)) {
Future<Boolean> future = requester.request(10);
await(new QueueSize(queue, 1), 1000);
Assert.assertEquals(queue.size(), 1);
satisfyRequestBuilder().requestAndCallback(queue.poll()).satisfy();
future.get(1, TimeUnit.SECONDS);
Assert.assertTrue(future.isDone());
Assert.assertTrue(future.get());
}
}
@Test
public void testNoMoreThanOneRequestAtATime() throws Exception {
Queue<RequestAndCallback> queue = Queues.newArrayDeque();
BatchedPermitsRequester container = BatchedPermitsRequester.builder().resourceId("resource")
.requestorIdentifier("requestor").requestSender(new TestRequestSender(queue, false)).build();
try (ParallelRequester requester = new ParallelRequester(container)) {
Future<Boolean> future = requester.request(1);
await(new QueueSize(queue, 1), 1000);
Assert.assertEquals(queue.size(), 1);
Future<Boolean> future2 = requester.request(2);
Future<Boolean> future3 = requester.request(3);
Future<Boolean> future4 = requester.request(4);
Future<Boolean> future5 = requester.request(5);
Thread.sleep(100);
Assert.assertEquals(queue.size(), 1);
satisfyRequestBuilder().requestAndCallback(queue.poll()).satisfy();
future.get(1, TimeUnit.SECONDS);
Assert.assertTrue(future.isDone());
Assert.assertTrue(future.get());
await(new QueueSize(queue, 1), 1000);
Assert.assertEquals(queue.size(), 1);
satisfyRequestBuilder().requestAndCallback(queue.poll()).satisfy();
future2.get(1, TimeUnit.SECONDS);
future3.get(1, TimeUnit.SECONDS);
future4.get(1, TimeUnit.SECONDS);
future5.get(1, TimeUnit.SECONDS);
Assert.assertTrue(future2.get());
Assert.assertTrue(future3.get());
Assert.assertTrue(future4.get());
Assert.assertTrue(future5.get());
}
}
@Test
public void testRetriableFail() throws Exception {
Queue<RequestAndCallback> queue = Queues.newArrayDeque();
BatchedPermitsRequester container = BatchedPermitsRequester.builder().resourceId("resource")
.requestorIdentifier("requestor").requestSender(new TestRequestSender(queue, false)).build();
try (ParallelRequester requester = new ParallelRequester(container)) {
Future<Boolean> future = requester.request(10);
for (int i = 0; i < BatchedPermitsRequester.MAX_RETRIES; i++) {
// container will fail 5 times
await(new QueueSize(queue, 1), 1000);
Assert.assertFalse(future.isDone());
failRequestBuilder().requestAndCallback(queue.poll()).fail();
}
// should return a failure
Assert.assertFalse(future.get());
// should not make any more request
Assert.assertEquals(queue.size(), 0);
}
}
@Test
public void testNonRetriableFail() throws Exception {
Queue<RequestAndCallback> queue = Queues.newArrayDeque();
BatchedPermitsRequester container = BatchedPermitsRequester.builder().resourceId("resource")
.requestorIdentifier("requestor").requestSender(new TestRequestSender(queue, false)).build();
try (ParallelRequester requester = new ParallelRequester(container)) {
Future<Boolean> future = requester.request(10);
// container should only try request once
await(new QueueSize(queue, 1), 1000);
Assert.assertFalse(future.isDone());
failRequestBuilder().requestAndCallback(queue.poll()).errorStatus(HttpStatus.S_422_UNPROCESSABLE_ENTITY).fail();
Assert.assertFalse(future.get());
Assert.assertEquals(queue.size(), 0);
}
}
public static class TestRequestSender implements RequestSender {
private final Queue<RequestAndCallback> requestAndCallbacks;
private final boolean autoSatisfyRequests;
public TestRequestSender(Queue<RequestAndCallback> requestAndCallbacks, boolean autoSatisfyRequests) {
this.requestAndCallbacks = requestAndCallbacks;
this.autoSatisfyRequests = autoSatisfyRequests;
}
@Override
public void sendRequest(PermitRequest request, Callback<Response<PermitAllocation>> callback) {
if (this.autoSatisfyRequests) {
satisfyRequestBuilder().requestAndCallback(new RequestAndCallback(request, callback)).satisfy();
} else {
this.requestAndCallbacks.add(new RequestAndCallback(request, callback));
}
}
}
@Builder(builderMethodName = "satisfyRequestBuilder", buildMethodName = "satisfy")
public static void satisfyRequest(RequestAndCallback requestAndCallback, long expiration) {
PermitAllocation allocation = new PermitAllocation();
allocation.setPermits(requestAndCallback.getRequest().getPermits());
allocation.setExpiration(expiration > 0 ? expiration : Long.MAX_VALUE);
Response<PermitAllocation> response = Mockito.mock(Response.class);
Mockito.when(response.getEntity()).thenReturn(allocation);
requestAndCallback.getCallback().onSuccess(response);
}
@Builder(builderMethodName = "failRequestBuilder", buildMethodName = "fail")
public static void failRequest(RequestAndCallback requestAndCallback, Throwable exception, HttpStatus errorStatus) {
Throwable actualException;
if (errorStatus != null) {
RestLiResponseException restException = Mockito.mock(RestLiResponseException.class);
Mockito.when(restException.getStatus()).thenReturn(errorStatus.getCode());
actualException = restException;
} else if (exception != null) {
actualException = exception;
} else {
actualException = new RuntimeException();
}
requestAndCallback.callback.onError(actualException);
}
@Data
public static class RequestAndCallback {
private final PermitRequest request;
private final Callback<Response<PermitAllocation>> callback;
}
private static class ParallelRequester implements Closeable {
private final BatchedPermitsRequester container;
private final ExecutorService executorService;
public ParallelRequester(BatchedPermitsRequester container) {
this.container = container;
this.executorService = Executors.newCachedThreadPool(ExecutorsUtils.newThreadFactory(Optional.<Logger>absent(), Optional.of("parallel-requester-%d")));
}
public Future<Boolean> request(final long permits) {
return this.executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return container.getPermits(permits);
}
});
}
@Override
public void close() throws IOException {
if (this.executorService != null) {
this.executorService.shutdownNow();
}
}
}
private void await(Callable<Boolean> condition, int millis) throws Exception {
while (!condition.call()) {
millis -= 50;
if (millis < 0) {
throw new RuntimeException("Await failed");
}
Thread.sleep(50);
}
}
@AllArgsConstructor
private class QueueSize implements Callable<Boolean> {
private final Queue queue;
private final int size;
@Override
public Boolean call() throws Exception {
return queue.size() == size;
}
}
}