Ehcache implementation of APi Rate limit plugin.

This commit is contained in:
Min Chen 2013-01-10 17:47:48 -08:00
parent 0b69d9449a
commit d900345a20
13 changed files with 985 additions and 0 deletions

View File

@ -0,0 +1,24 @@
# 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.
# bitmap of permissions at the end of each classname, 1 = ADMIN, 2 =
# RESOURCE_DOMAIN_ADMIN, 4 = DOMAIN_ADMIN, 8 = USER
# Please standardize naming conventions to camel-case (even for acronyms).
# CloudStack API Rate Limit service command
getApiLimit=15
resetApiLimit=1

View File

@ -0,0 +1,29 @@
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-plugin-api-limit-account-based</artifactId>
<name>Apache CloudStack Plugin - API Rate Limit</name>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-plugins</artifactId>
<version>4.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
</project>

View File

@ -0,0 +1,87 @@
// 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.cloudstack.api.command.user.ratelimit;
import java.util.ArrayList;
import java.util.List;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.PlugService;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.BaseCmd.CommandType;
import org.apache.cloudstack.api.commands.admin.ratelimit.ResetApiLimitCmd;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.ApiLimitResponse;
import org.apache.cloudstack.api.response.PhysicalNetworkResponse;
import org.apache.log4j.Logger;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.ratelimit.ApiRateLimitService;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.user.Account;
import com.cloud.user.UserContext;
import com.cloud.utils.exception.CloudRuntimeException;
@APICommand(name = "getApiLimit", responseObject=ApiLimitResponse.class, description="Get API limit count for the caller")
public class GetApiLimitCmd extends BaseListCmd {
private static final Logger s_logger = Logger.getLogger(GetApiLimitCmd.class.getName());
private static final String s_name = "getapilimitresponse";
@PlugService
ApiRateLimitService _apiLimitService;
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
Account account = UserContext.current().getCaller();
if (account != null) {
return account.getId();
}
return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked
}
@Override
public void execute(){
ApiLimitResponse response = _apiLimitService.searchApiLimit(this);
response.setResponseName(getCommandName());
this.setResponseObject(response);
}
}

View File

@ -0,0 +1,94 @@
// 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.cloudstack.api.commands.admin.ratelimit;
import org.apache.cloudstack.api.*;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.ApiLimitResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.log4j.Logger;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.ratelimit.ApiRateLimitService;
import com.cloud.user.Account;
import com.cloud.user.UserContext;
@APICommand(name = "resetApiLimit", responseObject=ApiLimitResponse.class, description="Reset api count")
public class ResetApiLimitCmd extends BaseCmd {
private static final Logger s_logger = Logger.getLogger(ResetApiLimitCmd.class.getName());
private static final String s_name = "resetapilimitresponse";
@PlugService
ApiRateLimitService _apiLimitService;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@ACL
@Parameter(name=ApiConstants.ACCOUNT, type=CommandType.UUID, entityType=AccountResponse.class,
description="the ID of the acount whose limit to be reset")
private Long accountId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
Account account = UserContext.current().getCaller();
if (account != null) {
return account.getId();
}
return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked
}
@Override
public void execute(){
boolean result = _apiLimitService.resetApiLimit(this);
if (result) {
SuccessResponse response = new SuccessResponse(getCommandName());
this.setResponseObject(response);
} else {
throw new ServerApiException(BaseCmd.INTERNAL_ERROR, "Failed to reset api limit counter");
}
}
}

View File

@ -0,0 +1,82 @@
// 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.cloudstack.api.response;
import org.apache.cloudstack.api.ApiConstants;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.BaseResponse;
public class ApiLimitResponse extends BaseResponse {
@SerializedName(ApiConstants.ACCOUNT_ID) @Param(description="the account uuid of the api remaining count")
private String accountId;
@SerializedName(ApiConstants.ACCOUNT) @Param(description="the account name of the api remaining count")
private String accountName;
@SerializedName("apiIssued") @Param(description="number of api already issued")
private int apiIssued;
@SerializedName("apiAllowed") @Param(description="currently allowed number of apis")
private int apiAllowed;
@SerializedName("expireAfter") @Param(description="seconds left to reset counters")
private long expireAfter;
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public void setApiIssued(int apiIssued) {
this.apiIssued = apiIssued;
}
public void setApiAllowed(int apiAllowed) {
this.apiAllowed = apiAllowed;
}
public void setExpireAfter(long duration) {
this.expireAfter = duration;
}
public String getAccountId() {
return accountId;
}
public String getAccountName() {
return accountName;
}
public int getApiIssued() {
return apiIssued;
}
public int getApiAllowed() {
return apiAllowed;
}
public long getExpireAfter() {
return expireAfter;
}
}

