Skip to content

Commit

Permalink
Redesign the properties of auto block feature to make it easier to co…
Browse files Browse the repository at this point in the history
…nfigure correctly + Fix the auto block cannot work + Add some tests
  • Loading branch information
JamesChenX committed Dec 18, 2024
1 parent b980316 commit 4e14b53
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 266 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,87 +21,114 @@
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.ObjLongConsumer;
import jakarta.annotation.Nullable;

import lombok.AllArgsConstructor;
import lombok.Data;

import im.turms.server.common.infra.collection.CollectionUtil;
import im.turms.server.common.infra.lang.MathUtil;
import im.turms.server.common.infra.property.env.common.security.AutoBlockItemProperties;
import im.turms.server.common.infra.property.env.common.security.AutoBlockItemProperties.BlockLevel;
import im.turms.server.common.infra.test.VisibleForTesting;
import im.turms.server.common.infra.time.DateTimeUtil;

/**
* @author James Chen
*/
public class AutoBlockManager<T> {

private static final int UNSET_BLOCK_LEVEL = -1;
@VisibleForTesting
public static final int UNSET_BLOCK_LEVEL = -1;

private final ObjLongConsumer<T> onClientBlocked;

private final boolean isEnabled;
private final List<BlockLevel> levels;
private final List<ParsedBlockLevelProperties> blockLevelPropertiesList;
private final int maxLevel;
private final int blockTriggerTimes;

private final ConcurrentHashMap<T, BlockStatus> blockedClientIdToStatus;

public AutoBlockManager(
AutoBlockItemProperties autoBlockProperties,
ObjLongConsumer<T> onClientBlocked) {
this.onClientBlocked = onClientBlocked;
levels = CollectionUtil.toListSupportRandomAccess(autoBlockProperties.getBlockLevels());
isEnabled = autoBlockProperties.isEnabled() && !levels.isEmpty();
blockLevelPropertiesList =
CollectionUtil.transformAsList(autoBlockProperties.getBlockLevels(),
properties -> new ParsedBlockLevelProperties(
properties.getBlockDurationSeconds(),
DateTimeUtil.millisToNanos(
properties.getReduceOneTriggerTimeIntervalMillis()),
properties.getTriggerTimesThreshold()));
isEnabled = autoBlockProperties.isEnabled() && !blockLevelPropertiesList.isEmpty();
if (!isEnabled) {
blockedClientIdToStatus = null;
maxLevel = UNSET_BLOCK_LEVEL;
blockTriggerTimes = 0;
return;
}
blockedClientIdToStatus = new ConcurrentHashMap<>(1024);
maxLevel = levels.size() - 1;
blockTriggerTimes = autoBlockProperties.getBlockTriggerTimes();
maxLevel = blockLevelPropertiesList.size() - 1;
}

public void tryBlockClient(T id) {
@Nullable
public BlockStatus tryBlockClient(T id) {
if (!isEnabled) {
return;
return null;
}
blockedClientIdToStatus.compute(id, (key, status) -> {
return blockedClientIdToStatus.compute(id, (key, status) -> {
long now = System.nanoTime();
if (status == null) {
status = new BlockStatus(UNSET_BLOCK_LEVEL, null, 0, now);
} else {
status = new BlockStatus(
UNSET_BLOCK_LEVEL,
null,
blockLevelPropertiesList.getFirst(),
0,
now);
}
ParsedBlockLevelProperties nextLevelProperties = status.nextLevelProperties;
// If already reaching the max level,
// notify the callback to refresh the block end time.
if (nextLevelProperties == null) {
status.lastBlockTriggerTimeNanos = now;
status.triggerTimes++;
onClientBlocked.accept(id, status.currentLevelProperties.blockDurationSeconds);
return status;
}
// Update status

long previousBlockTriggerTimeNanos = status.lastBlockTriggerTimeNanos;
int reduceOneTriggerTimeIntervalMillis =
status.currentLevelProperties.getReduceOneTriggerTimeIntervalMillis();
int times = status.triggerTimes;
if (reduceOneTriggerTimeIntervalMillis > 0) {
times -= (int) ((status.lastBlockTriggerTimeNanos - previousBlockTriggerTimeNanos)
/ (reduceOneTriggerTimeIntervalMillis * DateTimeUtil.NANOS_PER_MILLI));
if (times < 0) {
times = 0;
status.lastBlockTriggerTimeNanos = now;
// Update the trigger times
long reduceOneTriggerTimeIntervalNanos =
nextLevelProperties.reduceOneTriggerTimeIntervalNanos;
int triggerTimes = status.triggerTimes;
if (reduceOneTriggerTimeIntervalNanos > 0) {
triggerTimes -= MathUtil.toInt(((now - previousBlockTriggerTimeNanos)
/ reduceOneTriggerTimeIntervalNanos));
if (triggerTimes <= 0) {
status.triggerTimes = 1;
} else {
status.triggerTimes = triggerTimes + 1;
}
} else {
status.triggerTimes++;
}
status.triggerTimes = times + 1;
boolean isBlocked = status.currentLevel != UNSET_BLOCK_LEVEL;
if (isBlocked) {
if (status.triggerTimes >= status.currentLevelProperties
.getGoNextLevelTriggerTimes() && status.currentLevel < maxLevel) {
status.currentLevel++;
status.currentLevelProperties = levels.get(status.currentLevel);
status.triggerTimes = 0;
ParsedBlockLevelProperties currentLevelProperties = status.currentLevelProperties;
// Check if the status needs to advance to the next level
if (status.triggerTimes >= nextLevelProperties.triggerTimesThreshold
&& status.currentLevel < maxLevel) {
status.currentLevel++;
status.currentLevelProperties = blockLevelPropertiesList.get(status.currentLevel);
if (status.currentLevel + 1 <= maxLevel) {
status.nextLevelProperties =
blockLevelPropertiesList.get(status.currentLevel + 1);
} else {
status.nextLevelProperties = null;
}
onClientBlocked.accept(id, status.currentLevelProperties.getBlockDurationSeconds());
} else if (status.triggerTimes >= blockTriggerTimes) {
status.currentLevel = 0;
status.currentLevelProperties = levels.getFirst();
status.triggerTimes = 0;
onClientBlocked.accept(id, status.currentLevelProperties.getBlockDurationSeconds());
} else {
status.triggerTimes++;
onClientBlocked.accept(id, status.currentLevelProperties.blockDurationSeconds);
} else if (currentLevelProperties != null) {
// If already blocked,
// notify the callback to refresh the block end time.
onClientBlocked.accept(id, currentLevelProperties.blockDurationSeconds);
}
return status;
});
Expand All @@ -123,24 +150,54 @@ public void evictExpiredBlockedClients() {
.iterator();
while (iterator.hasNext()) {
BlockStatus status = iterator.next();
int reduceOneTriggerTimeInterval =
status.currentLevelProperties.getReduceOneTriggerTimeIntervalMillis();
if (reduceOneTriggerTimeInterval > 0) {
int times = status.triggerTimes - (int) ((now - status.lastBlockTriggerTimeNanos)
/ (reduceOneTriggerTimeInterval * DateTimeUtil.NANOS_PER_MILLI));
if (times <= 0) {
ParsedBlockLevelProperties currentLevelProperties = status.currentLevelProperties;
// If the client has been blocked,
// remove if the block duration has expired.
if (currentLevelProperties != null) {
if ((now - status.lastBlockTriggerTimeNanos)
/ DateTimeUtil.NANOS_PER_SECOND > currentLevelProperties.blockDurationSeconds) {
iterator.remove();
}
continue;
}
// If the client is not blocked,
// remove if the trigger times have expired.
long reduceOneTriggerTimeIntervalNanos =
status.nextLevelProperties.reduceOneTriggerTimeIntervalNanos;
if (reduceOneTriggerTimeIntervalNanos > 0) {
int triggerTimes = status.triggerTimes
- MathUtil.toInt((now - status.lastBlockTriggerTimeNanos)
/ reduceOneTriggerTimeIntervalNanos);
if (triggerTimes <= 0) {
iterator.remove();
}
}
}
}

@AllArgsConstructor
private static class BlockStatus {
@Data
public static class BlockStatus {
private int currentLevel;
private BlockLevel currentLevelProperties;
/**
* Null if the client is not blocked.
*/
@Nullable
private ParsedBlockLevelProperties currentLevelProperties;
/**
* Null if the current level is the max level.
*/
@Nullable
private ParsedBlockLevelProperties nextLevelProperties;
private int triggerTimes;
private long lastBlockTriggerTimeNanos;
}

public record ParsedBlockLevelProperties(
long blockDurationSeconds,
long reduceOneTriggerTimeIntervalNanos,
int triggerTimesThreshold
) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,38 +36,37 @@
@NoArgsConstructor
public class AutoBlockItemProperties {

public static final List<BlockLevel> DEFAULT_BLOCK_LEVELS =
List.of(new BlockLevel(10 * 60, 60 * 1000, 1),
new BlockLevel(30 * 60, 60 * 1000, 1),
new BlockLevel(60 * 60, 60 * 1000, 0));
public static final List<BlockLevelProperties> DEFAULT_BLOCK_LEVELS =
List.of(new BlockLevelProperties(10 * 60, 60 * 1000, 5),
new BlockLevelProperties(30 * 60, 60 * 1000, 1),
new BlockLevelProperties(60 * 60, 60 * 1000, 1));

protected boolean enabled;
protected boolean enabled = true;

@Description("Block the client when the block condition is triggered the times")
@Min(0)
protected int blockTriggerTimes = 5;

protected List<BlockLevel> blockLevels = DEFAULT_BLOCK_LEVELS;
protected List<BlockLevelProperties> blockLevels = DEFAULT_BLOCK_LEVELS;

@AllArgsConstructor
@Builder(toBuilder = true)
@Data
@NoArgsConstructor
public static class BlockLevel {
public static class BlockLevelProperties {

@Description("Block the client for the specified duration in seconds")
@Description("Block the client for the specified duration in seconds. "
+ "After the block duration, the block level will be reset, "
+ "and the client will be unblocked automatically")
@Min(1)
protected long blockDurationSeconds = 10L * 60;

@Description("Reduce the trigger time by 1 when the time passes. "
@Description("If a user's block level is the previous level of this level, "
+ "reduce the trigger time by 1 when the time passes. "
+ "If 0, never reduce the trigger times and "
+ "the block status will remain in the memory until the server is closed")
@Min(0)
protected int reduceOneTriggerTimeIntervalMillis = 60 * 1000;

@Description("Go to the next block level when the block condition is triggered the times")
@Min(0)
protected int goNextLevelTriggerTimes = 1;
@Description("When the block condition is triggered the specified times, advance to this block level")
@Min(1)
protected int triggerTimesThreshold = 1;

}

Expand Down
Loading

0 comments on commit 4e14b53

Please sign in to comment.