Skip to content

Commit

Permalink
feat: implemented rates caching
Browse files Browse the repository at this point in the history
  • Loading branch information
RoinujNosde committed Jun 20, 2024
1 parent c829820 commit 03f006d
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import net.epconsortium.cryptomarket.CryptoMarket;
import net.epconsortium.cryptomarket.conversation.prompt.ExitWarningPrompt;
import net.epconsortium.cryptomarket.database.dao.Investor;
import net.epconsortium.cryptomarket.finances.Negotiation;
import net.epconsortium.cryptomarket.util.Configuration;
import org.bukkit.ChatColor;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.ConversationFactory;
Expand All @@ -13,14 +15,10 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import net.epconsortium.cryptomarket.database.dao.InvestorDao;
import net.epconsortium.cryptomarket.database.dao.Investor;
import org.bukkit.ChatColor;
import org.bukkit.scheduler.BukkitRunnable;

/**
* Class used to have a conversation with the player
*
*
* @author roinujnosde
*/
public class NegotiationConversation implements ConversationPrefix {
Expand Down
140 changes: 140 additions & 0 deletions src/main/java/net/epconsortium/cryptomarket/finances/CachedRates.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package net.epconsortium.cryptomarket.finances;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import net.epconsortium.cryptomarket.CryptoMarket;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

public class CachedRates {

private final CryptoMarket plugin;
private final File cacheFolder;
private final Gson gson = new GsonBuilder().setPrettyPrinting()
.registerTypeAdapter(LocalDate.class, new LocalDateAdapter()).registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()).create();
private static final Type MAP_TYPE = TypeToken.getParameterized(Map.class, LocalDate.class, CachedExchangeRate.class).getType();


public CachedRates(CryptoMarket plugin) {
this.plugin = plugin;
cacheFolder = new File(plugin.getDataFolder(), "cache");
//noinspection ResultOfMethodCallIgnored
cacheFolder.mkdir();
}

public boolean isCached(String coin) {
return getCacheFile(coin).exists();
}

public Map<LocalDate, CachedExchangeRate> getRates(String coin) {
if (isCached(coin)) {
try (JsonReader reader = new JsonReader(new FileReader(getCacheFile(coin)))) {
Map<LocalDate, CachedExchangeRate> map = gson.fromJson(reader, MAP_TYPE);
if (map != null) {
return map;
}
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "There was an error while reading the cache", ex);
}
}
return new HashMap<>();
}

public @Nullable CachedExchangeRate getCachedExchangeRate(String coin, LocalDate date) {
return getRates(coin).get(date);
}

public void saveRates(String coin, Map<LocalDate, BigDecimal> rates) {
Map<LocalDate, CachedExchangeRate> currentCache = getRates(coin);
LocalDateTime now = LocalDateTime.now();
for (Map.Entry<LocalDate, BigDecimal> entry : rates.entrySet()) {
currentCache.put(entry.getKey(), new CachedExchangeRate(entry.getValue(), now));
}
try (FileWriter fw = new FileWriter(getCacheFile(coin))) {
gson.toJson(currentCache, MAP_TYPE, fw);
} catch (IOException ex) {
plugin.getLogger().log(Level.SEVERE, "There was an error while saving the cache", ex);
}
}

private File getCacheFile(String coin) {
return new File(cacheFolder, String.format("%s.json", coin.toLowerCase()));
}

public static class CachedExchangeRate {

private final BigDecimal value;
private final LocalDateTime lastUpdated;

public CachedExchangeRate(BigDecimal value, LocalDateTime lastUpdated) {
this.value = value;
this.lastUpdated = lastUpdated;
}

public boolean isFresh(int updateInterval) {
return lastUpdated.plusMinutes(updateInterval).isAfter(LocalDateTime.now());
}

public BigDecimal getCoinValue() {
return value;
}
}

static class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {

@Override
public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException {
if (localDateTime == null) {
jsonWriter.nullValue();
} else {
jsonWriter.value(localDateTime.toString());
}
}

@Override
public LocalDateTime read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
return null;
}
return LocalDateTime.parse(jsonReader.nextString());
}
}

