incubator-cloudstack-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mc...@apache.org
Subject git commit: Ehcache implementation of APi Rate limit plugin.
Date Fri, 11 Jan 2013 01:48:36 GMT
Updated Branches:
  refs/heads/api_limit 0b69d9449 -> d900345a2


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/api_limit
Commit: d900345a20acab8cc7f6759425b27d1b28d935f7
Parents: 0b69d94
Author: Min Chen <min.chen@citrix.com>
Authored: Thu Jan 10 17:47:48 2013 -0800
Committer: Min Chen <min.chen@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>


Mime
View raw message