mirror of
https://github.com/apache/cloudstack.git
synced 2025-12-18 11:34:23 +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>
|
<testSourceDirectory>test</testSourceDirectory>
|
||||||
</build>
|
</build>
|
||||||
<modules>
|
<modules>
|
||||||
|
<module>api/rate-limit</module>
|
||||||
<module>api/discovery</module>
|
<module>api/discovery</module>
|
||||||
<module>acl/static-role-based</module>
|
<module>acl/static-role-based</module>
|
||||||
<module>deployment-planners/user-concentrated-pod</module>
|
<module>deployment-planners/user-concentrated-pod</module>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user