/*
* Copyright 2011 The Netty Project
*
* The Netty Project 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 io.netty.handler.traffic;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import io.netty.buffer.ChannelBuffer;
import io.netty.channel.Channel;
import io.netty.channel.ChannelEvent;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelState;
import io.netty.channel.ChannelStateEvent;
import io.netty.channel.MessageEvent;
import io.netty.channel.SimpleChannelHandler;
import io.netty.logging.InternalLogger;
import io.netty.logging.InternalLoggerFactory;
import io.netty.util.ExternalResourceReleasable;
import io.netty.util.internal.ExecutorUtil;
/**
* AbstractTrafficShapingHandler allows to limit the global bandwidth
* (see {@link GlobalTrafficShapingHandler}) or per session
* bandwidth (see {@link ChannelTrafficShapingHandler}), as traffic shaping.
* It allows too to implement an almost real time monitoring of the bandwidth using
* the monitors from {@link TrafficCounter} that will call back every checkInterval
* the method doAccounting of this handler.<br>
* <br>
*
* If you want for any particular reasons to stop the monitoring (accounting) or to change
* the read/write limit or the check interval, several methods allow that for you:<br>
* <ul>
* <li><tt>configure</tt> allows you to change read or write limits, or the checkInterval</li>
* <li><tt>getTrafficCounter</tt> allows you to have access to the TrafficCounter and so to stop
* or start the monitoring, to change the checkInterval directly, or to have access to its values.</li>
* <li></li>
* </ul>
*/
public abstract class AbstractTrafficShapingHandler extends
SimpleChannelHandler implements ExternalResourceReleasable {
/**
* Internal logger
*/
static InternalLogger logger = InternalLoggerFactory
.getInstance(AbstractTrafficShapingHandler.class);
/**
* Default delay between two checks: 1s
*/
public static final long DEFAULT_CHECK_INTERVAL = 1000;
/**
* Default minimal time to wait
*/
private static final long MINIMAL_WAIT = 10;
/**
* Traffic Counter
*/
protected TrafficCounter trafficCounter;
/**
* Executor to associated to any TrafficCounter
*/
protected Executor executor;
/**
* Limit in B/s to apply to write
*/
private long writeLimit;
/**
* Limit in B/s to apply to read
*/
private long readLimit;
/**
* Delay between two performance snapshots
*/
protected long checkInterval = DEFAULT_CHECK_INTERVAL; // default 1 s
/**
* Boolean associated with the release of this TrafficShapingHandler.
* It will be true only once when the releaseExternalRessources is called
* to prevent waiting when shutdown.
*/
final AtomicBoolean release = new AtomicBoolean(false);
private void init(
Executor newExecutor, long newWriteLimit, long newReadLimit, long newCheckInterval) {
executor = newExecutor;
writeLimit = newWriteLimit;
readLimit = newReadLimit;
checkInterval = newCheckInterval;
//logger.info("TSH: "+writeLimit+":"+readLimit+":"+checkInterval+":"+isPerChannel());
}
/**
*
* @param newTrafficCounter the TrafficCounter to set
*/
void setTrafficCounter(TrafficCounter newTrafficCounter) {
trafficCounter = newTrafficCounter;
}
/**
* @param executor
* created for instance like Executors.newCachedThreadPool
* @param writeLimit
* 0 or a limit in bytes/s
* @param readLimit
* 0 or a limit in bytes/s
* @param checkInterval
* The delay between two computations of performances for
* channels or 0 if no stats are to be computed
*/
public AbstractTrafficShapingHandler(Executor executor, long writeLimit,
long readLimit, long checkInterval) {
init(executor, writeLimit, readLimit, checkInterval);
}
/**
* @param executor
* created for instance like Executors.newCachedThreadPool
* @param writeLimit
* 0 or a limit in bytes/s
* @param readLimit
* 0 or a limit in bytes/s
*/
public AbstractTrafficShapingHandler(Executor executor, long writeLimit,
long readLimit) {
init(executor, writeLimit, readLimit, DEFAULT_CHECK_INTERVAL);
}
/**
* Change the underlying limitations and check interval.
*/
public void configure(long newWriteLimit, long newReadLimit,
long newCheckInterval) {
this.configure(newWriteLimit, newReadLimit);
this.configure(newCheckInterval);
}
/**
* Change the underlying limitations.
*/
public void configure(long newWriteLimit, long newReadLimit) {
writeLimit = newWriteLimit;
readLimit = newReadLimit;
if (trafficCounter != null) {
trafficCounter.resetAccounting(System.currentTimeMillis() + 1);
}
}
/**
* Change the check interval.
*/
public void configure(long newCheckInterval) {
checkInterval = newCheckInterval;
if (trafficCounter != null) {
trafficCounter.configure(checkInterval);
}
}
/**
* Called each time the accounting is computed from the TrafficCounters.
* This method could be used for instance to implement almost real time accounting.
*
* @param counter
* the TrafficCounter that computes its performance
*/
protected void doAccounting(TrafficCounter counter) {
// NOOP by default
}
/**
* Class to implement setReadable at fix time
*/
private class ReopenRead implements Runnable {
/**
* Associated ChannelHandlerContext
*/
private ChannelHandlerContext ctx;
/**
* Time to wait before clearing the channel
*/
private long timeToWait;
/**
* @param ctx
* the associated channelHandlerContext
* @param timeToWait
*/
protected ReopenRead(ChannelHandlerContext ctx, long timeToWait) {
this.ctx = ctx;
this.timeToWait = timeToWait;
}
/**
* Truly run the waken up of the channel
*/
@Override
public void run() {
try {
if (release.get()) {
return;
}
Thread.sleep(timeToWait);
} catch (InterruptedException e) {
// interruption so exit
return;
}
// logger.info("WAKEUP!");
if (ctx != null && ctx.getChannel() != null &&
ctx.getChannel().isConnected()) {
//logger.info(" setReadable TRUE: "+timeToWait);
// readSuspended = false;
ctx.setAttachment(null);
ctx.getChannel().setReadable(true);
}
}
}
/**
*
* @return the time that should be necessary to wait to respect limit. Can
* be negative time
*/
private long getTimeToWait(long limit, long bytes, long lastTime,
long curtime) {
long interval = curtime - lastTime;
if (interval == 0) {
// Time is too short, so just lets continue
return 0;
}
return bytes * 1000 / limit - interval;
}
@Override
public void messageReceived(ChannelHandlerContext arg0, MessageEvent arg1)
throws Exception {
try {
long curtime = System.currentTimeMillis();
long size = ((ChannelBuffer) arg1.getMessage()).readableBytes();
if (trafficCounter != null) {
trafficCounter.bytesRecvFlowControl(arg0, size);
if (readLimit == 0) {
// no action
return;
}
// compute the number of ms to wait before reopening the channel
long wait = getTimeToWait(readLimit, trafficCounter
.getCurrentReadBytes(), trafficCounter.getLastTime(),
curtime);
if (wait > MINIMAL_WAIT) { // At least 10ms seems a minimal time in order to
Channel channel = arg0.getChannel();
// try to limit the traffic
if (channel != null && channel.isConnected()) {
// Channel version
if (executor == null) {
// Sleep since no executor
//logger.info("Read sleep since no executor for "+wait+" ms for "+this);
if (release.get()) {
return;
}
Thread.sleep(wait);
return;
}
if (arg0.getAttachment() == null) {
// readSuspended = true;
arg0.setAttachment(Boolean.TRUE);
channel.setReadable(false);
//logger.info("Read will wakeup after "+wait+" ms "+this);
executor.execute(new ReopenRead(arg0, wait));
} else {
// should be waiting: but can occurs sometime so as a FIX
//logger.info("Read sleep ok but should not be here: "+wait+" "+this);
if (release.get()) {
return;
}
Thread.sleep(wait);
}
} else {
// Not connected or no channel
//logger.info("Read sleep "+wait+" ms for "+this);
if (release.get()) {
return;
}
Thread.sleep(wait);
}
}
}
} finally {
// The message is then just passed to the next handler
super.messageReceived(arg0, arg1);
}
}
@Override
public void writeRequested(ChannelHandlerContext arg0, MessageEvent arg1)
throws Exception {
try {
long curtime = System.currentTimeMillis();
long size = ((ChannelBuffer) arg1.getMessage()).readableBytes();
if (trafficCounter != null) {
trafficCounter.bytesWriteFlowControl(size);
if (writeLimit == 0) {
return;
}
// compute the number of ms to wait before continue with the channel
long wait = getTimeToWait(writeLimit, trafficCounter
.getCurrentWrittenBytes(), trafficCounter.getLastTime(),
curtime);
if (wait > MINIMAL_WAIT) {
// Global or Channel
if (release.get()) {
return;
}
Thread.sleep(wait);
}
}
} finally {
// The message is then just passed to the next handler
super.writeRequested(arg0, arg1);
}
}
@Override
public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e)
throws Exception {
if (e instanceof ChannelStateEvent) {
ChannelStateEvent cse = (ChannelStateEvent) e;
if (cse.getState() == ChannelState.INTEREST_OPS &&
(((Integer) cse.getValue()).intValue() & Channel.OP_READ) != 0) {
// setReadable(true) requested
boolean readSuspended = ctx.getAttachment() != null;
if (readSuspended) {
// Drop the request silently if this handler has
// set the flag.
e.getFuture().setSuccess();
return;
}
}
}
super.handleDownstream(ctx, e);
}
/**
*
* @return the current TrafficCounter (if
* channel is still connected)
*/
public TrafficCounter getTrafficCounter() {
return trafficCounter;
}
@Override
public void releaseExternalResources() {
if (trafficCounter != null) {
trafficCounter.stop();
}
release.set(true);
ExecutorUtil.terminate(executor);
}
@Override
public String toString() {
return "TrafficShaping with Write Limit: " + writeLimit +
" Read Limit: " + readLimit + " and Counter: " +
(trafficCounter != null? trafficCounter.toString() : "none");
}
}