mirror of
https://github.com/apache/cloudstack.git
synced 2025-12-16 02:22:52 +01:00
Ehcache implementation of APi Rate limit plugin.
This commit is contained in:
parent
0b69d9449a
commit
d900345a20
24
client/tomcatconf/api-limit_commands.properties.in
Normal file
24
client/tomcatconf/api-limit_commands.properties.in
Normal 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
|
||||
29
plugins/api/rate-limit/pom.xml
Normal file
29
plugins/api/rate-limit/pom.xml
Normal 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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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();
|
||||
|
||||
}
|
||||
@ -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 */
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user