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

Commit

Permalink
rhbz988202 - add introspectable REST service and make rate limiting b…
Browse files Browse the repository at this point in the history
…ucket monitorable
  • Loading branch information
Patrick Huang committed Mar 10, 2014
1 parent 520aab9 commit bc91d20
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 21 deletions.
@@ -0,0 +1,147 @@
package org.zanata.rest.service;

import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlRootElement;

import org.jboss.resteasy.annotations.providers.jaxb.Wrapped;
import org.jboss.resteasy.util.GenericType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Transactional;
import org.jboss.seam.annotations.security.Restrict;
import org.zanata.common.Namespaces;
import org.zanata.rest.MediaTypes;
import org.zanata.rest.dto.Link;
import org.zanata.seam.interceptor.RateLimitingInterceptor;
import org.zanata.util.Introspectable;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;

/**
* @author Patrick Huang <a
* href="mailto:pahuang@redhat.com">pahuang@redhat.com</a>
*/
@Name("introspectableObjectMonitorService")
@Path("/monitor")
@Produces({ "application/xml" })
@Consumes({ "application/xml" })
@Transactional
@Restrict("#{s:hasRole('admin')}")
@Slf4j
public class IntrospectableObjectMonitorService {
// TODO check http://code.google.com/p/reflections/ and re-implement this
private static List<Introspectable> introspectables = ImmutableList
.<Introspectable> builder().add(new RateLimitingInterceptor())
.build();

/** Type of media requested. */
@HeaderParam("Accept")
@DefaultValue(MediaType.APPLICATION_XML)
@Context
private MediaType accept;

/**
* Return all Introspectable objects link.
*
* @return The following response status codes will be returned from this
* operation:<br>
* OK(200) - all available introspectable objects with hypermedia link.<br>
* UNAUTHORIZED(401) - if not admin role.<br>
* INTERNAL SERVER ERROR(500) - If there is an unexpected error in
* the server while performing this operation.
*/
@GET
@Wrapped(element = "introspectable", namespace = Namespaces.ZANATA_API)
public Response get() {
List<LinkRoot> all =
Lists.transform(introspectables,
new Function<Introspectable, LinkRoot>() {
@Override
public LinkRoot apply(Introspectable input) {
return new LinkRoot(
URI.create("/"
+ new RateLimitingInterceptor()
.getId()), "self",
MediaTypes.createFormatSpecificType(
MediaType.APPLICATION_XML,
accept));
}
});

Type genericType = new GenericType<List<LinkRoot>>() {
}.getGenericType();
Object entity = new GenericEntity<List<LinkRoot>>(all, genericType);
return Response.ok().entity(entity).build();
}

/**
* Return a single introspectable fields as String.
* @param id introspectable id
* @return The following response status codes will be returned from this
* operation:<br>
* OK(200) - Response containing a string of all fields and values.<br>
* NOT_FOUND(404) - given id does not represent an introspectable.<br>
* INTERNAL SERVER ERROR(500) - If there is an unexpected error in
* the server while performing this operation.
*/
@GET
@Path("/{id}")
public Response get(@PathParam("id") final String id) {

Optional<Introspectable> optional =
Iterables.tryFind(introspectables,
new Predicate<Introspectable>() {
@Override
public boolean apply(Introspectable input) {
return input.getId().equals(id);
}
});
if (!optional.isPresent()) {
return Response.status(Response.Status.NOT_FOUND).build();
}
final Introspectable introspectable = optional.get();
final String format = "%s:%s\n";
Iterable<String> report =
Iterables.transform(introspectable.getFieldNames(),
new Function<String, String>() {
@Override
public String apply(String fieldName) {
return String.format(format, fieldName,
introspectable.get(fieldName));
}
});
return Response
.ok()
.entity(introspectable.getId() + "{"
+ Iterables.toString(report) + "}").build();
}

@XmlRootElement(name = "link")
public static class LinkRoot extends Link {
@SuppressWarnings("unused")
public LinkRoot() {
super();
}

public LinkRoot(URI href, String rel, String type) {
super(href, rel, type);
}
}
}
Expand Up @@ -46,8 +46,6 @@ public class ServerConfigurationService implements ServerConfigurationResource {

private static List<String> availableKeys;

@Context
private UriInfo uriInfo;
/** Type of media requested. */
@HeaderParam("Accept")
@DefaultValue(MediaType.APPLICATION_XML)
Expand Down
@@ -1,7 +1,11 @@
package org.zanata.seam.interceptor;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import javax.ws.rs.WebApplicationException;
Expand All @@ -10,7 +14,6 @@
import org.isomorphism.util.TokenBucket;
import org.isomorphism.util.TokenBuckets;
import org.jboss.seam.Component;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.intercept.AroundInvoke;
import org.jboss.seam.annotations.intercept.Interceptor;
import org.jboss.seam.intercept.InvocationContext;
Expand All @@ -19,10 +22,14 @@
import org.zanata.ApplicationConfiguration;
import org.zanata.annotation.RateLimiting;
import org.zanata.security.ZanataIdentity;
import org.zanata.webtrans.client.Application;
import org.zanata.util.Introspectable;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;

/**
Expand All @@ -31,10 +38,12 @@
*/
@Interceptor(stateless = true, around = JavaBeanInterceptor.class)
@Slf4j
public class RateLimitingInterceptor implements OptimizedInterceptor {
private static final Cache<String, TokenBucket> ACTIVE_CALLERS = CacheBuilder
.newBuilder().maximumSize(100).build();
public class RateLimitingInterceptor implements OptimizedInterceptor,
Introspectable {
private static final Cache<String, TokenBucket> ACTIVE_CALLERS =
CacheBuilder.newBuilder().maximumSize(100).build();
private static final int PERIOD = 1;

private int rateLimit;

public static int readRateLimitConfig() {
Expand All @@ -52,11 +61,6 @@ public Object aroundInvoke(InvocationContext ic) throws Exception {
return ic.proceed();
}

if (log.isDebugEnabled()) {
Class<?> target = ic.getTarget().getClass();
log.debug("rate limit potential target: {}#{}", target, method.getName());
}

int oldRateLimit = rateLimit;
rateLimit = readRateLimitConfig();

Expand All @@ -73,18 +77,24 @@ public Object aroundInvoke(InvocationContext ic) throws Exception {
ACTIVE_CALLERS.get(apiKey, new Callable<TokenBucket>() {
@Override
public TokenBucket call() throws Exception {
return TokenBuckets.newFixedIntervalRefill(
rateLimit, rateLimit, PERIOD,
TimeUnit.SECONDS);
return TokenBuckets.newFixedIntervalRefill(rateLimit,
rateLimit, PERIOD, TimeUnit.SECONDS);
}
});
log.debug("accessing {} will be rate limited to {} per second", method.getName(),
rateLimit);
if (log.isDebugEnabled()) {
Class<?> target = ic.getTarget().getClass();
// current bucket size is 0 doesn't mean request can't be served.
// org.isomorphism.util.TokenBucket.tryConsume()() will always try refill before checking.
// @see org.isomorphism.util.TokenBucket
log.debug(
"accessing {}#{} will be rate limited to {} per second. Current size {}",
target, method.getName(), rateLimit, getSize(tokenBucket));
}
if (!tokenBucket.tryConsume()) {
throw new WebApplicationException(Response
.status(Response.Status.FORBIDDEN)
.entity("requests for this API key has exceeded allowed rate limit per second:" + rateLimit)
.build());
throw new WebApplicationException(
Response.status(Response.Status.FORBIDDEN)
.entity("requests for this API key has exceeded allowed rate limit per second:"
+ rateLimit).build());
}
return ic.proceed();
}
Expand All @@ -100,4 +110,66 @@ private static void cleanUpBucketIfRateChanged(int oldRate, int newRate) {
public boolean isInterceptorEnabled() {
return true;
}

@Override
public String getId() {
return getClass().getCanonicalName();
}

@Override
public Collection<String> getFieldNames() {

return Lists.newArrayList(IntrospectableFields.Buckets.name());
}

@Override
@SuppressWarnings("unchecked")
public String get(String fieldName) {
IntrospectableFields field = IntrospectableFields.valueOf(fieldName);
switch (field) {
case Buckets:
return Iterables.toString(peekCurrentBuckets());
default:
throw new IllegalArgumentException("unknown field:" + fieldName);
}
}

private static Iterable<String> peekCurrentBuckets() {
ConcurrentMap<String, TokenBucket> map = ACTIVE_CALLERS.asMap();
return Iterables.transform(map.entrySet(),
new Function<Map.Entry<String, TokenBucket>, String>() {

@Override
public String apply(Map.Entry<String, TokenBucket> input) {

TokenBucket bucket = input.getValue();
try {
Object size = getSize(bucket);
return input.getKey() + ":" + size;
} catch (Exception e) {
String msg = "can not peek bucket size";
log.warn(msg);
return msg;
}
}
});
}

private static Object getSize(TokenBucket bucket) {
try {
Field field = TokenBucket.class.getDeclaredField("size");
field.setAccessible(true);
return field.get(bucket);
}
catch (NoSuchFieldException e) {
throw Throwables.propagate(e);
}
catch (IllegalAccessException e) {
throw Throwables.propagate(e);
}
}

private enum IntrospectableFields {
Buckets
}
}
12 changes: 12 additions & 0 deletions zanata-war/src/main/java/org/zanata/util/Introspectable.java
@@ -0,0 +1,12 @@
package org.zanata.util;

import java.util.Collection;

public interface Introspectable {

String getId();

Collection<String> getFieldNames();

String get(String fieldName);
}

0 comments on commit bc91d20

Please sign in to comment.