From a0d0740f1002b0f49f8ac3eaad2273e93d299f6e Mon Sep 17 00:00:00 2001 From: Minimons Date: Mon, 13 Jul 2026 12:41:11 +0200 Subject: [PATCH] 28: Add simple ObjectCache and cached SolanaBlockChain decorator --- .../impl/ref/JupiterPerpsAlarmImpl.tjava | 13 +- .../r35157/libs/objcache/ObjectCache.tjava | 24 ++ .../r35157/libs/objcache/ObjectCacheKey.tjava | 62 +++++ .../objcache/impl/ref/ObjectCacheEntry.tjava | 32 +++ .../objcache/impl/ref/ObjectCacheImpl.tjava | 76 ++++++ .../impl/cached/CachedSolanaBlockChain.tjava | 233 ++++++++++++++++++ 6 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 src/main/tjava/com/r35157/libs/objcache/ObjectCache.tjava create mode 100644 src/main/tjava/com/r35157/libs/objcache/ObjectCacheKey.tjava create mode 100644 src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheEntry.tjava create mode 100644 src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheImpl.tjava create mode 100644 src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava diff --git a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava index 36ee60a..7d5dae4 100644 --- a/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava +++ b/src/main/tjava/com/r35157/jupiterperpsalarm/impl/ref/JupiterPerpsAlarmImpl.tjava @@ -2,7 +2,10 @@ package com.r35157.jupiterperpsalarm.impl.ref; import com.r35157.libs.jupiter.perps.JupiterPerpsService; import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl; +import com.r35157.libs.objcache.ObjectCache; +import com.r35157.libs.objcache.impl.ref.ObjectCacheImpl; import com.r35157.libs.solana.SolanaBlockChain; +import com.r35157.libs.solana.impl.cached.CachedSolanaBlockChain; import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl; import java.net.URI; @@ -46,8 +49,14 @@ public final class JupiterPerpsAlarmImpl { alarmConfiguration.variables() ); - System.out.print("Initializing dependecies... "); - SolanaBlockChain solanaBlockChain = new SolanaBlockChainImpl(); + System.out.print("Initializing dependencies... "); + ObjectCache objectCache = new ObjectCacheImpl(); + SolanaBlockChain realSolanaBlockChain = new SolanaBlockChainImpl(); + SolanaBlockChain solanaBlockChain = new CachedSolanaBlockChain( + realSolanaBlockChain, + objectCache + ); + JupiterPerpsService jupiterPerpsService = new AnchorIdlJupiterPerpsServiceImpl(solanaBlockChain); JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher = new JupiterPerpsEntryPriceVariableRefresher(alarmConfiguration.variables(), jupiterPerpsService); diff --git a/src/main/tjava/com/r35157/libs/objcache/ObjectCache.tjava b/src/main/tjava/com/r35157/libs/objcache/ObjectCache.tjava new file mode 100644 index 0000000..7715f72 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/objcache/ObjectCache.tjava @@ -0,0 +1,24 @@ +package com.r35157.libs.objcache; + +import org.jetbrains.annotations.Nullable; + +public interface ObjectCache { + + void put( + ObjectCacheKey key, + Object object, + long ttlMillis + ); + + @Nullable + Object get( + ObjectCacheKey key + ); + + void remove( + ObjectCacheKey key + ); + + void clear(); + +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/objcache/ObjectCacheKey.tjava b/src/main/tjava/com/r35157/libs/objcache/ObjectCacheKey.tjava new file mode 100644 index 0000000..9e374d2 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/objcache/ObjectCacheKey.tjava @@ -0,0 +1,62 @@ +package com.r35157.libs.objcache; + +import java.util.List; +import java.util.Objects; + +public final class ObjectCacheKey { + + public ObjectCacheKey( + String owner, + String operation, + Object... parameters + ) { + this.owner = Objects.requireNonNull(owner); + this.operation = Objects.requireNonNull(operation); + this.parameters = List.copyOf(List.of(parameters)); + } + + public String getOwner() { + return owner; + } + + public String getOperation() { + return operation; + } + + public List getParameters() { + return parameters; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof ObjectCacheKey other)) { + return false; + } + + return owner.equals(other.owner) + && operation.equals(other.operation) + && parameters.equals(other.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(owner, operation, parameters); + } + + @Override + public String toString() { + return "ObjectCacheKey{" + + "owner='" + owner + '\'' + + ", operation='" + operation + '\'' + + ", parameters=" + parameters + + '}'; + } + + private final String owner; + private final String operation; + private final List parameters; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheEntry.tjava b/src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheEntry.tjava new file mode 100644 index 0000000..c02a9a4 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheEntry.tjava @@ -0,0 +1,32 @@ +package com.r35157.libs.objcache.impl.ref; + +import java.util.Objects; + +public final class ObjectCacheEntry { + + public ObjectCacheEntry( + Object object, + long insertedAtMillis, + long ttlMillis + ) { + this.object = Objects.requireNonNull(object); + this.insertedAtMillis = insertedAtMillis; + this.ttlMillis = ttlMillis; + } + + public Object getObject() { + return object; + } + + public long getAgeMillis() { + return System.currentTimeMillis() - insertedAtMillis; + } + + public boolean isExpired() { + return getAgeMillis() >= ttlMillis; + } + + private final Object object; + private final long insertedAtMillis; + private final long ttlMillis; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheImpl.tjava b/src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheImpl.tjava new file mode 100644 index 0000000..080d76f --- /dev/null +++ b/src/main/tjava/com/r35157/libs/objcache/impl/ref/ObjectCacheImpl.tjava @@ -0,0 +1,76 @@ +package com.r35157.libs.objcache.impl.ref; + +import com.r35157.libs.objcache.ObjectCache; +import com.r35157.libs.objcache.ObjectCacheKey; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public final class ObjectCacheImpl implements ObjectCache { + + @Override + public void put( + ObjectCacheKey key, + Object object, + long ttlMillis + ) { + Objects.requireNonNull(key); + Objects.requireNonNull(object); + + if (ttlMillis < 0) { + throw new IllegalArgumentException( + "ttlMillis must be zero or greater" + ); + } + + objects.put( + key, + new ObjectCacheEntry( + object, + System.currentTimeMillis(), + ttlMillis + ) + ); + } + + @Override + public @Nullable Object get( + ObjectCacheKey key + ) { + Objects.requireNonNull(key); + + ObjectCacheEntry entry = objects.get(key); + + if (entry == null) { + return null; + } + + if (entry.isExpired()) { + objects.remove(key, entry); + return null; + } + + return entry.getObject(); + } + + @Override + public void remove( + ObjectCacheKey key + ) { + Objects.requireNonNull(key); + objects.remove(key); + } + + @Override + public void clear() { + objects.clear(); + } + + private final ConcurrentMap< + ObjectCacheKey, + ObjectCacheEntry + > objects = new ConcurrentHashMap<>(); + +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava b/src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava new file mode 100644 index 0000000..69340b3 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava @@ -0,0 +1,233 @@ +package com.r35157.libs.solana.impl.cached; + +import com.r35157.libs.objcache.ObjectCache; +import com.r35157.libs.objcache.ObjectCacheKey; +import com.r35157.libs.solana.SPLTokenHolding; +import com.r35157.libs.solana.SPLTokenSupply; +import com.r35157.libs.solana.SolanaAccountInfo; +import com.r35157.libs.solana.SolanaBlockChain; +import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter; +import com.r35157.libs.solana.SolanaProgramAddressSeed; +import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress; +import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram; +import com.r35157.libs.valuetypes.basic.MoneyAmount; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class CachedSolanaBlockChain implements SolanaBlockChain { + + public CachedSolanaBlockChain( + SolanaBlockChain delegate, + ObjectCache objectCache + ) { + this.delegate = Objects.requireNonNull(delegate); + this.objectCache = Objects.requireNonNull(objectCache); + } + + @Override + public ΩSolanaAmountΩ getBalanceInSolana( + ΩSolanaAddressΩ address + ) throws IOException, InterruptedException { + return delegate.getBalanceInSolana(address); + } + + @Override + public ΩlamportsΩ getBalanceInLamport( + ΩSolanaAddressΩ address + ) throws IOException, InterruptedException { + return delegate.getBalanceInLamport(address); + } + + @Override + public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings( + ΩSolanaAddressΩ ownerAddress, + SolanaSPLTokenProgram splProgramId + ) throws IOException, InterruptedException { + long t0 = System.currentTimeMillis(); + Map<ΩSPLMintAddressΩ, SPLTokenHolding> result = delegate.getSPLTokenHoldings( + ownerAddress, + splProgramId + ); + System.out.println("**F** LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms"); + return result; + } + + @Override + public Set<ΩSolanaNFTAddressΩ> getSolanaNFTCandidateAddresses( + ΩSolanaAddressΩ ownerAddress, + SolanaSPLTokenProgram splProgram + ) throws IOException, InterruptedException { + long t0 = System.currentTimeMillis(); + Set<ΩSolanaNFTAddressΩ> result = delegate.getSolanaNFTCandidateAddresses( + ownerAddress, + splProgram + ); + System.out.println("@@@E@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms"); + return result; + } + + @Override + public SolanaProgramDerivedAddress findProgramAddress( + ΩSolanaProgramIdΩ programId, + List seeds + ) { + long t0 = System.currentTimeMillis(); + SolanaProgramDerivedAddress addr = delegate.findProgramAddress( + programId, + seeds + ); + System.out.println("@@@D@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms"); + return addr; + } + + @Override + public @Nullable SolanaAccountInfo getAccountInfo( + ΩSolanaAddressΩ accountAddress + ) throws IOException, InterruptedException { + long t0 = System.currentTimeMillis(); + SolanaAccountInfo result; + + ObjectCacheKey key = new ObjectCacheKey( + SolanaBlockChain.class.getName(), + "getAccountInfo", + accountAddress + ); + + Object cachedObject = objectCache.get(key); + + result = (cachedObject != null) + ? castAccountInfo(cachedObject) + : loadAccountInfo( + key, + accountAddress + ); + + System.out.println("@@@C@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms"); + return result; + } + + @Override + public ΩSolanaAddressΩ encodeSolanaAddress( + byte[] addressBytes + ) { + return delegate.encodeSolanaAddress(addressBytes); + } + + @Override + public SPLTokenSupply getSPLTokenSupply( + ΩSPLMintAddressΩ mintAddress, + SolanaSPLTokenProgram splProgram + ) throws IOException, InterruptedException { + long t0 = System.currentTimeMillis(); + SPLTokenSupply supply = delegate.getSPLTokenSupply( + mintAddress, + splProgram + ); + System.out.println("@@@B@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms"); + return supply; + } + + @Override + public Set getProgramAccounts( + ΩSolanaProgramIdΩ programId, + Set filters + ) throws IOException, InterruptedException { + long t0 = System.currentTimeMillis(); + Set result; + Set copiedFilters = + Set.copyOf(filters); + + ObjectCacheKey key = new ObjectCacheKey( + SolanaBlockChain.class.getName(), + "getProgramAccounts", + programId, + copiedFilters + ); + + Object cachedObject = objectCache.get(key); + + result = (cachedObject != null) + ? castProgramAccounts(cachedObject) + : loadProgramAccounts( + key, + programId, + copiedFilters + ); + + System.out.println("@@@A@@@ LOOKUP TIME: " + (System.currentTimeMillis() - t0) + "ms"); + return result; + } + + private synchronized Set loadProgramAccounts( + ObjectCacheKey key, + ΩSolanaProgramIdΩ programId, + Set filters + ) throws IOException, InterruptedException { + Object cachedObject = objectCache.get(key); + + if (cachedObject != null) { + return castProgramAccounts(cachedObject); + } + + Set programAccounts = Set.copyOf( + delegate.getProgramAccounts( + programId, + filters + ) + ); + + objectCache.put( + key, + programAccounts, + PROGRAM_ACCOUNTS_TTL_MILLIS + ); + + return programAccounts; + } + + private synchronized SolanaAccountInfo loadAccountInfo( + ObjectCacheKey key, + ΩSolanaAddressΩ accountAddress + ) throws IOException, InterruptedException { + Object cachedObject = objectCache.get(key); + + if (cachedObject != null) { + return castAccountInfo(cachedObject); + } + + SolanaAccountInfo accountInfo = delegate.getAccountInfo(accountAddress); + + objectCache.put( + key, + accountInfo, + ACCOUNT_INFO_TTL_MILLIS + ); + + return accountInfo; + } + + @SuppressWarnings("unchecked") + private Set castProgramAccounts( + Object object + ) { + return (Set) object; + } + + @SuppressWarnings("unchecked") + private SolanaAccountInfo castAccountInfo( + Object object + ) { + return (SolanaAccountInfo) object; + } + + private static final long ACCOUNT_INFO_TTL_MILLIS = 60_000; + private static final long PROGRAM_ACCOUNTS_TTL_MILLIS = 60_000; + + private final SolanaBlockChain delegate; + private final ObjectCache objectCache; +} \ No newline at end of file