diff --git a/Plugins/Mineplex.Core/src/mineplex/core/brawl/fountain/Fountain.java b/Plugins/Mineplex.Core/src/mineplex/core/brawl/fountain/Fountain.java index 81c03a54a..2cbe3c2c6 100644 --- a/Plugins/Mineplex.Core/src/mineplex/core/brawl/fountain/Fountain.java +++ b/Plugins/Mineplex.Core/src/mineplex/core/brawl/fountain/Fountain.java @@ -8,6 +8,7 @@ import mineplex.core.hologram.Hologram; import mineplex.core.hologram.HologramManager; import mineplex.core.stats.StatsManager; import mineplex.serverdata.redis.counter.Counter; +import mineplex.serverdata.redis.counter.GoalCounter; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -25,7 +26,7 @@ public class Fountain private final Location _location; private Location _lavaLocation; private final Hologram _hologram; - private final Counter _counter; + private final GoalCounter _counter; private final FountainShop _shop; @@ -41,7 +42,7 @@ public class Fountain _location = location; _lavaLocation = lavaLocation; _hologram = new Hologram(hologramManager, location.clone().add(0, 2, 0), name).start(); - _counter = new Counter(dataKey, goal); + _counter = new GoalCounter(dataKey, 5000, goal); _shop = new FountainShop(this, fountainManager, clientManager, donationManager); @@ -56,19 +57,13 @@ public class Fountain protected void updateCount() { - // Make sure we only update if it has been 5 seconds since the last update - // Players incrementing the counter will automatically cause it to update - if (System.currentTimeMillis() - _counter.getCacheLastUpdated() > 5000) - { - _counter.updateCount(); - updateHologram(); - } + updateHologram(); } public void increment(Player player, long amount) { _statsManager.incrementStat(player, "Global.Fountain." + getDataKey(), amount); - _counter.increment(amount); + _counter.addAndGet(amount); updateHologram(); } diff --git a/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/Counter.java b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/Counter.java index a7cad37f2..97b3e841a 100644 --- a/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/Counter.java +++ b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/Counter.java @@ -3,73 +3,100 @@ package mineplex.serverdata.redis.counter; import java.util.concurrent.atomic.AtomicLong; /** - * A counter represents an atomic number that is trying to hit a goal. Once the counter - * reaches it's goal you can either reset it or let it stay completed for an indefinite time. + * A counter represents an incrementing atomic number that is stored and updated through redis. This allows for + * multiple servers to share and update the same counter * * @author Shaun Bennett */ public class Counter { - // Cached count for the fountain - private AtomicLong _count = new AtomicLong(0); + // Cached count of the counter + private final AtomicLong _count = new AtomicLong(0); // The System.currentTimeMillis() when cached count was last updated - private long _cacheLastUpdated; - // The goal that this fountain is trying to reach - private final long _goal; + private volatile long _lastUpdated; + // The maximum time to wait before syncing the count with repository + private final long _syncTime; // The unique key to reference this counter private final String _dataKey; // Redis repository to store the count - private CounterRedisRepository _redisRepository; + private final CounterRedisRepository _redisRepository; - public Counter(String dataKey, long goal) + public Counter(String dataKey, long syncTime) { - _goal = goal; _dataKey = dataKey; + _syncTime = syncTime; _redisRepository = new CounterRedisRepository(dataKey); - updateCount(); - } - - public void updateCount() - { updateCount(_redisRepository.getCount()); } - protected void updateCount(long newCount) + public Counter(String dataKey) { - _count.set(newCount); - _cacheLastUpdated = System.currentTimeMillis(); + this(dataKey, 5000); // 5 seconds } - public void increment(long amount) + /** + * Add a value to the counter and return the new counter value. This method is thread-safe and interacts + * directly with the atomic value stored in redis. The value returned from redis is then returned + * + * addAndGet will also update the cached counter value so we don't need to make extra trips to redis + * + * @param amount the amount to add to the counter + * @return the updated value of the counter from redis repository + */ + public long addAndGet(long amount) { long newCount = _redisRepository.incrementCount(amount); updateCount(newCount); + return newCount; } + /** + * Get the latest count of the counter. This will use the last cached value for the counter, or if + * the last cached count hasn't been updated in the {@link #_syncTime} period it will pull the latest + * count from the redis repository and use that value. + * + * @return The counter count + */ public long getCount() { + if (System.currentTimeMillis() - _lastUpdated > _syncTime) + { + updateCount(_redisRepository.getCount()); + } + return _count.get(); } - public long getGoal() + /** + * Reset the counter back to 0. This will take a maximum time of {@link #_syncTime} to propagate + * across all instances of this counter. Immediately updates the redis repository. + * + * @return The value of the counter before it was reset + */ + public long reset() { - return _goal; - } - - public double getFillPercent() - { - return Math.min(1, (((double) getCount()) / _goal)); + updateCount(0); + return _redisRepository.reset(); } + /** + * Get the data key for this counter. The data key is used as the redis repository key + * @return The data key for this counter + */ public String getDataKey() { return _dataKey; } - public long getCacheLastUpdated() + /** + * Update the cached count with a new value + * @param newCount updated count + */ + protected void updateCount(long newCount) { - return _cacheLastUpdated; + _count.set(newCount); + _lastUpdated = System.currentTimeMillis(); } } diff --git a/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/CounterRedisRepository.java b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/CounterRedisRepository.java index 478e2e7f3..96a1cb0e3 100644 --- a/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/CounterRedisRepository.java +++ b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/CounterRedisRepository.java @@ -42,10 +42,10 @@ public class CounterRedisRepository extends RedisRepository } /** - * Increment the current fountain count by {@code increment} and then return the latest - * count of the fountain. This is handled in an atomic process through redis - * @param increment Amount to increment the fountain by - * @return The new count for the fountain + * Increment the current count by {@code increment} and then return the latest + * count from redis. This is handled in an atomic process + * @param increment Amount to increment counter by + * @return The updated count from redis */ public long incrementCount(long increment) { @@ -59,6 +59,26 @@ public class CounterRedisRepository extends RedisRepository return count; } + /** + * Reset the counter back to 0 + * @return the value of the counter before it was reset + */ + public long reset() + { + long count = -1; + + try (Jedis jedis = getResource(true)) + { + count = Long.parseLong(jedis.getSet(getKey(), "0")); + } + + return count; + } + + /** + * Get the key for this counter + * @return The key is used to store the value in redis + */ private String getKey() { return getKey(_dataKey); diff --git a/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounter.java b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounter.java new file mode 100644 index 000000000..fe85c6536 --- /dev/null +++ b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounter.java @@ -0,0 +1,101 @@ +package mineplex.serverdata.redis.counter; + +import java.util.ArrayList; +import java.util.List; + +/** + * A redis counter that is aiming to reach a goal + * + * @author Shaun Bennett + */ +public class GoalCounter extends Counter +{ + // Has the goal been completed? + private boolean _completed; + // The goal we are trying to reach + private long _goal; + + private List _listeners; + + public GoalCounter(String dataKey, long syncTime, long goal) + { + super(dataKey, syncTime); + + _completed = false; + _goal = goal; + + _listeners = new ArrayList<>(); + + update(); + } + + /** + * Get the progress towards the goal as a percent ranging from 0 to 1. + * @return the percent progress towards goal + */ + public double getFillPercent() + { + return Math.min(1, (((double) getCount()) / _goal)); + } + + /** + * Has the goal been completed? + * @return + */ + public boolean isCompleted() + { + return _completed; + } + + /** + * Get the goal for this GoalCounter + * @return the goal of this counter + */ + public long getGoal() + { + return _goal; + } + + /** + * Add a listener to this GoalCounter + * @param listener the listener to be added + */ + public void addListener(GoalCounterListener listener) + { + _listeners.add(listener); + } + + /** + * Clear all the listeners + */ + public void clearListeners() + { + _listeners.clear(); + } + + /** + * Update {@link #_completed} and notify listeners if it has completed + */ + private void update() + { + boolean updatedValue = getCount() > _goal; + + if (updatedValue != _completed) + { + _completed = updatedValue; + _listeners.forEach(listener -> { + if (_completed) listener.onGoalComplete(this); + else listener.onGoalReset(this); + }); + } + } + + @Override + protected void updateCount(long newCount) + { + super.updateCount(newCount); + + update(); + } + +} diff --git a/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounterListener.java b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounterListener.java new file mode 100644 index 000000000..3560342de --- /dev/null +++ b/Plugins/Mineplex.ServerData/src/mineplex/serverdata/redis/counter/GoalCounterListener.java @@ -0,0 +1,11 @@ +package mineplex.serverdata.redis.counter; + +/** + * @author Shaun Bennett + */ +public interface GoalCounterListener +{ + public void onGoalComplete(GoalCounter counter); + + public void onGoalReset(GoalCounter counter); +}