Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add caching, network handler improvements, and performance improvements #2

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Project exclude paths
/target/
/target/
.idea/**
out/
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>net.aboodyy</groupId>
<artifactId>localtime-expansion</artifactId>
<version>1.2</version>
<version>1.3</version>

<repositories>
<repository>
Expand Down
115 changes: 78 additions & 37 deletions src/main/java/net/aboodyy/localtime/DateManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,39 @@

package net.aboodyy.localtime;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.logging.Level;

public class DateManager implements Listener {

private final Map<UUID, String> timezones;

DateManager() {
timezones = new HashMap<>();
private final Cache<String, String> cache;
private final ScheduledExecutorService executorService;
private int retryDelay;

public DateManager() {
this.timezones = new ConcurrentHashMap<>();
this.cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.DAYS)
.build();
this.retryDelay = 5; // default to 5 seconds
this.executorService = Executors.newSingleThreadScheduledExecutor();
}

public String getDate(String format, String timezone) {
Expand All @@ -53,55 +64,85 @@ public String getDate(String format, String timezone) {
return dateFormat.format(date);
}

public String getTimeZone(Player player) {
public CompletableFuture<String> getTimeZone(Player player) {
final String FAILED = "[LocalTime] Couldn't get " + player.getName() + "'s timezone. Will use default timezone.";
String timezone = TimeZone.getDefault().getID();

if (timezones.containsKey(player.getUniqueId()))
return timezones.get(player.getUniqueId());
String cachedTimezone = cache.getIfPresent(player.getUniqueId().toString());
if (cachedTimezone != null) {
return CompletableFuture.completedFuture(cachedTimezone);
}

InetSocketAddress address = player.getAddress();
timezones.put(player.getUniqueId(), timezone);
String timezone = timezones.get(player.getUniqueId());
if (timezone != null) {
return CompletableFuture.completedFuture(timezone);
}

InetSocketAddress address = player.getAddress();
if (address == null) {
Bukkit.getLogger().info(FAILED);
return timezone;
PlaceholderAPIPlugin.getInstance().getLogger().log(Level.WARNING, FAILED);
timezone = TimeZone.getDefault().getID();
cache.put(player.getUniqueId().toString(), timezone);
return CompletableFuture.completedFuture(timezone);
}

new BukkitRunnable() {
@Override
public void run() {
String timezone;
final String defaultTimezone = TimeZone.getDefault().getID();

CompletableFuture<String> futureTimezone = CompletableFuture.supplyAsync(() -> {
String result = "undefined";
int retries = 3;

while (retries-- > 0) {
try {
URL api = new URL("https://ipapi.co/" + address.getAddress().getHostAddress() + "/timezone/");
URLConnection connection = api.openConnection();

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
timezone = bufferedReader.readLine();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", "Mozilla/5.0");

try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
result = bufferedReader.readLine();

if (result == null) {
result = "undefined";
} else {
cache.put(player.getUniqueId().toString(), result);
}
break;
}
} catch (Exception e) {
timezone = "undefined";
}

if (timezone.equalsIgnoreCase("undefined")) {
Bukkit.getLogger().info(FAILED);
timezone = TimeZone.getDefault().getID();
result = "undefined";
PlaceholderAPIPlugin.getInstance().getLogger().log(Level.WARNING, "[LocalTime] Exception while getting timezone for player " + player.getName() + ": " + e.getMessage(), e);
try {
Thread.sleep(retryDelay * 1000);
} catch (InterruptedException ignored) {}
}
}

timezones.put(player.getUniqueId(), timezone);
if (result.equalsIgnoreCase("undefined")) {
PlaceholderAPIPlugin.getInstance().getLogger().log(Level.WARNING, FAILED);
result = defaultTimezone;
}
}.runTaskAsynchronously(PlaceholderAPIPlugin.getInstance());

return timezones.get(player.getUniqueId());
timezones.put(player.getUniqueId(), result);
return result;
}, executorService);

futureTimezone.exceptionally(ex -> {
PlaceholderAPIPlugin.getInstance().getLogger().log(Level.WARNING, "[LocalTime] Exception while getting timezone for player " + player.getName() + ": " + ex.getMessage(), ex);
cache.put(player.getUniqueId().toString(), defaultTimezone);
timezones.put(player.getUniqueId(), defaultTimezone);
return defaultTimezone;
});

return futureTimezone;
}

public void clear() {
timezones.clear();
cache.invalidateAll();
}

@SuppressWarnings("unused")
@EventHandler
public void onLeave(PlayerQuitEvent e) {
timezones.remove(e.getPlayer().getUniqueId());
public void shutdown() {
this.executorService.shutdown();
}
}
}
19 changes: 14 additions & 5 deletions src/main/java/net/aboodyy/localtime/LocalTimeExpansion.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@SuppressWarnings("unused")
public class LocalTimeExpansion extends PlaceholderExpansion implements Cacheable, Configurable {
Expand All @@ -43,12 +44,12 @@ public String getIdentifier() {

@Override
public String getAuthor() {
return "aBooDyy";
return "aBooDyy, Opal";
}

@Override
public String getVersion() {
return "1.2";
return "1.3";
}

@Override
Expand All @@ -69,6 +70,7 @@ public Map<String, Object> getDefaults() {
public void clear() {
dateManager.clear();
HandlerList.unregisterAll(dateManager);
dateManager.shutdown();
}

@Override
Expand All @@ -82,7 +84,10 @@ public String onPlaceholderRequest(Player p, String identifier) {
args = identifier.split("time_");
if (args.length < 2) return null;

return dateManager.getDate(args[1], dateManager.getTimeZone(p));
CompletableFuture<String> timezoneFuture = dateManager.getTimeZone(p);
String timezone = timezoneFuture.join();

return dateManager.getDate(args[1], timezone);
}

if (identifier.startsWith("timezone_")) {
Expand All @@ -99,8 +104,12 @@ public String onPlaceholderRequest(Player p, String identifier) {
return dateManager.getDate(format, args[1]);
}

if (identifier.equalsIgnoreCase("time"))
return dateManager.getDate(format, dateManager.getTimeZone(p));
if (identifier.equalsIgnoreCase("time")) {
CompletableFuture<String> timezoneFuture = dateManager.getTimeZone(p);
String timezone = timezoneFuture.join();

return dateManager.getDate(format, timezone);
}

return null;
}
Expand Down