View File

@ -0,0 +1,40 @@
// 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.cloudstack.ratelimit;
import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd;
import org.apache.cloudstack.api.commands.admin.ratelimit.ResetApiLimitCmd;
import org.apache.cloudstack.api.response.ApiLimitResponse;
import org.apache.cloudstack.api.response.ListResponse;
import com.cloud.utils.component.PluggableService;
/**
* Provide API rate limit service
* @author minc
*
*/
public interface ApiRateLimitService extends PluggableService{
public ApiLimitResponse searchApiLimit(GetApiLimitCmd cmd);
public boolean resetApiLimit(ResetApiLimitCmd cmd);
public void setTimeToLive(int timeToLive);
public void setMaxAllowed(int max);
}

View File

@ -0,0 +1,172 @@
// 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.cloudstack.ratelimit;
import java.util.Map;
import javax.ejb.Local;
import javax.naming.ConfigurationException;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import org.apache.log4j.Logger;
import com.cloud.configuration.Config;
import com.cloud.configuration.dao.ConfigurationDao;
import org.apache.cloudstack.acl.APILimitChecker;
import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd;
import org.apache.cloudstack.api.commands.admin.ratelimit.ResetApiLimitCmd;
import org.apache.cloudstack.api.response.ApiLimitResponse;
import com.cloud.network.element.NetworkElement;
import com.cloud.user.Account;
import com.cloud.user.UserContext;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.component.Inject;
@Local(value = NetworkElement.class)
public class ApiRateLimitServiceImpl extends AdapterBase implements APILimitChecker, ApiRateLimitService {
private static final Logger s_logger = Logger.getLogger(ApiRateLimitServiceImpl.class);
/**
* Fixed time duration where api rate limit is set, in seconds
*/
private int timeToLive = 1;
/**
* Max number of api requests during timeToLive duration.
*/
private int maxAllowed = 30;
@Inject
ConfigurationDao _configDao;
private LimitStore _store;
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
super.configure(name, params);
// get global configured duration and max values
String duration = _configDao.getValue(Config.ApiLimitInterval.key());
if (duration != null ){
timeToLive = Integer.parseInt(duration);
}
String maxReqs = _configDao.getValue(Config.ApiLimitMax.key());
if ( maxReqs != null){
maxAllowed = Integer.parseInt(maxReqs);
}
// create limit store
EhcacheLimitStore cacheStore = new EhcacheLimitStore();
int maxElements = 10000; //TODO: what should be the proper number here?
CacheManager cm = CacheManager.create();
Cache cache = new Cache("api-limit-cache", maxElements, true, false, timeToLive, timeToLive);
cm.addCache(cache);
s_logger.info("Limit Cache created: " + cache.toString());
cacheStore.setCache(cache);
_store = cacheStore;
return true;
}
@Override
public ApiLimitResponse searchApiLimit(GetApiLimitCmd cmd) {
Account caller = UserContext.current().getCaller();
ApiLimitResponse response = new ApiLimitResponse();
response.setAccountId(caller.getUuid());
response.setAccountName(caller.getAccountName());
StoreEntry entry = _store.get(caller.getId());
if (entry == null) {
/* Populate the entry, thus unlocking any underlying mutex */
entry = _store.create(caller.getId(), timeToLive);
response.setApiIssued(0);
response.setApiAllowed(maxAllowed);
response.setExpireAfter(timeToLive);
}
else{
response.setApiIssued(entry.getCounter());
response.setApiAllowed(maxAllowed - entry.getCounter());
response.setExpireAfter(entry.getExpireDuration());
}
return response;
}
@Override
public boolean resetApiLimit(ResetApiLimitCmd cmd) {
if ( cmd.getAccountId() != null ){
_store.create(cmd.getAccountId(), timeToLive);
}
else{
_store.resetCounters();
}
return true;
}
@Override
public boolean isUnderLimit(Account account) {
Long accountId = account.getId();
StoreEntry entry = _store.get(accountId);
if (entry == null) {
/* Populate the entry, thus unlocking any underlying mutex */
entry = _store.create(accountId, timeToLive);
}
/* Increment the client count and see whether we have hit the maximum allowed clients yet. */
int current = entry.incrementAndGet();
if (current <= maxAllowed) {
return true;
} else {
return false;
}
}
@Override
public String[] getPropertiesFiles() {
return new String[] { "api-limit_commands.properties" };
}
@Override
public void setTimeToLive(int timeToLive) {
this.timeToLive = timeToLive;
}
@Override
public void setMaxAllowed(int max) {
this.maxAllowed = max;
}
}

