package mediathek.controller; import java.util.concurrent.Semaphore; import mSearch.tool.Listener; import mediathek.config.MVConfig; /** * This singleton class provides the necessary tokens for direct file downloads. * It ensures that selected bandwidth limit will not be exceeded for all concurrent direct downloads. * Bandwidth throttling based on http://en.wikipedia.org/wiki/Token_bucket */ public class MVBandwidthTokenBucket { public static final int DEFAULT_BUFFER_SIZE = 4 * 1024; // default byte buffer size private final Semaphore bucketSize = new Semaphore(0, false); public static final int BANDWIDTH_MAX_RED_KBYTE = 500; // 500 kByte/s public static final int BANDWIDTH_MAX_BYTE = 1_000_000; // 1.000 kByte/s public static final int BANDWIDTH_MAX_KBYTE = 1_000; // 1.000 kByte/s private volatile int bucketCapacity = BANDWIDTH_MAX_RED_KBYTE * 1_000; // 500kByte/s private MVBandwidthTokenBucketFillerThread fillerThread = null; public MVBandwidthTokenBucket() { setBucketCapacity(getBandwidth()); Listener.addListener(new Listener(Listener.EREIGNIS_BANDBREITE, MVBandwidthTokenBucket.class.getSimpleName()) { @Override public void ping() { setBucketCapacity(getBandwidth()); } }); } /** * Ensure that bucket filler thread is running. * If it running, nothing will happen. */ public synchronized void ensureBucketThreadIsRunning() { if (fillerThread == null) { fillerThread = new MVBandwidthTokenBucketFillerThread(); fillerThread.start(); } } /** * Take number of byte tickets from bucket. * * @param howMany The number of bytes to acquire. */ public void takeBlocking(final int howMany) { //if bucket size equals BANDWIDTH_MAX_BYTE then unlimited speed... if (getBucketCapacity() < BANDWIDTH_MAX_BYTE) { try { bucketSize.acquire(howMany); } catch (Exception ignored) { } } } /** * Acquire one byte ticket from bucket. */ public void takeBlocking() { takeBlocking(1); } /** * Get the capacity of the Token Bucket. * * @return Maximum number of tokens in the bucket. */ public synchronized int getBucketCapacity() { return bucketCapacity; } /** * Kill the semaphore filling thread. */ private void terminateFillerThread() { if (fillerThread != null) { fillerThread.interrupt(); fillerThread = null; } } public synchronized void setBucketCapacity(int bucketCapacity) { this.bucketCapacity = bucketCapacity; if (bucketCapacity == BANDWIDTH_MAX_BYTE) { terminateFillerThread(); //if we have waiting callers, release them by releasing buckets in the semaphore... while (bucketSize.hasQueuedThreads()) { bucketSize.release(); } //reset semaphore bucketSize.drainPermits(); } else { terminateFillerThread(); bucketSize.drainPermits(); //restart filler thread with new settings... ensureBucketThreadIsRunning(); } } /** * Read bandwidth settings from config. * * @return The maximum bandwidth in bytes set or zero for unlimited speed. */ private int getBandwidth() { int bytesPerSecond; try { final int maxKBytePerSec = (int) Long.parseLong(MVConfig.get(MVConfig.Configs.SYSTEM_BANDBREITE_KBYTE)); bytesPerSecond = maxKBytePerSec * 1_000; } catch (Exception ex) { bytesPerSecond = BANDWIDTH_MAX_KBYTE * 1_000; MVConfig.add(MVConfig.Configs.SYSTEM_BANDBREITE_KBYTE, BANDWIDTH_MAX_KBYTE + ""); } return bytesPerSecond; } /** * Fills the bucket semaphore with available download buckets for speed management. */ private class MVBandwidthTokenBucketFillerThread extends Thread { public MVBandwidthTokenBucketFillerThread() { setName("MVBandwidthTokenBucket Filler Thread"); } @Override public void run() { try { while (!isInterrupted()) { // run 2times per second, its more regular final int bucketCapacity = getBucketCapacity(); //for unlimited speed we dont need the thread if (bucketCapacity == MVBandwidthTokenBucket.BANDWIDTH_MAX_BYTE) { break; } final int releaseCount = bucketCapacity / 2 - bucketSize.availablePermits(); if (releaseCount > 0) { bucketSize.release(releaseCount); } sleep(500); } } catch (Exception ignored) { } } } }