/* * Copyright (C) 2015 SoftIndex LLC. * * Licensed 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.datakernel.eventloop; import io.datakernel.jmx.EventloopJmxMBean; import io.datakernel.jmx.JmxAttribute; import io.datakernel.jmx.JmxOperation; import io.datakernel.jmx.JmxReducers.JmxReducerSum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Random; import static io.datakernel.util.Preconditions.check; import static java.lang.Math.pow; public final class ThrottlingController implements EventloopJmxMBean { private static int staticInstanceCounter = 0; private final Logger logger = LoggerFactory.getLogger(ThrottlingController.class.getName() + "." + staticInstanceCounter++); public static final int TARGET_TIME_MILLIS = 20; public static final int GC_TIME_MILLIS = 20; public static final int SMOOTHING_WINDOW = 10000; public static final double THROTTLING_DECREASE = 0.1; public static final double INITIAL_KEYS_PER_SECOND = 100; public static final double INITIAL_THROTTLING = 0.0; private static final Random random = new Random() { private long prev = System.nanoTime(); @Override protected int next(int nbits) { long x = this.prev; x ^= (x << 21); x ^= (x >>> 35); x ^= (x << 4); this.prev = x; x &= ((1L << nbits) - 1); return (int) x; } }; private Eventloop eventloop; // settings private int targetTimeMillis; private int gcTimeMillis; private double throttlingDecrease; private int smoothingWindow; // intermediate counters for current round private int bufferedRequests; private int bufferedRequestsThrottled; // exponentially smoothed values private double smoothedThrottling; private double smoothedTimePerKeyMillis; // JMX private long infoTotalRequests; private long infoTotalRequestsThrottled; private long infoTotalTimeMillis; private long infoRounds; private long infoRoundsZeroThrottling; private long infoRoundsExceededTargetTime; private long infoRoundsGc; private float throttling; private ThrottlingController() { } // region builders public static ThrottlingController create() { return new ThrottlingController() .withTargetTimeMillis(TARGET_TIME_MILLIS) .withGcTimeMillis(GC_TIME_MILLIS) .withSmoothingWindow(SMOOTHING_WINDOW) .withThrottlingDecrease(THROTTLING_DECREASE) .withInitialKeysPerSecond(INITIAL_KEYS_PER_SECOND) .withInitialThrottling(INITIAL_THROTTLING); } public ThrottlingController withTargetTimeMillis(long targetTimeMillis) { setTargetTimeMillis((int) targetTimeMillis); return this; } public ThrottlingController withGcTimeMillis(long gcTimeMillis) { setGcTimeMillis((int) gcTimeMillis); return this; } public ThrottlingController withSmoothingWindow(int smoothingWindow) { setSmoothingWindow(smoothingWindow); return this; } public ThrottlingController withThrottlingDecrease(double throttlingDecrease) { setThrottlingDecrease(throttlingDecrease); return this; } public ThrottlingController withInitialKeysPerSecond(double initialKeysPerSecond) { this.smoothedTimePerKeyMillis = 1000.0 / initialKeysPerSecond; return this; } public ThrottlingController withInitialThrottling(double initialThrottling) { this.smoothedThrottling = initialThrottling; return this; } // endregion public boolean isOverloaded() { bufferedRequests++; if (random.nextFloat() < throttling) { bufferedRequestsThrottled++; return true; } return false; } void updateInternalStats(int lastKeys, int lastTime) { if (lastTime < 0 || lastTime > 60000) { logger.warn("Invalid processing time: {}", lastTime); return; } int lastTimePredicted = (int) (lastKeys * getAvgTimePerKeyMillis()); if (gcTimeMillis != 0.0 && lastTime > lastTimePredicted + gcTimeMillis) { logger.debug("GC detected {} ms, {} keys", lastTime, lastKeys); lastTime = lastTimePredicted + gcTimeMillis; infoRoundsGc++; } double weight = 1.0 - 1.0 / smoothingWindow; if (bufferedRequests != 0) { assert bufferedRequestsThrottled <= bufferedRequests; double value = (double) bufferedRequestsThrottled / bufferedRequests; smoothedThrottling = (smoothedThrottling - value) * pow(weight, bufferedRequests) + value; infoTotalRequests += bufferedRequests; infoTotalRequestsThrottled += bufferedRequestsThrottled; bufferedRequests = 0; bufferedRequestsThrottled = 0; } if (lastKeys != 0) { double value = (double) lastTime / lastKeys; smoothedTimePerKeyMillis = (smoothedTimePerKeyMillis - value) * pow(weight, lastKeys) + value; } infoTotalTimeMillis += lastTime; } void calculateThrottling(int newKeys) { double predictedTime = newKeys * getAvgTimePerKeyMillis(); double newThrottling = getAvgThrottling() - throttlingDecrease; if (newThrottling < 0) newThrottling = 0; if (predictedTime > targetTimeMillis) { double extraThrottling = 1.0 - targetTimeMillis / predictedTime; if (extraThrottling > newThrottling) { newThrottling = extraThrottling; infoRoundsExceededTargetTime++; } } if (newThrottling == 0) infoRoundsZeroThrottling++; infoRounds++; this.throttling = (float) newThrottling; } public double getAvgTimePerKeyMillis() { return smoothedTimePerKeyMillis; } @JmxAttribute public double getAvgKeysPerSecond() { return 1000.0 / getAvgTimePerKeyMillis(); } @JmxAttribute public double getAvgThrottling() { return smoothedThrottling; } @JmxAttribute public int getTargetTimeMillis() { return targetTimeMillis; } @JmxAttribute public void setTargetTimeMillis(int targetTimeMillis) { check(targetTimeMillis > 0); this.targetTimeMillis = targetTimeMillis; } @JmxAttribute public int getGcTimeMillis() { return gcTimeMillis; } @JmxAttribute public void setGcTimeMillis(int gcTimeMillis) { check(gcTimeMillis > 0); this.gcTimeMillis = gcTimeMillis; } @JmxAttribute public double getThrottlingDecrease() { return throttlingDecrease; } @JmxAttribute public void setThrottlingDecrease(double throttlingDecrease) { check(throttlingDecrease >= 0.0 && throttlingDecrease <= 1.0); this.throttlingDecrease = throttlingDecrease; } @JmxAttribute public int getSmoothingWindow() { return smoothingWindow; } @JmxAttribute public void setSmoothingWindow(int smoothingWindow) { check(smoothingWindow > 0); this.smoothingWindow = smoothingWindow; } @JmxAttribute(reducer = JmxReducerSum.class) public long getTotalRequests() { return infoTotalRequests; } @JmxAttribute(reducer = JmxReducerSum.class) public long getTotalRequestsThrottled() { return infoTotalRequestsThrottled; } @JmxAttribute(reducer = JmxReducerSum.class) public long getTotalProcessed() { return infoTotalRequests - infoTotalRequestsThrottled; } @JmxAttribute(reducer = JmxReducerSum.class) public long getTotalTimeMillis() { return infoTotalTimeMillis; } @JmxAttribute(reducer = JmxReducerSum.class) public long getRounds() { return infoRounds; } @JmxAttribute(reducer = JmxReducerSum.class) public long getRoundsZeroThrottling() { return infoRoundsZeroThrottling; } @JmxAttribute(reducer = JmxReducerSum.class) public long getRoundsExceededTargetTime() { return infoRoundsExceededTargetTime; } @JmxAttribute(reducer = JmxReducerSum.class) public long getInfoRoundsGc() { return infoRoundsGc; } @JmxAttribute public double getThrottling() { return throttling; } @JmxOperation public void resetInfo() { infoTotalRequests = 0; infoTotalRequestsThrottled = 0; infoTotalTimeMillis = 0; infoRounds = 0; infoRoundsZeroThrottling = 0; infoRoundsExceededTargetTime = 0; } @Override public String toString() { return String.format("{throttling:%2d%% avgKps=%-4d avgThrottling=%2d%% requests=%-4d throttled=%-4d rounds=%-3d zero=%-3d >targetTime=%-3d}", (int) (throttling * 100), (int) getAvgKeysPerSecond(), (int) (getAvgThrottling() * 100), infoTotalRequests, infoTotalRequestsThrottled, infoRounds, infoRoundsZeroThrottling, infoRoundsExceededTargetTime); } void setEventloop(Eventloop eventloop) { this.eventloop = eventloop; } @Override public Eventloop getEventloop() { return eventloop; } }