View File

@ -0,0 +1,99 @@
// 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.cloudstack.ratelimit;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.constructs.blocking.BlockingCache;
import net.sf.ehcache.constructs.blocking.LockTimeoutException;
/**
* A Limit store implementation using Ehcache.
* @author minc
*
*/
public class EhcacheLimitStore implements LimitStore {
private BlockingCache cache;
public void setCache(Ehcache cache) {
BlockingCache ref;
if (!(cache instanceof BlockingCache)) {
ref = new BlockingCache(cache);
cache.getCacheManager().replaceCacheWithDecoratedCache(cache, new BlockingCache(cache));
} else {
ref = (BlockingCache) cache;
}
this.cache = ref;
}
@Override
public StoreEntry create(Long key, int timeToLive) {
StoreEntryImpl result = new StoreEntryImpl(timeToLive);
Element element = new Element(key, result);
element.setTimeToLive(timeToLive);
cache.put(element);
return result;
}
@Override
public StoreEntry get(Long key) {
Element entry = null;
try {
/* This may block. */
entry = cache.get(key);
} catch (LockTimeoutException e) {
throw new RuntimeException();
} catch (RuntimeException e) {
/* Release the lock that may have been acquired. */
cache.put(new Element(key, null));
}
StoreEntry result = null;
if (entry != null) {
/*
* We don't need to check isExpired() on the result, since ehcache takes care of expiring entries for us.
* c.f. the get(Key) implementation in this class.
*/
result = (StoreEntry) entry.getObjectValue();
}
return result;
}
@Override
public void resetCounters() {
cache.removeAll();
}
}

View File

@ -0,0 +1,51 @@
// 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.cloudstack.ratelimit;
import com.cloud.user.Account;
/**
* Interface to define how an api limit store should work.
* @author minc
*
*/
public interface LimitStore {
/**
* Returns a store entry for the given account. A value of null means that there is no
* such entry and the calling client must call create to avoid
* other clients potentially being blocked without any hope of progressing. A non-null
* entry means that it has not expired and can be used to determine whether the current client should be allowed to
* proceed with the rate-limited action or not.
*
*/
StoreEntry get(Long account);
/**
* Creates a new store entry
*
* @param account
* the user account, key to the store
* @param timeToLiveInSecs
* the positive time-to-live in seconds
* @return a non-null entry
*/
StoreEntry create(Long account, int timeToLiveInSecs);
void resetCounters();
}

View File

@ -0,0 +1,33 @@
// 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.cloudstack.ratelimit;
/**
* Interface for each entry in LimitStore.
* @author minc
*
*/
public interface StoreEntry {
int getCounter();
int incrementAndGet();
boolean isExpired();
long getExpireDuration(); /* seconds to reset counter */
}

View File

@ -0,0 +1,64 @@
// 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.cloudstack.ratelimit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Implementation of limit store entry.
* @author minc
*
*/
public class StoreEntryImpl implements StoreEntry {
private final long expiry;
private final AtomicInteger counter;
StoreEntryImpl(int timeToLive) {
this.expiry = System.currentTimeMillis() + timeToLive * 1000;
this.counter = new AtomicInteger(0);
}
@Override
public boolean isExpired() {
return System.currentTimeMillis() > expiry;
}
@Override
public long getExpireDuration() {
if ( isExpired() )
return 0; // already expired
else {
return (expiry - System.currentTimeMillis()) * 1000;
}
}
@Override
public int incrementAndGet() {
return this.counter.incrementAndGet();
}
@Override
public int getCounter(){
return this.counter.get();
}
}

View File

