/*******************************************************************************
* Copyright 2016 Klaus Pfeiffer - klaus@allpiper.com
*
* 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 com.jfastnet.peers;
import com.jfastnet.ConfigStateContainer;
import com.jfastnet.messages.Message;
import com.jfastnet.state.ClientState;
import com.jfastnet.state.NetworkQuality;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.function.Consumer;
/** @author Klaus Pfeiffer - klaus@allpiper.com */
@Slf4j
public class CongestionControl<T> {
private final ConfigStateContainer configStateContainer;
private final Consumer<T> packetSender;
private final CongestionControlConfig congestionControlConfig;
private final Queue<DelayedPacket> packetQueue = new ArrayDeque<>();
private long delay;
public CongestionControl(ConfigStateContainer configStateContainer, Consumer<T> packetSender) {
this.configStateContainer = configStateContainer;
this.packetSender = packetSender;
this.congestionControlConfig = configStateContainer.config.getAdditionalConfig(CongestionControlConfig.class);
}
public void send(Message message, T packet) {
if (!configStateContainer.state.isHost() || message.isResendMessage()) {
immediateSend(packet);
return;
}
InetSocketAddress socketAddressRecipient = message.socketAddressRecipient;
// Congestion Control only supported for server right now
ClientState clientState = configStateContainer.state.getClientStates().getBySocketAddress(socketAddressRecipient);
float qualityFactor = 1f;
if (clientState != null) {
NetworkQuality networkQuality = clientState.getNetworkQuality();
qualityFactor = networkQuality.qualityFactor;
}
if (qualityFactor >= congestionControlConfig.immediateSendQualityFactorThreshold && packetQueue.isEmpty()) {
immediateSend(packet);
delay = 0;
} else {
delay = (long) ((1f - qualityFactor) * congestionControlConfig.maximumDelayFactor);
long sendTimeStamp;
DelayedPacket lastDelayedPacket = packetQueue.peek();
if (lastDelayedPacket != null) {
sendTimeStamp = lastDelayedPacket.sendTimeStamp + delay;
} else {
long currentTime = configStateContainer.config.timeProvider.get();
sendTimeStamp = currentTime + delay;
}
packetQueue.add(new DelayedPacket(sendTimeStamp, packet));
}
}
private void immediateSend(T packet) {
packetSender.accept(packet);
}
public void process() {
long currentTime = configStateContainer.config.timeProvider.get();
for (DelayedPacket delayedPacket = packetQueue.peek();
delayedPacket != null && delayedPacket.sendTimeStamp <= currentTime;
delayedPacket = packetQueue.peek()) {
immediateSend(delayedPacket.packet);
packetQueue.poll();
}
}
@Setter
@Getter
@Accessors(chain = true)
public static class CongestionControlConfig {
public int maximumDelayFactor = 1000;
public float immediateSendQualityFactorThreshold = 0.9f;
/** Considered time frame for network quality. */
public int consideredTimeFrameInMs = 3000;
}
private class DelayedPacket {
long sendTimeStamp;
T packet;
DelayedPacket(long sendTimeStamp, T packet) {
this.sendTimeStamp = sendTimeStamp;
this.packet = packet;
}
}
}