28: Add simple ObjectCache and cached SolanaBlockChain decorator

This commit is contained in:
2026-08-05 10:12:38 +02:00
parent 776f626abb
commit a0d0740f10
6 changed files with 438 additions and 2 deletions
@@ -2,7 +2,10 @@ package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.libs.jupiter.perps.JupiterPerpsService; import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl; 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.SolanaBlockChain;
import com.r35157.libs.solana.impl.cached.CachedSolanaBlockChain;
import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl; import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl;
import java.net.URI; import java.net.URI;
@@ -46,8 +49,14 @@ public final class JupiterPerpsAlarmImpl {
alarmConfiguration.variables() alarmConfiguration.variables()
); );
System.out.print("Initializing dependecies... "); System.out.print("Initializing dependencies... ");
SolanaBlockChain solanaBlockChain = new SolanaBlockChainImpl(); ObjectCache objectCache = new ObjectCacheImpl();
SolanaBlockChain realSolanaBlockChain = new SolanaBlockChainImpl();
SolanaBlockChain solanaBlockChain = new CachedSolanaBlockChain(
realSolanaBlockChain,
objectCache
);
JupiterPerpsService jupiterPerpsService = new AnchorIdlJupiterPerpsServiceImpl(solanaBlockChain); JupiterPerpsService jupiterPerpsService = new AnchorIdlJupiterPerpsServiceImpl(solanaBlockChain);
JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher = JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher =
new JupiterPerpsEntryPriceVariableRefresher(alarmConfiguration.variables(), jupiterPerpsService); new JupiterPerpsEntryPriceVariableRefresher(alarmConfiguration.variables(), jupiterPerpsService);
@@ -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();
}
@@ -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<Object> 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<Object> parameters;
}
@@ -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;
}
@@ -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<>();
}
@@ -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<SolanaProgramAddressSeed> 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<SolanaAccountInfo> getProgramAccounts(
ΩSolanaProgramIdΩ programId,
Set<SolanaProgramAccountMemcmpFilter> filters
) throws IOException, InterruptedException {
long t0 = System.currentTimeMillis();
Set<SolanaAccountInfo> result;
Set<SolanaProgramAccountMemcmpFilter> 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<SolanaAccountInfo> loadProgramAccounts(
ObjectCacheKey key,
ΩSolanaProgramIdΩ programId,
Set<SolanaProgramAccountMemcmpFilter> filters
) throws IOException, InterruptedException {
Object cachedObject = objectCache.get(key);
if (cachedObject != null) {
return castProgramAccounts(cachedObject);
}
Set<SolanaAccountInfo> 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<SolanaAccountInfo> castProgramAccounts(
Object object
) {
return (Set<SolanaAccountInfo>) 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;
}