@ -0,0 +1,209 @@
// 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.cloudstack.ratelimit;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd;
import org.apache.cloudstack.api.commands.admin.ratelimit.ResetApiLimitCmd;
import org.apache.cloudstack.api.response.ApiLimitResponse;
import org.apache.cloudstack.ratelimit.ApiRateLimitServiceImpl;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import com.cloud.configuration.Config;
import com.cloud.configuration.dao.ConfigurationDao;
import com.cloud.user.Account;
import com.cloud.user.AccountVO;
import com.cloud.user.UserContext;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class ApiRateLimitTest {
static ApiRateLimitServiceImpl _limitService = new ApiRateLimitServiceImpl();
static ConfigurationDao _configDao = mock(ConfigurationDao.class);
private static long acctIdSeq = 0L;
@BeforeClass
public static void setUp() throws ConfigurationException {
_limitService._configDao = _configDao;
// No global configuration set, will set in each test case
when(_configDao.getValue(Config.ApiLimitInterval.key())).thenReturn(null);
when(_configDao.getValue(Config.ApiLimitMax.key())).thenReturn(null);
_limitService.configure("ApiRateLimitTest", Collections.<String, Object> emptyMap());
}
private Account createFakeAccount(){
return new AccountVO(acctIdSeq++);
}
@Test
public void sequentialApiAccess() {
int allowedRequests = 1;
_limitService.setMaxAllowed(allowedRequests);
_limitService.setTimeToLive(1);
Account key = createFakeAccount();
assertTrue("Allow for the first request", _limitService.isUnderLimit(key));
assertFalse("Second request should be blocked, since we assume that the two api "
+ " accesses take less than a second to perform", _limitService.isUnderLimit(key));
}
@Test
public void canDoReasonableNumberOfApiAccessPerSecond() throws Exception {
int allowedRequests = 50000;
_limitService.setMaxAllowed(allowedRequests);
_limitService.setTimeToLive(1);
Account key = createFakeAccount();
for (int i = 0; i < allowedRequests; i++) {
assertTrue("We should allow " + allowedRequests + " requests per second", _limitService.isUnderLimit(key));
}
assertFalse("We should block >" + allowedRequests + " requests per second", _limitService.isUnderLimit(key));
}
@Test
public void multipleClientsCanAccessWithoutBlocking() throws Exception {
int allowedRequests = 200;
_limitService.setMaxAllowed(allowedRequests);
_limitService.setTimeToLive(1);
final Account key = createFakeAccount();
int clientCount = allowedRequests;
Runnable[] clients = new Runnable[clientCount];
final boolean[] isUsable = new boolean[clientCount];
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(clientCount);
for (int i = 0; i < isUsable.length; ++i) {
final int j = i;
clients[j] = new Runnable() {
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
startGate.await();
isUsable[j] = _limitService.isUnderLimit(key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
}
};
}
ExecutorService executor = Executors.newFixedThreadPool(clientCount);
for (Runnable runnable : clients) {
executor.execute(runnable);
}
startGate.countDown();
endGate.await();
for (boolean b : isUsable) {
assertTrue("Concurrent client request should be allowed within limit", b);
}
}
@Test
public void expiryOfCounterIsSupported() throws Exception {
int allowedRequests = 1;
_limitService.setMaxAllowed(allowedRequests);
_limitService.setTimeToLive(1);
Account key = this.createFakeAccount();
assertTrue("The first request should be allowed", _limitService.isUnderLimit(key));
// Allow the token to expire
Thread.sleep(1001);
assertTrue("Another request after interval should be allowed as well", _limitService.isUnderLimit(key));
}
@Test
public void verifyResetCounters() throws Exception {
int allowedRequests = 1;
_limitService.setMaxAllowed(allowedRequests);
_limitService.setTimeToLive(1);
Account key = this.createFakeAccount();
assertTrue("The first request should be allowed", _limitService.isUnderLimit(key));
assertFalse("Another request should be blocked", _limitService.isUnderLimit(key));
ResetApiLimitCmd cmd = new ResetApiLimitCmd();
cmd.setAccountId(key.getId());
_limitService.resetApiLimit(cmd);
assertTrue("Another request should be allowed after reset counter", _limitService.isUnderLimit(key));
}
/* Disable this since I cannot mock Static method UserContext.current()
@Test
public void verifySearchCounter() throws Exception {
int allowedRequests = 10;
_limitService.setMaxAllowed(allowedRequests);
_limitService.setTimeToLive(1);
Account key = this.createFakeAccount();
for ( int i = 0; i < 5; i++ ){
assertTrue("Issued 5 requests", _limitService.isUnderLimit(key));
}
GetApiLimitCmd cmd = new GetApiLimitCmd();
UserContext ctx = mock(UserContext.class);
when(UserContext.current().getCaller()).thenReturn(key);
ApiLimitResponse response = _limitService.searchApiLimit(cmd);
assertEquals("apiIssued is incorrect", 5, response.getApiIssued());
assertEquals("apiAllowed is incorrect", 5, response.getApiAllowed());
assertTrue("expiredAfter is incorrect", response.getExpireAfter() < 1);
}
*/
}

View File

@ -32,6 +32,7 @@
<testSourceDirectory>test</testSourceDirectory>
</build>
<modules>
<module>api/rate-limit</module>
<module>api/discovery</module>
<module>acl/static-role-based</module>
<module>deployment-planners/user-concentrated-pod</module>