You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ml...@apache.org on 2013/01/18 23:23:45 UTC
[5/50] [abbrv] git commit: Ehcache implementation of APi Rate limit
plugin.
Ehcache implementation of APi Rate limit plugin.
Project: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/commit/d900345a
Tree: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/tree/d900345a
Diff: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/diff/d900345a
Branch: refs/heads/add_remove_nics
Commit: d900345a20acab8cc7f6759425b27d1b28d935f7
Parents: 0b69d94
Author: Min Chen <mi...@citrix.com>
Authored: Thu Jan 10 17:47:48 2013 -0800
Committer: Min Chen <mi...@citrix.com>
Committed: Thu Jan 10 17:47:48 2013 -0800
----------------------------------------------------------------------
client/tomcatconf/api-limit_commands.properties.in | 24 ++
plugins/api/rate-limit/pom.xml | 29 ++
.../api/command/user/ratelimit/GetApiLimitCmd.java | 87 ++++++
.../commands/admin/ratelimit/ResetApiLimitCmd.java | 94 +++++++
.../cloudstack/api/response/ApiLimitResponse.java | 82 ++++++
.../cloudstack/ratelimit/ApiRateLimitService.java | 40 +++
.../ratelimit/ApiRateLimitServiceImpl.java | 172 ++++++++++++
.../cloudstack/ratelimit/EhcacheLimitStore.java | 99 +++++++
.../apache/cloudstack/ratelimit/LimitStore.java | 51 ++++
.../apache/cloudstack/ratelimit/StoreEntry.java | 33 +++
.../cloudstack/ratelimit/StoreEntryImpl.java | 64 +++++
.../cloudstack/ratelimit/ApiRateLimitTest.java | 209 +++++++++++++++
plugins/pom.xml | 1 +
13 files changed, 985 insertions(+), 0 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/client/tomcatconf/api-limit_commands.properties.in
----------------------------------------------------------------------
diff --git a/client/tomcatconf/api-limit_commands.properties.in b/client/tomcatconf/api-limit_commands.properties.in
new file mode 100644
index 0000000..fcb963a
--- /dev/null
+++ b/client/tomcatconf/api-limit_commands.properties.in
@@ -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
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/pom.xml
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/pom.xml b/plugins/api/rate-limit/pom.xml
new file mode 100644
index 0000000..416c901
--- /dev/null
+++ b/plugins/api/rate-limit/pom.xml
@@ -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>
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/user/ratelimit/GetApiLimitCmd.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/user/ratelimit/GetApiLimitCmd.java b/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/user/ratelimit/GetApiLimitCmd.java
new file mode 100644
index 0000000..3f9e4eb
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/user/ratelimit/GetApiLimitCmd.java
@@ -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);
+ }
+}
+
+
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/api/commands/admin/ratelimit/ResetApiLimitCmd.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/api/commands/admin/ratelimit/ResetApiLimitCmd.java b/plugins/api/rate-limit/src/org/apache/cloudstack/api/commands/admin/ratelimit/ResetApiLimitCmd.java
new file mode 100644
index 0000000..8029ab3
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/api/commands/admin/ratelimit/ResetApiLimitCmd.java
@@ -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");
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/api/response/ApiLimitResponse.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/api/response/ApiLimitResponse.java b/plugins/api/rate-limit/src/org/apache/cloudstack/api/response/ApiLimitResponse.java
new file mode 100644
index 0000000..245e8f1
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/api/response/ApiLimitResponse.java
@@ -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;
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java
new file mode 100644
index 0000000..e7ba9d4
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java
@@ -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);
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java
new file mode 100644
index 0000000..e14f65d
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java
@@ -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;
+
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/EhcacheLimitStore.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/EhcacheLimitStore.java b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/EhcacheLimitStore.java
new file mode 100644
index 0000000..659cf81
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/EhcacheLimitStore.java
@@ -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();
+
+ }
+
+
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/LimitStore.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/LimitStore.java b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/LimitStore.java
new file mode 100644
index 0000000..a5e086b
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/LimitStore.java
@@ -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();
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntry.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntry.java b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntry.java
new file mode 100644
index 0000000..76e8a2d
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntry.java
@@ -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 */
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntryImpl.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntryImpl.java b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntryImpl.java
new file mode 100644
index 0000000..40965d9
--- /dev/null
+++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/StoreEntryImpl.java
@@ -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();
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java
----------------------------------------------------------------------
diff --git a/plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java b/plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java
new file mode 100644
index 0000000..0e2080a
--- /dev/null
+++ b/plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java
@@ -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);
+
+ }
+ */
+}
http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/d900345a/plugins/pom.xml
----------------------------------------------------------------------
diff --git a/plugins/pom.xml b/plugins/pom.xml
index a42ae29..7bb60a9 100644
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -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>