Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Commit

Permalink
rhbz988202 - add global switch to turn on/off rate limiting
Browse files Browse the repository at this point in the history
  • Loading branch information
Patrick Huang committed Mar 12, 2014
1 parent a060550 commit cae4f31
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 54 deletions.
Expand Up @@ -55,6 +55,8 @@ public class HApplicationConfiguration extends ModelEntityBase {
public static String KEY_RATE_LIMIT_PER_SECOND = "rate.limit.per.second";
public static String KEY_MAX_CONCURRENT_REQ_PER_API_KEY = "max.concurrent.req.per.apikey";
public static String KEY_MAX_ACTIVE_REQ_PER_API_KEY = "max.active.req.per.apikey";
public static String KEY_RATE_LIMIT_SWITCH = "rate.limit.switch";

private static final long serialVersionUID = 8652817113098817448L;

private String key;
Expand Down
Expand Up @@ -392,6 +392,10 @@ private String getBaseWebAssetsUrl() {
"//assets-zanata.rhcloud.com");
}

public boolean getRateLimitSwitch() {
String rateLimitSwitch = databaseBackedConfig.getRateLimitSwitch();
return Boolean.parseBoolean(rateLimitSwitch);
}
public double getRateLimitPerSecond() {
String limitPerSecond =
databaseBackedConfig.getRateLimitPerSecond();
Expand Down
Expand Up @@ -27,6 +27,7 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.hibernate.validator.constraints.Email;
import org.jboss.seam.ScopeType;
Expand All @@ -51,6 +52,7 @@
@Restrict("#{s:hasRole('admin')}")
@Getter
@Setter
@Slf4j
public class ServerConfigurationBean implements Serializable {
private static final long serialVersionUID = 1L;

Expand Down Expand Up @@ -105,6 +107,8 @@ public class ServerConfigurationBean implements Serializable {
@Pattern(regexp = "\\d{0,5}")
private String maxActiveRequestsPerApiKey;

private boolean rateLimitSwitch;

public String getHomeContent() {
HApplicationConfiguration var =
applicationConfigurationDAO
Expand Down Expand Up @@ -215,6 +219,12 @@ public void onCreate() {
this.termsOfUseUrl = termsOfUseUrlValue.getValue();
}

HApplicationConfiguration rateLimitSwitch = applicationConfigurationDAO
.findByKey(HApplicationConfiguration.KEY_RATE_LIMIT_SWITCH);
if (rateLimitSwitch != null) {
this.rateLimitSwitch = Boolean.valueOf(rateLimitSwitch.getValue());
}

HApplicationConfiguration rateLimitValue = applicationConfigurationDAO
.findByKey(HApplicationConfiguration.KEY_RATE_LIMIT_PER_SECOND);
if (rateLimitValue != null) {
Expand Down Expand Up @@ -326,6 +336,14 @@ public String update() {
HApplicationConfiguration.KEY_TERMS_CONDITIONS_URL,
termsOfUseUrlValue, termsOfUseUrl, applicationConfigurationDAO);

HApplicationConfiguration rateLimitSwitchValue =
applicationConfigurationDAO
.findByKey(HApplicationConfiguration.KEY_RATE_LIMIT_SWITCH);
ServerConfigurationService.persistApplicationConfig(
HApplicationConfiguration.KEY_RATE_LIMIT_SWITCH,
rateLimitSwitchValue, "" + rateLimitSwitch,
applicationConfigurationDAO);

HApplicationConfiguration rateLimitValue =
applicationConfigurationDAO
.findByKey(HApplicationConfiguration.KEY_RATE_LIMIT_PER_SECOND);
Expand Down
Expand Up @@ -158,6 +158,10 @@ public String getTermsOfUseUrl() {
return getConfigValue(HApplicationConfiguration.KEY_TERMS_CONDITIONS_URL);
}

public String getRateLimitSwitch() {
return getConfigValue(HApplicationConfiguration.KEY_RATE_LIMIT_SWITCH);
}

public String getRateLimitPerSecond() {
return getConfigValue(HApplicationConfiguration.KEY_RATE_LIMIT_PER_SECOND);
}
Expand Down
Expand Up @@ -42,7 +42,7 @@ public class RateLimiterHolder implements Introspectable {

public static RateLimiterHolder getInstance() {
return (RateLimiterHolder) Component
.getInstance(RateLimiterHolder.class);
.getInstance("rateLimiterHolder");
}

@Create
Expand All @@ -53,7 +53,7 @@ public void loadConfig() {
private void readRateLimitState() {
ApplicationConfiguration appConfig =
(ApplicationConfiguration) Component
.getInstance(ApplicationConfiguration.class);
.getInstance("applicationConfiguration");
int maxConcurrent = appConfig.getMaxConcurrentRequestsPerApiKey();
int maxActive = appConfig.getMaxActiveRequestsPerApiKey();
double rateLimitPerSecond = appConfig.getRateLimitPerSecond();
Expand Down
Expand Up @@ -118,9 +118,8 @@ protected double rateLimitRate() {
public String toString() {
return Objects
.toStringHelper(this)
.add("id", super.toString())
.add("maxConcurrent", maxConcurrentSemaphore.availablePermits())
.add("maxConcurrent queue",
maxConcurrentSemaphore.getQueueLength())
.add("maxActive", maxActiveSemaphore.availablePermits())
.add("maxActive queue", maxActiveSemaphore.getQueueLength())
.add("rateLimiter", rateLimiter).toString();
Expand Down
Expand Up @@ -13,6 +13,8 @@
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.interception.PostProcessInterceptor;
import org.jboss.resteasy.spi.interception.PreProcessInterceptor;
import org.jboss.seam.Component;
import org.zanata.ApplicationConfiguration;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -30,10 +32,17 @@ public class ZanataRestRateLimiterInterceptor implements PreProcessInterceptor,
@Override
public ServerResponse preProcess(HttpRequest request, ResourceMethod method)
throws Failure, WebApplicationException {
String apiKey = HeaderHelper.getApiKey(request);
final String apiKey = HeaderHelper.getApiKey(request);
if (apiKey == null) {
return null;
}

ApplicationConfiguration appConfig =
(ApplicationConfiguration) Component
.getInstance("applicationConfiguration");
if (!appConfig.getRateLimitSwitch()) {
return null;
}
ActiveApiKeys apiKeys = ActiveApiKeys.getInstance();
apiKeys.setApiKeyForCurrentThread(apiKey);

Expand All @@ -46,6 +55,7 @@ public ServerResponse preProcess(HttpRequest request, ResourceMethod method)
rateLimiter = rateLimiterHolder.get(apiKey, new Callable<RestRateLimiter>() {
@Override
public RestRateLimiter call() throws Exception {
log.info("creating for api key: {}", apiKey);
return new RestRateLimiter(limitConfig);
}
});
Expand All @@ -54,7 +64,6 @@ public RestRateLimiter call() throws Exception {
throw new WebApplicationException(e,
Response.Status.INTERNAL_SERVER_ERROR);
}
rateLimiterHolder.put(apiKey, rateLimiter);
ContextInfo contextInfo = ContextInfo.of(apiKey, request.getUri().getPath(), rateLimiter);
log.debug("check semaphore for {}", contextInfo);
if (!rateLimiter.tryAcquireConcurrentPermit()) {
Expand Down
1 change: 1 addition & 0 deletions zanata-war/src/main/resources/messages.properties
Expand Up @@ -670,6 +670,7 @@ jsf.config.PiwikIdSite=Piwik Id
jsf.config.PiwikIdSitetooltip=Website Id in Piwik
jsf.config.TermsOfUseUrl=Terms of Use URL
jsf.config.TermsOfUseUrltooltip=The URL for terms of use statement
jsf.config.EnablingRateLimiting=Rate limiting REST services
jsf.config.RateLimitPerSecond=Rate Limit per second
jsf.config.RateLimitPerSecondtooltip=Rate Limiting REST API access (per second)
jsf.config.MaxConcurrentRequestsPerApiKey=Max concurrent requests per API key (default 6)
Expand Down
110 changes: 63 additions & 47 deletions zanata-war/src/main/webapp/admin/server_configuration.xhtml
Expand Up @@ -204,57 +204,73 @@
</s:span>
</s:decorate>

<s:decorate id="rateLimitField"
<s:decorate id="rateLimitSwitchField"
template="../WEB-INF/layout/edit.xhtml" enclose="true">
<ui:define
name="label">#{messages['jsf.config.RateLimitPerSecond']}</ui:define>
<h:inputText id="rateLimitEml"
value="#{serverConfigurationBean.rateLimitPerSecond}">
<a4j:ajax event="blur" render="rateLimitField"/>
</h:inputText>

<s:span styleClass="icon-info-circle-2 input_help"
id="rateLimitHelp">
<rich:tooltip>
#{messages['jsf.config.RateLimitPerSecondtooltip']}
</rich:tooltip>
</s:span>
</s:decorate>

<s:decorate id="maxConcurrentPerApiKeyField"
template="../WEB-INF/layout/edit.xhtml" enclose="true">
<ui:define
name="label">#{messages['jsf.config.MaxConcurrentRequestsPerApiKey']}</ui:define>
<h:inputText id="maxConcurrentPerApiKeyEml"
value="#{serverConfigurationBean.maxConcurrentRequestsPerApiKey}">
<a4j:ajax event="blur" render="maxConcurrentPerApiKeyField"/>
</h:inputText>

<s:span styleClass="icon-info-circle-2 input_help"
id="maxConcurrentRequestPerApiKeyHelp">
<rich:tooltip>
#{messages['jsf.config.MaxConcurrentRequestsPerApiKeytooltip']}
</rich:tooltip>
</s:span>
</s:decorate>

<s:decorate id="maxActiveRequestsPerApiKeyField"
template="../WEB-INF/layout/edit.xhtml" enclose="true">
<ui:define
name="label">#{messages['jsf.config.MaxActiveRequestsPerApiKey']}</ui:define>
<h:inputText id="maxActiveRequestsPerApiKeyEml"
value="#{serverConfigurationBean.maxActiveRequestsPerApiKey}">
<a4j:ajax event="blur" render="maxActiveRequestsPerApiKeyField"/>
</h:inputText>

<s:span styleClass="icon-info-circle-2 input_help"
id="maxActiveRequestsPerApiKeyHelp">
<rich:tooltip>
#{messages['jsf.config.MaxActiveRequestsPerApiKeytooltip']}
</rich:tooltip>
</s:span>
name="label">#{messages['jsf.config.EnablingRateLimiting']}</ui:define>
<h:selectOneRadio value="#{serverConfigurationBean.rateLimitSwitch}">
<f:selectItem itemValue="True" itemLabel="On" />
<f:selectItem itemValue="False" itemLabel="Off" />
<a4j:ajax event="change" render="serverConfigForm:rateLimitSettings"/>
</h:selectOneRadio>
</s:decorate>

<h:panelGroup id="rateLimitSettings">
<h:panelGroup rendered="#{serverConfigurationBean.rateLimitSwitch}">
<s:decorate id="rateLimitField"
template="../WEB-INF/layout/edit.xhtml" enclose="true">
<ui:define
name="label">#{messages['jsf.config.RateLimitPerSecond']}</ui:define>
<h:inputText id="rateLimitEml"
value="#{serverConfigurationBean.rateLimitPerSecond}">
<a4j:ajax event="blur" render="rateLimitField"/>
</h:inputText>

<s:span styleClass="icon-info-circle-2 input_help"
id="rateLimitHelp">
<rich:tooltip>
#{messages['jsf.config.RateLimitPerSecondtooltip']}
</rich:tooltip>
</s:span>
</s:decorate>

<s:decorate id="maxConcurrentPerApiKeyField"
template="../WEB-INF/layout/edit.xhtml" enclose="true">
<ui:define
name="label">#{messages['jsf.config.MaxConcurrentRequestsPerApiKey']}</ui:define>
<h:inputText id="maxConcurrentPerApiKeyEml"
value="#{serverConfigurationBean.maxConcurrentRequestsPerApiKey}">
<a4j:ajax event="blur" render="maxConcurrentPerApiKeyField"/>
</h:inputText>

<s:span styleClass="icon-info-circle-2 input_help"
id="maxConcurrentRequestPerApiKeyHelp">
<rich:tooltip>
#{messages['jsf.config.MaxConcurrentRequestsPerApiKeytooltip']}
</rich:tooltip>
</s:span>
</s:decorate>

<s:decorate id="maxActiveRequestsPerApiKeyField"
template="../WEB-INF/layout/edit.xhtml" enclose="true">
<ui:define
name="label">#{messages['jsf.config.MaxActiveRequestsPerApiKey']}</ui:define>
<h:inputText id="maxActiveRequestsPerApiKeyEml"
value="#{serverConfigurationBean.maxActiveRequestsPerApiKey}">
<a4j:ajax event="blur"
render="maxActiveRequestsPerApiKeyField"/>
</h:inputText>

<s:span styleClass="icon-info-circle-2 input_help"
id="maxActiveRequestsPerApiKeyHelp">
<rich:tooltip>
#{messages['jsf.config.MaxActiveRequestsPerApiKeytooltip']}
</rich:tooltip>
</s:span>
</s:decorate>
</h:panelGroup>

</h:panelGroup>

<div style="clear:both"/>

Expand Down
5 changes: 4 additions & 1 deletion zanata-war/src/test/java/org/zanata/ZanataRestTest.java
Expand Up @@ -29,6 +29,7 @@
import org.zanata.rest.ConstraintViolationExceptionMapper;
import org.zanata.rest.NoSuchEntityExceptionMapper;
import org.zanata.rest.NotLoggedInExceptionMapper;
import org.zanata.rest.RateLimiterHolder;
import org.zanata.rest.ZanataServiceExceptionMapper;
import org.zanata.rest.client.TraceDebugInterceptor;
import org.zanata.seam.SeamAutowire;
Expand Down Expand Up @@ -160,7 +161,9 @@ protected void prepareSeamAutowire() {
.reset()
.ignoreNonResolvable()
.use(SeamAutowire.getComponentName(JndiBackedConfig.class),
jndiBackedConfig);
jndiBackedConfig)
.use(SeamAutowire.getComponentName(RateLimiterHolder.class),
new RateLimiterHolder());
}

/**
Expand Down

0 comments on commit cae4f31

Please sign in to comment.