package org.caudexorigo.http.netty4;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.util.ReferenceCountUtil;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import org.caudexorigo.concurrent.CustomExecutors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CacheAdapter extends HttpAction
{
private static Logger log = LoggerFactory.getLogger(HttpAction.class);
private static final ScheduledExecutorService schedExec = CustomExecutors.newScheduledThreadPool(2, "sched-exec");
private static final CharSequence ncache = HttpHeaders.newEntity("X-NCache");
private static final CharSequence hit = HttpHeaders.newEntity("hit");
private static final CharSequence lookup = HttpHeaders.newEntity("lookup");
private static final CharSequence pass_through = HttpHeaders.newEntity("pass-through");
private HttpAction wrapped;
private final ConcurrentMap<CacheKey, FullHttpResponse> cachedContent = new ConcurrentHashMap<CacheKey, FullHttpResponse>();
private CacheKeyBuilder cacheKeyBuilder;
public CacheAdapter(HttpAction wrapped, CacheKeyBuilder cacheKeyBuilder)
{
this.wrapped = wrapped;
this.cacheKeyBuilder = cacheKeyBuilder;
}
@Override
public void service(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse rsp)
{
}
@Override
protected void process(ChannelHandlerContext ctx, FullHttpRequest request, RequestObserver requestObserver)
{
if (request.getMethod().equals(HttpMethod.GET))
{
observeBegin(ctx, request, requestObserver);
FullHttpResponse response = cachedProcess(ctx, request, requestObserver);
boolean is_keep_alive = HttpHeaders.isKeepAlive(request);
commitResponse(ctx, response, is_keep_alive);
observeEnd(ctx, request, response, requestObserver);
}
else
{
this.wrapped.process(ctx, request, requestObserver);
}
}
private FullHttpResponse cachedProcess(ChannelHandlerContext ctx, FullHttpRequest request, RequestObserver requestObserver)
{
prepareRequest(request);
final CacheKey ck = cacheKeyBuilder.build(ctx, request);
FullHttpResponse response = cachedContent.get(ck);
if ((response == null))
{
response = buildResponse(ctx);
log.debug("Cache miss for: {}", ck);
ReferenceCountUtil.retain(response);
try
{
wrapped.service(ctx, request, response);
}
catch (Throwable t)
{
if (response != null)
{
ReferenceCountUtil.release(response);
}
throw new RuntimeException(t);
}
prepareResponse(request, response);
if (!isCacheable(response))
{
log.warn("Response object for resource '{}' is not cacheable.", request.getUri());
response.headers().set(ncache, pass_through);
ReferenceCountUtil.release(response);
return response;
}
response.headers().set(ncache, lookup);
cachedContent.put(ck, response);
Runnable evictioner = new Runnable()
{
public void run()
{
evict(ck);
}
};
schedExec.schedule(evictioner, ck.getCacheTime(), ck.getCacheTimeUnit());
}
else
{
if (response.content().readableBytes() == 0)
{
log.warn("Empty cache hit for: {}", ck, response.content());
evict(ck);
return cachedProcess(ctx, request, requestObserver);
}
log.debug("Cache hit for: {}", ck);
response.headers().set(ncache, hit);
ReferenceCountUtil.retain(response);
}
return response;
}
private boolean isCacheable(FullHttpResponse response)
{
if (response.content().readableBytes() == 0)
{
log.warn("netty bug 'response.content().readableBytes() == 0', pass-through");
return false;
}
if (response.headers().contains(HttpHeaders.Names.SET_COOKIE))
{
return false;
}
else
{
return true;
}
}
public void prepareRequest(FullHttpRequest request)
{
// extension point
}
public void prepareResponse(FullHttpRequest request, FullHttpResponse response)
{
// extension point
}
private FullHttpResponse evict(final CacheKey ck)
{
log.debug("Evict entry '{}'", ck);
FullHttpResponse rsp = cachedContent.remove(ck);
if (rsp != null)
{
ReferenceCountUtil.release(rsp);
}
return rsp;
}
public FullHttpResponse removeCachedEntry(CacheKey ck)
{
return evict(ck);
}
public void clear()
{
Set<CacheKey> keys = new HashSet<CacheKey>();
keys.addAll(cachedContent.keySet());
for (CacheKey ck : keys)
{
try
{
evict(ck);
}
catch (Throwable t)
{
log.warn("Error on eviction: {}", t.getMessage());
}
}
cachedContent.clear();
keys.clear();
}
}