Reuse APIChecker adapter interface for APi Rate limit checking and optimize ApiRateLimitService interface.
Signed-off-by: Min Chen <min.c...@citrix.com> Project: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/commit/4355d06a Tree: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/tree/4355d06a Diff: http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/diff/4355d06a Branch: refs/heads/add_remove_nics Commit: 4355d06a869ce1a745bfc483f7bd0fc246cdf2c5 Parents: ec3dd71 7f1486e Author: Min Chen <min.c...@citrix.com> Authored: Tue Jan 15 15:53:19 2013 -0800 Committer: Min Chen <min.c...@citrix.com> Committed: Tue Jan 15 15:53:19 2013 -0800 ---------------------------------------------------------------------- .../com/cloud/exception/RequestLimitException.java | 43 ++++ api/src/com/cloud/network/NetworkService.java | 2 + api/src/com/cloud/user/DomainService.java | 2 + api/src/org/apache/cloudstack/acl/APIChecker.java | 11 +- .../apache/cloudstack/api/ResponseGenerator.java | 7 - .../api/command/user/vm/DeployVMCmd.java | 17 ++- .../api/command/user/volume/ExtractVolumeCmd.java | 2 +- client/bindir/cloud-setup-management.in | 10 +- client/tomcatconf/api-limit_commands.properties.in | 24 --- .../cisconexusvsm_commands.properties.in | 25 --- client/tomcatconf/commands-ext.properties.in | 28 --- client/tomcatconf/commands.properties.in | 89 ++++++++ client/tomcatconf/components.xml.in | 8 +- client/tomcatconf/f5bigip_commands.properties.in | 32 --- .../tomcatconf/junipersrx_commands.properties.in | 30 --- client/tomcatconf/netapp_commands.properties.in | 33 --- .../netscalerloadbalancer_commands.properties.in | 26 --- .../tomcatconf/nicira-nvp_commands.properties.in | 29 --- client/tomcatconf/simulator_commands.properties.in | 19 -- .../virtualrouter_commands.properties.in | 24 --- .../acl/StaticRoleBasedAPIAccessChecker.java | 52 +++--- .../api/command/user/discovery/ListApisCmd.java | 10 +- .../cloudstack/discovery/ApiDiscoveryService.java | 4 +- .../discovery/ApiDiscoveryServiceImpl.java | 116 +++++------ .../command/admin/ratelimit/ResetApiLimitCmd.java | 2 +- .../api/command/user/ratelimit/GetApiLimitCmd.java | 3 +- .../cloudstack/ratelimit/ApiRateLimitService.java | 9 +- .../ratelimit/ApiRateLimitServiceImpl.java | 65 ++++--- .../cloudstack/ratelimit/ApiRateLimitTest.java | 72 ++++--- .../hypervisor/kvm/resource/KVMGuestOsMapper.java | 2 + .../kvm/resource/LibvirtComputingResource.java | 5 +- .../server/ManagementServerSimulatorImpl.java | 11 +- .../network/element/CiscoNexusVSMElement.java | 11 +- .../xen/resource/CitrixResourceBase.java | 6 +- .../element/F5ExternalLoadBalancerElement.java | 15 +- .../element/JuniperSRXExternalFirewallElement.java | 14 +- .../network/element/MidokuraMidonetElement.java | 9 +- .../cloud/network/element/NetscalerElement.java | 12 +- .../cloud/network/element/NiciraNvpElement.java | 10 +- pom.xml | 2 + python/lib/cloudutils/utilities.py | 6 +- server/src/com/cloud/api/ApiDispatcher.java | 6 - server/src/com/cloud/api/ApiResponseHelper.java | 5 - server/src/com/cloud/api/ApiServer.java | 90 +++------ server/src/com/cloud/api/ApiServlet.java | 45 ++--- server/src/com/cloud/configuration/Config.java | 5 +- .../configuration/ConfigurationManagerImpl.java | 54 +++--- .../consoleproxy/ConsoleProxyManagerImpl.java | 35 ++-- .../com/cloud/hypervisor/HypervisorGuruBase.java | 1 + .../src/com/cloud/network/NetworkManagerImpl.java | 6 + .../network/element/VirtualRouterElement.java | 10 +- .../router/VirtualNetworkApplianceManagerImpl.java | 4 +- .../com/cloud/server/ManagementServerExtImpl.java | 6 +- .../src/com/cloud/server/ManagementServerImpl.java | 7 +- server/src/com/cloud/user/DomainManagerImpl.java | 5 + server/test/com/cloud/api/ListPerfTest.java | 22 ++- .../com/cloud/network/MockNetworkManagerImpl.java | 6 + .../test/com/cloud/user/MockDomainManagerImpl.java | 6 + .../test/com/cloud/vpc/MockNetworkManagerImpl.java | 6 + setup/db/db/schema-40to410.sql | 2 - setup/db/templates.kvm.sql | 11 +- setup/db/templates.sql | 7 + tools/apidoc/gen_toc.py | 1 + tools/apidoc/pom.xml | 2 +- tools/devcloud-kvm/devcloud-kvm-advanced.cfg | 115 +++++++++++ tools/devcloud-kvm/devcloud-kvm.py | 157 +++++++++++++++ tools/devcloud-kvm/kvm.properties | 62 ++++++ tools/devcloud/devcloud-advanced.cfg | 118 +++++++++++ tools/devcloud/devcloud-advanced_internal-mgt.cfg | 124 ++++++++++++ tools/marvin/marvin/configGenerator.py | 3 +- tools/marvin/marvin/deployDataCenter.py | 10 +- tools/marvin/marvin/integration/lib/base.py | 3 + .../marvin/marvin/sandbox/advanced/advanced_env.py | 18 ++- .../marvin/sandbox/advanced/setup.properties | 16 +- ui/scripts/network.js | 10 +- ui/scripts/sharedFunctions.js | 3 +- .../cloud/utils/component/PluggableService.java | 3 +- utils/src/com/cloud/utils/db/GenericDao.java | 2 +- utils/src/com/cloud/utils/db/GenericDaoBase.java | 2 +- .../utils/exception/CSExceptionErrorCode.java | 1 + 80 files changed, 1209 insertions(+), 677 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/api/src/com/cloud/exception/RequestLimitException.java ---------------------------------------------------------------------- diff --cc api/src/com/cloud/exception/RequestLimitException.java index 0000000,0000000..0142f8e new file mode 100644 --- /dev/null +++ b/api/src/com/cloud/exception/RequestLimitException.java @@@ -1,0 -1,0 +1,43 @@@ ++// 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 com.cloud.exception; ++ ++import com.cloud.utils.SerialVersionUID; ++import com.cloud.utils.exception.CloudRuntimeException; ++ ++/** ++ * Exception thrown if number of requests is over api rate limit set. ++ * @author minc ++ * ++ */ ++public class RequestLimitException extends CloudRuntimeException { ++ ++ private static final long serialVersionUID = SerialVersionUID.AccountLimitException; ++ ++ protected RequestLimitException() { ++ super(); ++ } ++ ++ public RequestLimitException(String msg) { ++ super(msg); ++ } ++ ++ public RequestLimitException(String msg, Throwable cause) { ++ super(msg, cause); ++ } ++ ++} http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/api/src/org/apache/cloudstack/acl/APIChecker.java ---------------------------------------------------------------------- diff --cc api/src/org/apache/cloudstack/acl/APIChecker.java index 61dd7de,0d0dfd1..2e2b73b --- a/api/src/org/apache/cloudstack/acl/APIChecker.java +++ b/api/src/org/apache/cloudstack/acl/APIChecker.java @@@ -16,13 -16,15 +16,16 @@@ // under the License. package org.apache.cloudstack.acl; - import org.apache.cloudstack.acl.RoleType; + import com.cloud.exception.PermissionDeniedException; ++import com.cloud.exception.RequestLimitException; + import com.cloud.user.User; import com.cloud.utils.component.Adapter; // APIChecker checks the ownership and access control to API requests public interface APIChecker extends Adapter { // Interface for checking access for a role using apiname - boolean checkAccess(RoleType roleType, String apiCommandName); - // Interface for checking existence of an api by name - boolean checkExistence(String apiCommandName); + // If true, apiChecker has checked the operation + // If false, apiChecker is unable to handle the operation or not implemented + // On exception, checkAccess failed don't allow - boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException; ++ boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException, RequestLimitException; } http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/client/tomcatconf/commands.properties.in ---------------------------------------------------------------------- diff --cc client/tomcatconf/commands.properties.in index 3872f2a,99cb874..db722ad --- a/client/tomcatconf/commands.properties.in +++ b/client/tomcatconf/commands.properties.in @@@ -424,3 -424,87 +424,92 @@@ resetVpnConnection=1 listVpnCustomerGateways=15 listVpnGateways=15 listVpnConnections=15 + + #### router commands + createVirtualRouterElement=7 + configureVirtualRouterElement=7 + listVirtualRouterElements=7 + + #### usage commands + generateUsageRecords=1 + listUsageRecords=1 + listUsageTypes=1 + + #### traffic monitor commands + addTrafficMonitor=1 + deleteTrafficMonitor=1 + listTrafficMonitors=1 + + #### Cisco Nexus 1000v Virtual Supervisor Module (VSM) commands + deleteCiscoNexusVSM=1 + enableCiscoNexusVSM=1 + disableCiscoNexusVSM=1 + listCiscoNexusVSMs=1 + + #### f5 big ip load balancer commands + + #Deprecated commands + addExternalLoadBalancer=1 + deleteExternalLoadBalancer=1 + listExternalLoadBalancers=1 + + addF5LoadBalancer=1 + configureF5LoadBalancer=1 + deleteF5LoadBalancer=1 + listF5LoadBalancers=1 + listF5LoadBalancerNetworks=1 + + #### juniper srx firewall commands + addExternalFirewall=1 + deleteExternalFirewall=1 + listExternalFirewalls=1 + + addSrxFirewall=1 + deleteSrxFirewall=1 + configureSrxFirewall=1 + listSrxFirewalls=1 + listSrxFirewallNetworks=1 + + ####Netapp integration commands + createVolumeOnFiler=15 + destroyVolumeOnFiler=15 + listVolumesOnFiler=15 + createLunOnFiler=15 + destroyLunOnFiler=15 + listLunsOnFiler=15 + associateLun=15 + dissociateLun=15 + createPool=15 + deletePool=15 + modifyPool=15 + listPools=15 + + #### netscaler load balancer commands + addNetscalerLoadBalancer=1 + deleteNetscalerLoadBalancer=1 + configureNetscalerLoadBalancer=1 + listNetscalerLoadBalancers=1 + listNetscalerLoadBalancerNetworks=1 + + #### nicira nvp commands + + addNiciraNvpDevice=1 + deleteNiciraNvpDevice=1 + listNiciraNvpDevices=1 + listNiciraNvpDeviceNetworks=1 + + # Not implemented (yet) + #configureNiciraNvpDevice=1 + + #### host simulator commands + + configureSimulator=1 + + #### api discovery commands + + listApis=15 ++ ++#### API Rate Limit service command ++ ++getApiLimit=15 ++resetApiLimit=1 http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/client/tomcatconf/components.xml.in ---------------------------------------------------------------------- diff --cc client/tomcatconf/components.xml.in index 630bd97,bb39839..e19b418 --- a/client/tomcatconf/components.xml.in +++ b/client/tomcatconf/components.xml.in @@@ -54,15 -54,8 +54,13 @@@ under the License <param name="premium">true</param> </dao> <adapters key="org.apache.cloudstack.acl.APIChecker"> - <adapter name="StaticRoleBasedAPIAccessChecker" class="org.apache.cloudstack.acl.StaticRoleBasedAPIAccessChecker"/> - </adapters> - <adapters key="org.apache.cloudstack.acl.APILimitChecker"> + <adapter name="AccountBasedAPIRateLimit" class="org.apache.cloudstack.ratelimit.ApiRateLimitServiceImpl" singleton="true"> + <param name="api.throttling.interval">1</param> + <param name="api.throttling.max">25</param> + <param name="api.throttling.cachesize">50000</param> - </adapter> - </adapters> ++ </adapter> + <adapter name="StaticRoleBasedAPIAccessChecker" class="org.apache.cloudstack.acl.StaticRoleBasedAPIAccessChecker"/> + </adapters> <adapters key="com.cloud.agent.manager.allocator.HostAllocator"> <adapter name="FirstFitRouting" class="com.cloud.agent.manager.allocator.impl.FirstFitRoutingAllocator"/> <!--adapter name="FirstFitRouting" class="com.cloud.agent.manager.allocator.impl.RecreateHostAllocator"/--> http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/admin/ratelimit/ResetApiLimitCmd.java ---------------------------------------------------------------------- diff --cc plugins/api/rate-limit/src/org/apache/cloudstack/api/command/admin/ratelimit/ResetApiLimitCmd.java index 3c612fa,0000000..771b63a mode 100644,000000..100644 --- a/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/admin/ratelimit/ResetApiLimitCmd.java +++ b/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/admin/ratelimit/ResetApiLimitCmd.java @@@ -1,94 -1,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.command.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); ++ boolean result = _apiLimitService.resetApiLimit(this.accountId); + 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/4355d06a/plugins/api/rate-limit/src/org/apache/cloudstack/api/command/user/ratelimit/GetApiLimitCmd.java ---------------------------------------------------------------------- diff --cc plugins/api/rate-limit/src/org/apache/cloudstack/api/command/user/ratelimit/GetApiLimitCmd.java index 0397fa8,0000000..ad1fb28 mode 100644,000000..100644 --- 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 @@@ -1,87 -1,0 +1,88 @@@ +// 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.command.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); ++ Account caller = UserContext.current().getCaller(); ++ ApiLimitResponse response = _apiLimitService.searchApiLimit(caller); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } +} + + http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java ---------------------------------------------------------------------- diff --cc plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java index 8c9d49b,0000000..c5b7150 mode 100644,000000..100644 --- a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java +++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitService.java @@@ -1,40 -1,0 +1,37 @@@ +// 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.admin.ratelimit.ResetApiLimitCmd; - import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd; +import org.apache.cloudstack.api.response.ApiLimitResponse; - import org.apache.cloudstack.api.response.ListResponse; - ++import com.cloud.user.Account; +import com.cloud.utils.component.PluggableService; + +/** + * Provide API rate limit service + * @author minc + * + */ +public interface ApiRateLimitService extends PluggableService{ + - public ApiLimitResponse searchApiLimit(GetApiLimitCmd cmd); ++ public ApiLimitResponse searchApiLimit(Account caller); + - public boolean resetApiLimit(ResetApiLimitCmd cmd); ++ public boolean resetApiLimit(Long accountId); + + public void setTimeToLive(int timeToLive); + + public void setMaxAllowed(int max); +} http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java ---------------------------------------------------------------------- diff --cc plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java index 00f39af,0000000..1e9b9ad mode 100644,000000..100644 --- a/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java +++ b/plugins/api/rate-limit/src/org/apache/cloudstack/ratelimit/ApiRateLimitServiceImpl.java @@@ -1,181 -1,0 +1,196 @@@ +// 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.ArrayList; ++import java.util.List; +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 org.apache.cloudstack.acl.APILimitChecker; - import org.apache.cloudstack.api.BaseCmd; - import org.apache.cloudstack.api.ServerApiException; ++import org.apache.cloudstack.acl.APIChecker; +import org.apache.cloudstack.api.command.admin.ratelimit.ResetApiLimitCmd; +import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd; +import org.apache.cloudstack.api.response.ApiLimitResponse; ++ ++import com.cloud.exception.PermissionDeniedException; ++import com.cloud.exception.RequestLimitException; +import com.cloud.user.Account; - import com.cloud.user.UserContext; - import com.cloud.utils.PropertiesUtil; ++import com.cloud.user.AccountService; ++import com.cloud.user.User; +import com.cloud.utils.component.AdapterBase; ++import com.cloud.utils.component.Inject; + - @Local(value = APILimitChecker.class) - public class ApiRateLimitServiceImpl extends AdapterBase implements APILimitChecker, ApiRateLimitService { ++@Local(value = APIChecker.class) ++public class ApiRateLimitServiceImpl extends AdapterBase implements APIChecker, 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; + + private LimitStore _store = null; + ++ @Inject ++ AccountService _accountService; ++ ++ + + @Override + public boolean configure(String name, Map<String, Object> params) throws ConfigurationException { + super.configure(name, params); + + if (_store == null) { + // not configured yet, note that since this class is both adapter + // and pluggableService, so this method + // may be invoked twice in ComponentLocator. + // get global configured duration and max values + Object duration = params.get("api.throttling.interval"); + if (duration != null) { + timeToLive = Integer.parseInt((String) duration); + } + Object maxReqs = params.get("api.throttling.max"); + if (maxReqs != null) { + maxAllowed = Integer.parseInt((String) maxReqs); + } + // create limit store + EhcacheLimitStore cacheStore = new EhcacheLimitStore(); + int maxElements = 10000; + Object cachesize = params.get("api.throttling.cachesize"); + if ( cachesize != null ){ + maxElements = Integer.parseInt((String)cachesize); + } + CacheManager cm = CacheManager.create(); + Cache cache = new Cache("api-limit-cache", maxElements, false, false, timeToLive, timeToLive); + cm.addCache(cache); - s_logger.info("Limit Cache created: " + cache.toString()); ++ s_logger.info("Limit Cache created with timeToLive=" + timeToLive + ", maxAllowed=" + maxAllowed + ", maxElements=" + maxElements ); + cacheStore.setCache(cache); + _store = cacheStore; ++ + } + + return true; + + } + + + + @Override - public ApiLimitResponse searchApiLimit(GetApiLimitCmd cmd) { - Account caller = UserContext.current().getCaller(); ++ public ApiLimitResponse searchApiLimit(Account caller) { + 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); ++ public boolean resetApiLimit(Long accountId) { ++ if ( accountId != null ){ ++ _store.create(accountId, timeToLive); + } + else{ + _store.resetCounters(); + } + return true; + } + + - @Override - public void checkLimit(Account account) throws ServerApiException { + - Long accountId = account.getId(); ++ @Override ++ public boolean checkAccess(User user, String apiCommandName) throws PermissionDeniedException, RequestLimitException { ++ Long accountId = user.getAccountId(); ++ Account account = _accountService.getAccount(accountId); ++ if ( _accountService.isRootAdmin(account.getType())){ ++ // no API throttling on root admin ++ return true; ++ } + 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; ++ s_logger.info("current count = " + current); ++ return true; + } else { + long expireAfter = entry.getExpireDuration(); - s_logger.warn("The given user has reached his/her account api limit, please retry after " + expireAfter + " ms."); - throw new ServerApiException(BaseCmd.API_LIMIT_EXCEED, "The given user has reached his/her account api limit, please retry after " + - expireAfter + " ms."); ++ // for this exception, we can just show the same message to user and admin users. ++ String msg = "The given user has reached his/her account api limit, please retry after " + expireAfter + " ms."; ++ s_logger.warn(msg); ++ throw new RequestLimitException(msg); + } + } + + - + @Override - public Map<String, String> getProperties() { - return PropertiesUtil.processConfigFile(new String[] - { "api-limit_commands.properties" }); ++ public List<Class<?>> getCommands() { ++ List<Class<?>> cmdList = new ArrayList<Class<?>>(); ++ cmdList.add(ResetApiLimitCmd.class); ++ cmdList.add(GetApiLimitCmd.class); ++ return cmdList; + } + + - + @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/4355d06a/plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java ---------------------------------------------------------------------- diff --cc plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java index ef3cf6d,0000000..850182d mode 100644,000000..100644 --- a/plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java +++ b/plugins/api/rate-limit/test/org/apache/cloudstack/ratelimit/ApiRateLimitTest.java @@@ -1,214 -1,0 +1,226 @@@ +// 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.ServerApiException; - import org.apache.cloudstack.api.command.admin.ratelimit.ResetApiLimitCmd; - import org.apache.cloudstack.api.command.user.ratelimit.GetApiLimitCmd; +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.exception.RequestLimitException; +import com.cloud.user.Account; ++import com.cloud.user.AccountService; +import com.cloud.user.AccountVO; - import com.cloud.user.UserContext; ++import com.cloud.user.User; ++import com.cloud.user.UserVO; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class ApiRateLimitTest { + + static ApiRateLimitServiceImpl _limitService = new ApiRateLimitServiceImpl(); - private static long acctIdSeq = 0L; ++ static AccountService _accountService = mock(AccountService.class); ++ private static long acctIdSeq = 5L; ++ private static Account testAccount; + + @BeforeClass + public static void setUp() throws ConfigurationException { + + _limitService.configure("ApiRateLimitTest", Collections.<String, Object> emptyMap()); ++ ++ _limitService._accountService = _accountService; ++ ++ // Standard responses ++ AccountVO acct = new AccountVO(acctIdSeq); ++ acct.setType(Account.ACCOUNT_TYPE_NORMAL); ++ acct.setAccountName("demo"); ++ testAccount = acct; ++ ++ when(_accountService.getAccount(5L)).thenReturn(testAccount); ++ when(_accountService.isRootAdmin(Account.ACCOUNT_TYPE_NORMAL)).thenReturn(false); + } + ++ @Before ++ public void testSetUp() { ++ // reset counter for each test ++ _limitService.resetApiLimit(null); ++ } + - private Account createFakeAccount(){ - return new AccountVO(acctIdSeq++); ++ private User createFakeUser(){ ++ UserVO user = new UserVO(); ++ user.setAccountId(acctIdSeq); ++ return user; + } + - private boolean isUnderLimit(Account key){ ++ private boolean isUnderLimit(User key){ + try{ - _limitService.checkLimit(key); ++ _limitService.checkAccess(key, null); + return true; + } - catch (ServerApiException ex){ ++ catch (RequestLimitException ex){ + return false; + } + } + + @Test + public void sequentialApiAccess() { + int allowedRequests = 1; + _limitService.setMaxAllowed(allowedRequests); + _limitService.setTimeToLive(1); + - Account key = createFakeAccount(); ++ User key = createFakeUser(); + assertTrue("Allow for the first request", isUnderLimit(key)); + + assertFalse("Second request should be blocked, since we assume that the two api " + + " accesses take less than a second to perform", isUnderLimit(key)); + } + + @Test + public void canDoReasonableNumberOfApiAccessPerSecond() throws Exception { + int allowedRequests = 50000; + _limitService.setMaxAllowed(allowedRequests); + _limitService.setTimeToLive(1); + - Account key = createFakeAccount(); ++ User key = createFakeUser(); + + for (int i = 0; i < allowedRequests; i++) { - assertTrue("We should allow " + allowedRequests + " requests per second", isUnderLimit(key)); ++ assertTrue("We should allow " + allowedRequests + " requests per second, but failed at request " + i, isUnderLimit(key)); + } + + + assertFalse("We should block >" + allowedRequests + " requests per second", isUnderLimit(key)); + } + + @Test + public void multipleClientsCanAccessWithoutBlocking() throws Exception { + int allowedRequests = 200; + _limitService.setMaxAllowed(allowedRequests); + _limitService.setTimeToLive(1); + + - final Account key = createFakeAccount(); ++ final User key = createFakeUser(); + + 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] = 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(); ++ User key = this.createFakeUser(); + + assertTrue("The first request should be allowed", isUnderLimit(key)); + + // Allow the token to expire + Thread.sleep(1001); + + assertTrue("Another request after interval should be allowed as well", isUnderLimit(key)); + } + + @Test + public void verifyResetCounters() throws Exception { + int allowedRequests = 1; + _limitService.setMaxAllowed(allowedRequests); + _limitService.setTimeToLive(1); + - Account key = this.createFakeAccount(); ++ User key = this.createFakeUser(); + + assertTrue("The first request should be allowed", isUnderLimit(key)); + + assertFalse("Another request should be blocked", isUnderLimit(key)); + - ResetApiLimitCmd cmd = new ResetApiLimitCmd(); - cmd.setAccountId(key.getId()); - - _limitService.resetApiLimit(cmd); ++ _limitService.resetApiLimit(key.getAccountId()); + + assertTrue("Another request should be allowed after reset counter", 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(); ++ User key = this.createFakeUser(); + + for ( int i = 0; i < 5; i++ ){ + assertTrue("Issued 5 requests", isUnderLimit(key)); + } + - GetApiLimitCmd cmd = new GetApiLimitCmd(); - UserContext ctx = mock(UserContext.class); - when(UserContext.current().getCaller()).thenReturn(key); - ApiLimitResponse response = _limitService.searchApiLimit(cmd); ++ ApiLimitResponse response = _limitService.searchApiLimit(testAccount); + assertEquals("apiIssued is incorrect", 5, response.getApiIssued()); + assertEquals("apiAllowed is incorrect", 5, response.getApiAllowed()); - assertTrue("expiredAfter is incorrect", response.getExpireAfter() < 1); ++ assertTrue("expiredAfter is incorrect", response.getExpireAfter() < 1000); + + } - */ ++ +} http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/server/src/com/cloud/api/ApiServer.java ---------------------------------------------------------------------- diff --cc server/src/com/cloud/api/ApiServer.java index 1d15acf,e106f03..72bed44 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@@ -115,6 -114,6 +115,7 @@@ import com.cloud.event.EventUtils import com.cloud.exception.CloudAuthenticationException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; ++import com.cloud.exception.RequestLimitException; import com.cloud.server.ManagementServer; import com.cloud.user.Account; import com.cloud.user.AccountManager; @@@ -555,15 -549,13 +553,18 @@@ public class ApiServer implements HttpR // if userId not null, that mean that user is logged in if (userId != null) { User user = ApiDBUtils.findUserById(userId); - if (apiThrottlingEnabled){ - // go through each API limit checker, throw exception inside adapter implementation so that message - // can contain some detailed information only known for each adapter implementation. - checkRequestLimit(user); ++ + try{ + checkCommandAvailable(user, commandName); } - if (!isCommandAvailable(user, commandName)) { + catch (PermissionDeniedException ex){ s_logger.debug("The given command:" + commandName + " does not exist or it is not available for user with id:" + userId); throw new ServerApiException(BaseCmd.UNSUPPORTED_ACTION_ERROR, "The given command does not exist or it is not available for user"); } ++ catch (RequestLimitException ex){ ++ s_logger.debug(ex.getMessage()); ++ throw new ServerApiException(BaseCmd.API_LIMIT_EXCEED, ex.getMessage()); ++ } return true; } else { // check against every available command to see if the command exists or not @@@ -686,11 -680,11 +689,10 @@@ s_logger.info("User signature: " + signature + " is not equaled to computed signature: " + computedSignature); } return equalSig; -- } catch (Exception ex) { -- if (ex instanceof ServerApiException && ((ServerApiException) ex).getErrorCode() == BaseCmd.UNSUPPORTED_ACTION_ERROR) { -- throw (ServerApiException) ex; -- } - s_logger.error("unable to verifty request signature", ex); - s_logger.error("unable to verify request signature", ex); ++ } catch (ServerApiException ex){ ++ throw ex; ++ } catch (Exception ex){ ++ s_logger.error("unable to verify request signature"); } return false; } @@@ -799,42 -786,14 +794,15 @@@ return true; } + - private void checkRequestLimit(User user) throws ServerApiException { - Account account = ApiDBUtils.findAccountById(user.getAccountId()); - if ( _accountMgr.isRootAdmin(account.getType()) ){ - // no api throttling for root admin - return; - } - for (APILimitChecker apiChecker : _apiLimitCheckers) { - // Fail the checking if any checker fails to verify - apiChecker.checkLimit(account); - } - } - - - private boolean doesCommandExist(String apiName) { - for (APIChecker apiChecker : _apiAccessCheckers) { - // If any checker has api info on the command, return true - if (apiChecker.checkExistence(apiName)) - return true; - } - return false; - } - - private boolean isCommandAvailable(User user, String commandName) { + private void checkCommandAvailable(User user, String commandName) throws PermissionDeniedException { if (user == null) { - return false; + throw new PermissionDeniedException("User is null for role based API access check for command" + commandName); } - Account account = _accountMgr.getAccount(user.getAccountId()); - RoleType roleType = _accountMgr.getRoleType(account); for (APIChecker apiChecker : _apiAccessCheckers) { - // Fail the checking if any checker fails to verify - if (!apiChecker.checkAccess(roleType, commandName)) - return false; + apiChecker.checkAccess(user, commandName); } - return true; } private Class<?> getCmdClass(String cmdName) { http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/server/src/com/cloud/api/ApiServlet.java ---------------------------------------------------------------------- diff --cc server/src/com/cloud/api/ApiServlet.java index 19091f2,19091f2..21373cd --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@@ -128,7 -128,7 +128,7 @@@ public class ApiServlet extends HttpSer reqStr = auditTrailSb.toString() + " " + req.getQueryString(); s_logger.debug("===START=== " + StringUtils.cleanString(reqStr)); } -- ++ try { HttpSession session = req.getSession(false); Object[] responseTypeParam = params.get("response"); @@@ -298,24 -298,24 +298,16 @@@ * params.put(BaseCmd.Properties.ACCOUNT_OBJ.getName(), new Object[] { accountObj }); } else { * params.put(BaseCmd.Properties.USER_ID.getName(), new String[] { userId }); * params.put(BaseCmd.Properties.ACCOUNT_OBJ.getName(), new Object[] { accountObj }); } } -- * ++ * * // update user context info here so that we can take information if the request is authenticated // via api * key mechanism updateUserContext(params, session != null ? session.getId() : null); */ -- auditTrailSb.insert(0, -- "(userId=" + UserContext.current().getCallerUserId() + " accountId=" + UserContext.current().getCaller().getId() + " sessionId=" + (session != null ? session.getId() : null) -- + ")"); -- -- try { -- String response = _apiServer.handleRequest(params, false, responseType, auditTrailSb); -- writeResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType); -- } catch (ServerApiException se) { -- String serializedResponseText = _apiServer.getSerializedApiError(se.getErrorCode(), se.getDescription(), params, responseType, null); -- resp.setHeader("X-Description", se.getDescription()); -- writeResponse(resp, serializedResponseText, se.getErrorCode(), responseType); -- auditTrailSb.append(" " + se.getErrorCode() + " " + se.getDescription()); -- } ++ auditTrailSb.insert(0, "(userId=" + UserContext.current().getCallerUserId() + " accountId=" ++ + UserContext.current().getCaller().getId() + " sessionId=" + (session != null ? session.getId() : null) + ")"); ++ ++ String response = _apiServer.handleRequest(params, false, responseType, auditTrailSb); ++ writeResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType); } else { if (session != null) { try { @@@ -329,17 -329,17 +321,14 @@@ writeResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType); } ++ } catch (ServerApiException se) { ++ String serializedResponseText = _apiServer.getSerializedApiError(se.getErrorCode(), se.getDescription(), params, responseType, null); ++ resp.setHeader("X-Description", se.getDescription()); ++ writeResponse(resp, serializedResponseText, se.getErrorCode(), responseType); ++ auditTrailSb.append(" " + se.getErrorCode() + " " + se.getDescription()); } catch (Exception ex) { -- if (ex instanceof ServerApiException && ((ServerApiException) ex).getErrorCode() == BaseCmd.UNSUPPORTED_ACTION_ERROR) { -- ServerApiException se = (ServerApiException) ex; -- String serializedResponseText = _apiServer.getSerializedApiError(se.getErrorCode(), se.getDescription(), params, responseType, null); -- resp.setHeader("X-Description", se.getDescription()); -- writeResponse(resp, serializedResponseText, se.getErrorCode(), responseType); -- auditTrailSb.append(" " + se.getErrorCode() + " " + se.getDescription()); -- } else { -- s_logger.error("unknown exception writing api response", ex); -- auditTrailSb.append(" unknown exception writing api response"); -- } ++ s_logger.error("unknown exception writing api response", ex); ++ auditTrailSb.append(" unknown exception writing api response"); } finally { s_accessLogger.info(auditTrailSb.toString()); if (s_logger.isDebugEnabled()) { @@@ -354,9 -354,9 +343,9 @@@ * private void updateUserContext(Map<String, Object[]> requestParameters, String sessionId) { String userIdStr = * (String)(requestParameters.get(BaseCmd.Properties.USER_ID.getName())[0]); Account accountObj = * (Account)(requestParameters.get(BaseCmd.Properties.ACCOUNT_OBJ.getName())[0]); -- * ++ * * Long userId = null; Long accountId = null; if(userIdStr != null) userId = Long.parseLong(userIdStr); -- * ++ * * if(accountObj != null) accountId = accountObj.getId(); UserContext.updateContext(userId, accountId, sessionId); } */ @@@ -386,7 -386,7 +375,7 @@@ private String getLoginSuccessResponse(HttpSession session, String responseType) { StringBuffer sb = new StringBuffer(); int inactiveInterval = session.getMaxInactiveInterval(); -- ++ String user_UUID = (String)session.getAttribute("user_UUID"); session.removeAttribute("user_UUID"); http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/server/src/com/cloud/configuration/Config.java ---------------------------------------------------------------------- diff --cc server/src/com/cloud/configuration/Config.java index e6bf3d5,b91fbdd..59abd99 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@@ -358,11 -358,9 +358,8 @@@ public enum Config DetailBatchQuerySize("Advanced", ManagementServer.class, Integer.class, "detail.batch.query.size", "2000", "Default entity detail batch query size for listing", null), ConcurrentSnapshotsThresholdPerHost("Advanced", ManagementServer.class, Long.class, "concurrent.snapshots.threshold.perhost", - null, "Limits number of snapshots that can be handled by the host concurrently; default is NULL - unlimited", null), - - // API throttling - ApiLimitEnabled("Advanced", ManagementServer.class, Boolean.class, "api.throttling.enable", "true", "If true, api throttline feature is enabled", "true,false"); + null, "Limits number of snapshots that can be handled by the host concurrently; default is NULL - unlimited", null); - private final String _category; private final Class<?> _componentClass; private final Class<?> _type; http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/server/test/com/cloud/api/ListPerfTest.java ---------------------------------------------------------------------- diff --cc server/test/com/cloud/api/ListPerfTest.java index e5d277a,eb98d91..350dde8 --- a/server/test/com/cloud/api/ListPerfTest.java +++ b/server/test/com/cloud/api/ListPerfTest.java @@@ -16,14 -16,11 +16,18 @@@ // under the License. package com.cloud.api; ++import static org.junit.Assert.*; ++ import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.junit.Before; import org.junit.Test; ++import com.cloud.utils.exception.CloudRuntimeException; ++ /** * Test fixture to do performance test for list command @@@ -166,57 -163,6 +170,71 @@@ public class ListPerfTest extends APITe } + @Test + public void testMultiListAccounts() throws Exception { + // log in using normal user + login("demo", "password"); + // issue list Accounts calls + final HashMap<String, String> params = new HashMap<String, String>(); + params.put("response", "json"); + params.put("listAll", "true"); + params.put("sessionkey", sessionKey); - int clientCount = 6; ++ // assuming ApiRateLimitService set api.throttling.max = 25 ++ int clientCount = 26; + 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(); + - System.out.println(sendRequest("listAccounts", params)); ++ sendRequest("listAccounts", params); + ++ isUsable[j] = true; ++ ++ } catch (CloudRuntimeException e){ ++ isUsable[j] = false; ++ e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + endGate.countDown(); + } + } + }; + } + + ExecutorService executor = Executors.newFixedThreadPool(clientCount); + + for (Runnable runnable : clients) { + executor.execute(runnable); + } + + startGate.countDown(); + + endGate.await(); + ++ int rejectCount = 0; ++ for ( int i = 0; i < isUsable.length; ++i){ ++ if ( !isUsable[i]) ++ rejectCount++; ++ } ++ ++ assertEquals("Only one request should be rejected!", 1, rejectCount); ++ + } } http://git-wip-us.apache.org/repos/asf/incubator-cloudstack/blob/4355d06a/utils/src/com/cloud/utils/exception/CSExceptionErrorCode.java ---------------------------------------------------------------------- diff --cc utils/src/com/cloud/utils/exception/CSExceptionErrorCode.java index 303e0d6,303e0d6..8f40233 --- a/utils/src/com/cloud/utils/exception/CSExceptionErrorCode.java +++ b/utils/src/com/cloud/utils/exception/CSExceptionErrorCode.java @@@ -96,6 -96,6 +96,7 @@@ public class CSExceptionErrorCode ExceptionErrorCodeMap.put("com.cloud.exception.UnsupportedServiceException", 4530); ExceptionErrorCodeMap.put("com.cloud.exception.VirtualMachineMigrationException", 4535); ExceptionErrorCodeMap.put("com.cloud.async.AsyncCommandQueued", 4540); ++ ExceptionErrorCodeMap.put("com.cloud.exception.RequestLimitException", 4545); // Have a special error code for ServerApiException when it is // thrown in a standalone manner when failing to detect any of the above