diff --git a/client/tomcatconf/api-limit_commands.properties.in b/client/tomcatconf/api-limit_commands.properties.in new file mode 100644 index 00000000000..fcb963ac867 --- /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 diff --git a/plugins/api/rate-limit/pom.xml b/plugins/api/rate-limit/pom.xml new file mode 100644 index 00000000000..416c901cf8a --- /dev/null +++ b/plugins/api/rate-limit/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + cloud-plugin-api-limit-account-based + Apache CloudStack Plugin - API Rate Limit + + org.apache.cloudstack + cloudstack-plugins + 4.1.0-SNAPSHOT + ../../pom.xml + + 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 00000000000..3f9e4eb4503 --- /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); + } +} + + 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 00000000000..8029ab3707a --- /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"); + } + } +} 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 00000000000..245e8f15d8a --- /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; + } + + +} 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 00000000000..e7ba9d45fc3 --- /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); +} 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 00000000000..e14f65d383e --- /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 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; + + } + + +} 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 00000000000..659cf81b0e6 --- /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(); + + } + + + +} 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 00000000000..a5e086b3029 --- /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(); + +} 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 00000000000..76e8a2d9281 --- /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 */ +} 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 00000000000..40965d9da8e --- /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(); + } +} 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 00000000000..0e2080ab2f1 --- /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. 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); + + } + */ +} diff --git a/plugins/pom.xml b/plugins/pom.xml index a42ae2967b1..7bb60a990fb 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -32,6 +32,7 @@ test + api/rate-limit api/discovery acl/static-role-based deployment-planners/user-concentrated-pod