/*
* Eduardo, an IRC bot framework
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) Eduardo team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.eduardo.service.throttle;
import com.google.common.base.Ticker;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.sk89q.eduardo.model.context.Context;
import com.sk89q.eduardo.model.context.Users;
import com.sk89q.eduardo.util.config.Config;
import org.isomorphism.util.TokenBucket;
import org.isomorphism.util.TokenBuckets;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@Singleton
public class RateLimiter {
private static final double FACTOR = 100;
private static final String GLOBAL_BUCKET = "global";
private static final String ROOM_BUCKET = "per-room";
private static final String HOST_BUCKET = "per-host";
private final Ticker ticker = Ticker.systemTicker();
private final Supplier<Config> thisConfig;
private final TokenBucket global;
private final LoadingCache<String, TokenBucket> perRoom;
private final LoadingCache<String, TokenBucket> perHost;
@Inject
public RateLimiter(Config config) {
this.thisConfig = config.configAt("rate-limiter");
global = createTokenBucket(GLOBAL_BUCKET);
perRoom = createTokenBucketCache(ROOM_BUCKET);
perHost = createTokenBucketCache(HOST_BUCKET);
}
public synchronized boolean tryConsume(Context context, double tokens) {
long tokensLong = applyFactor(tokens);
if (context.getUser() != null) {
TokenBucket bucket = perHost.getUnchecked(Users.getUserMask(context.getUser()));
if (!bucket.tryConsume(tokensLong)) {
return false;
}
}
if (context.getRoom() != null) {
TokenBucket bucket = perRoom.getUnchecked(context.getRoom().getId().toLowerCase());
if (!bucket.tryConsume(tokensLong)) {
return false;
}
}
return global.tryConsume(tokensLong);
}
private long applyFactor(double tokens) {
return (long) (tokens * FACTOR);
}
private TokenBucket createTokenBucket(String key) {
Config bucket = thisConfig.get().toObject().getConfig(key);
return TokenBuckets.builder()
.withCapacity(applyFactor(bucket.getDouble("capacity", 15)))
.withRefillStrategy(new FixedTimeRefillStrategy(
ticker,
applyFactor(bucket.getDouble("refill", 1)),
(long) (bucket.getDouble("wait", 3) * 1000), TimeUnit.MILLISECONDS))
.build();
}
private <V> LoadingCache<V, TokenBucket> createTokenBucketCache(String configKey) {
return CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(
new CacheLoader<V, TokenBucket>() {
@Override
public TokenBucket load(@Nullable V key) {
return createTokenBucket(configKey);
}
});
}
}