static class LocalDateAdapter extends TypeAdapter<LocalDate> {

@Override
public void write(JsonWriter jsonWriter, LocalDate localDate) throws IOException {
if (localDate == null) {
jsonWriter.nullValue();
} else {
jsonWriter.value(localDate.toString());
}
}

@Override
public LocalDate read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
return null;
}
return LocalDate.parse(jsonReader.nextString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.epconsortium.cryptomarket.CryptoMarket;
import net.epconsortium.cryptomarket.finances.CachedRates.CachedExchangeRate;
import net.epconsortium.cryptomarket.util.Configuration;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -34,8 +35,7 @@ public class ExchangeRates {
private static ExchangeRates instance;
private final CryptoMarket plugin;
private final Configuration config;

private static int requests = 0;
private final CachedRates cachedRates;
private static final String USER_AGENT = "Mozilla/5.0";
private static final Map<LocalDate, ExchangeRate> RATES = new HashMap<>();
private static LocalDate lastCurrentDay = ZonedDateTime.now(UTC).toLocalDate();
Expand All @@ -46,6 +46,18 @@ public class ExchangeRates {
private ExchangeRates(CryptoMarket plugin) {
this.plugin = Objects.requireNonNull(plugin);
config = new Configuration(plugin);
cachedRates = new CachedRates(plugin);
config.getCoins().forEach(coin -> {
for (Map.Entry<LocalDate, CachedExchangeRate> entry : cachedRates.getRates(coin).entrySet()) {
LocalDate date = entry.getKey();
ExchangeRate er = getExchangeRate(date);
if (er == null) {
er = new ExchangeRate();
}
er.update(coin, entry.getValue().getCoinValue());
RATES.put(date, er);
}
});
}

public static ExchangeRates getInstance(@NotNull CryptoMarket plugin) {
Expand All @@ -64,8 +76,8 @@ public void updateAll() {
setDailyError(false);
//Updating
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
updateCurrentExchangeRate0();
updateDailyRates0();
updateCurrentExchangeRate0();
});
}

Expand Down Expand Up @@ -159,27 +171,6 @@ private static void setCurrentError(boolean error) {
currentError = error;
}

/**
* Causes the Async Thread to sleep so that the 5 requests per minute limit
* (of the AlphaVantage API) is not trespassed
*/
@SuppressWarnings("SleepWhileHoldingLock")
private static synchronized void awaitServerLimit() {
if (requests < 5) {
//Requests are less than 5, so you don't need to sleep
requests++;
return;
}
//Requests exceeded, sleeping...
try {
Thread.sleep(70 * 1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
//Resetting requests count
requests = 1;
}

/**
* Returns the connection response in a JsonObject
*
Expand Down Expand Up @@ -238,7 +229,9 @@ private URL getCurrencyDailyUrl(String coin) throws MalformedURLException {
private void updateDailyRates0() {
HashMap<String, Boolean> errors = new HashMap<>();
for (String coin : config.getCoins()) {
awaitServerLimit();
if (cachedRates.isCached(coin)) {
continue;
}
try {
URL url = getCurrencyDailyUrl(coin);
HttpURLConnection connection = openHttpConnection(url);
Expand All @@ -248,13 +241,12 @@ private void updateDailyRates0() {
JsonObject json = extractJsonFrom(connection);
JsonObject jo = json.getAsJsonObject("Time Series (Digital Currency Daily)");
if (jo == null) {
plugin.getLogger().warning("API limit exceeded. Wait a few minutes and try again.");
CryptoMarket.debug(json.toString());
errors.put(coin, true);
continue;
}
Set<Map.Entry<String, JsonElement>> entries = jo.entrySet();

Map<LocalDate, BigDecimal> cache = new HashMap<>();
entries.forEach(entry -> {
ZonedDateTime date = ZonedDateTime.of(LocalDate.parse(entry.getKey()), LocalTime.MIN, UTC);
BigDecimal value = entry.getValue().getAsJsonObject().get("4a. close ("
Expand All @@ -265,7 +257,9 @@ private void updateDailyRates0() {
}
er.update(coin, value);
RATES.put(date.toLocalDate(), er);
cache.put(date.toLocalDate(), value);
});
cachedRates.saveRates(coin, cache);
} else {
errors.put(coin, true);
}
Expand All @@ -285,7 +279,10 @@ private void updateDailyRates0() {
private void updateCurrentExchangeRate0() {
boolean error = false;
for (String coin : config.getCoins()) {
awaitServerLimit();
CachedExchangeRate cached = cachedRates.getCachedExchangeRate(coin, LocalDate.now());
if (cached != null && cached.isFresh(config.getIntervalExchangeRatesUpdateInMinutes())) {
continue;
}
try {
HttpURLConnection connection = openHttpConnection(getExchangeRateUrl(coin));
int responseCode = connection.getResponseCode();
Expand All @@ -294,7 +291,6 @@ private void updateCurrentExchangeRate0() {

JsonElement realtimeCurrencyExchangeRate = json.get("Realtime Currency Exchange Rate");
if (realtimeCurrencyExchangeRate == null) {
plugin.getLogger().warning("API limit exceeded. Wait a few minutes and try again.");
CryptoMarket.debug(json.toString());
error = true;
continue;
Expand All @@ -310,6 +306,7 @@ private void updateCurrentExchangeRate0() {

er.update(coin, exchangeRate);
RATES.put(date, er);
cachedRates.saveRates(coin, Collections.singletonMap(date, exchangeRate));
} else {
error = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,11 @@ public boolean isAsync() {

@Override
public long getDelay() {
LocalDateTime tomorrow = LocalDate.now().plusDays(1).atTime(0, 1);
long delay = LocalDateTime.now().until(tomorrow, ChronoUnit.SECONDS) * 20;

return delay % getPeriod();
return 0;
}

@Override
public long getPeriod() {
return configuration.getIntervalExchangeRatesUpdateInTicks();
return 60 * 20; //every minute
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package net.epconsortium.cryptomarket.util;

import net.epconsortium.cryptomarket.CryptoMarket;

import org.bukkit.ChatColor;
import org.bukkit.configuration.file.FileConfiguration;

Expand Down Expand Up @@ -104,25 +103,24 @@ public String getMySQLPassword() {
}

/**
* Returns the interval to update the Exchange Rates in server ticks
* Returns the interval to update the Exchange Rates in minutes
*
* @return the interval
*/
public long getIntervalExchangeRatesUpdateInTicks() {
final int maxRequests = 500;
public int getIntervalExchangeRatesUpdateInMinutes() {
final int maxRequests = 25;
final int dayInMinutes = 1440;
double requests = getCoins().size() * 2;
double requests = getCoins().size();

double interval = getConfig().getInt("update-interval", 60);
double minInterval = dayInMinutes / (maxRequests / requests);

if (minInterval > interval) {
interval = Math.ceil(minInterval);
CryptoMarket.warn("Update interval set to " + interval + ""
+ " to obey the API limit!");
CryptoMarket.warn("Update interval set to " + interval + " to obey the API limit!");
}

return (long) (interval * 60 * 20);
return (int) (interval);
}

/**
Expand All @@ -135,15 +133,6 @@ public long getIntervalSavingInvestorsInTicks() {
return getConfig().getLong("saving-interval", 10) * 60 * 20;
}

/**
* Returns the interval to update the Exchange Rates in milliseconds
*
* @return the interval
*/
public long getIntervalExchangeRatesUpdateInMillis() {
return getIntervalExchangeRatesUpdateInTicks() / 20 * 1000;
}

/**
* Returns the richers update interval in ticks
*
Expand Down
4 changes: 1 addition & 3 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#API KEY for accessing the cryptocoins values, USE THE DEFAULT KEY FOR TESTING ONLY
api-key: 99X0JFXBLX2YRZA7
#The interval to update the Exchange Rates (in minutes)
#(Please note that the API has a limit of 500 requests per day,
#(Please note that the API has a limit of 25 requests per day,
#so if you set an interval that trespasses this limit, the plugin will automatically choose one)
update-interval: 60
#The interval to save the investors data (in minutes)
Expand All @@ -22,8 +22,6 @@ physical-currency: USD
#Coins that the plugin will work with (you can find a list of valid coin in the digital_currency_list.csv file inside the JAR)
coins:
- BTC
- LTC
- ETH
#The main menu of the plugin, accessed using /cryptomarket
menu:
#Name of the menu
Expand Down

0 comments on commit 03f006d

Please sign in to comment.