Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public ResponseEntity<BlockResponse> block(@RequestBody BlockRequest blockReques
throw ExceptionFactory.notSupportedInOfflineMode();
}
networkService.verifyNetworkRequest(blockRequest.getNetworkIdentifier());
networkService.verifySyncStatus();

if (blockRequest.getBlockIdentifier().getIndex() != null && blockRequest.getBlockIdentifier().getIndex() < 0) {
throw ExceptionFactory.invalidBlockIdentifier(blockRequest.getBlockIdentifier().getIndex());
Expand All @@ -66,7 +65,6 @@ public ResponseEntity<BlockTransactionResponse> blockTransaction(
throw ExceptionFactory.notSupportedInOfflineMode();
}
networkService.verifyNetworkRequest(blockReq.getNetworkIdentifier());
networkService.verifySyncStatus();
if (blockReq.getBlockIdentifier().getIndex() != null && blockReq.getBlockIdentifier().getIndex() < 0) {
throw ExceptionFactory.invalidBlockIdentifier(blockReq.getBlockIdentifier().getIndex());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@
import org.springframework.stereotype.Service;

import org.cardanofoundation.rosetta.api.block.service.LedgerBlockService;
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Scheduled;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

/**
* Service responsible for calculating sync status based on blockchain tip
Expand All @@ -37,9 +35,18 @@ public class SyncStatusService {
@Value("${cardano.rosetta.SYNC_GRACE_SLOTS_COUNT:200}")
private int allowedSlotsDelta;

/**
* Holds the most recently computed sync status. Populated on startup and refreshed
* periodically by {@link #refreshSyncStatus()}, so request-time reads via
* {@link #getSyncStatus()} never block on DB access or clock/index computation.
*/
private final AtomicReference<Optional<SyncStatus>> cachedSyncStatus =
new AtomicReference<>(Optional.empty());

@PostConstruct
public void init() {
log.info("[SyncStatusService] Initializing with allowedSlotsDelta: {}", allowedSlotsDelta);
refreshSyncStatus();
}

/**
Expand Down Expand Up @@ -146,25 +153,27 @@ public Optional<SyncStatus> calculateSyncStatus(BlockIdentifierExtended latestBl
}

/**
* Gets the sync status of the indexer. This is cached for 5 seconds to prevent
* redundant database checks on every request.
* Gets the most recently computed sync status of the indexer. This never performs DB
* access or computation itself - it simply reads the value last stored by the periodic
* background refresh ({@link #refreshSyncStatus()}), so request-time callers get an
* instant response.
*
* @return an Optional containing the SyncStatus of the indexer if available, empty otherwise
*/
@Cacheable(value = "syncStatusCache", unless = "#result == null")
public Optional<SyncStatus> getSyncStatus() {
log.info("[SyncStatusService] Cache miss - querying sync status from DB");
BlockIdentifierExtended latestBlock = ledgerBlockService.findLatestBlockIdentifier();
return calculateSyncStatus(latestBlock);
return cachedSyncStatus.get();
}

/**
* Periodically evicts all sync status entries from the cache.
* The rate is configurable via properties, defaulting to 5 seconds.
* Periodically recomputes the sync status from the DB and current time, and stores the
* result for {@link #getSyncStatus()} to serve. The rate is configurable via properties,
* defaulting to 10 seconds. Also invoked once on startup via {@link #init()} so the first
* requests don't see an empty status while waiting for the first scheduled tick.
*/
@Scheduled(fixedRateString = "${cardano.rosetta.sync-status-cache-ttl-ms:5000}")
@CacheEvict(value = "syncStatusCache", allEntries = true)
public void evictSyncStatusCache() {
log.trace("[SyncStatusService] Evicting syncStatusCache");
@Scheduled(fixedRateString = "${cardano.rosetta.sync-status-refresh-rate-ms:10000}")
public void refreshSyncStatus() {
log.debug("[SyncStatusService] Refreshing sync status from DB");
BlockIdentifierExtended latestBlock = ledgerBlockService.findLatestBlockIdentifier();
cachedSyncStatus.set(calculateSyncStatus(latestBlock));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class CacheConfig {

@Bean
public ConcurrentMapCacheManager cacheManager() {
return new ConcurrentMapCacheManager("protocolParamsCache", "syncStatusCache");
return new ConcurrentMapCacheManager("protocolParamsCache");
}

//a cache for token metadata from token registry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.cardanofoundation.rosetta.api.block.model.domain.BlockTx;
import org.cardanofoundation.rosetta.api.block.model.domain.ProtocolParams;
import org.cardanofoundation.rosetta.api.block.service.BlockService;
import org.cardanofoundation.rosetta.api.network.service.SyncStatusService;
import org.cardanofoundation.rosetta.api.common.service.TokenRegistryService;
import org.cardanofoundation.rosetta.api.network.service.NetworkService;
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;
Expand Down Expand Up @@ -49,6 +50,9 @@ class BlockApiImplTest extends BaseSpringMvcSetup {
@MockitoBean
private NetworkService networkService;

@MockitoBean
private SyncStatusService syncStatusService;

@Mock
private ProtocolParams protocolParams;

Expand All @@ -68,20 +72,6 @@ void blockInvalidIndex() {
assertThrows(ExceptionFactory.invalidBlockIdentifier(-1L).getClass(), () -> blockApi.block(blockRequest));
}

@Test
void blockIndexerNotReadyTest() {
BlockRequest blockRequest = newBlockRequest();
Mockito.doThrow(ExceptionFactory.indexerNotReady()).when(networkService).verifySyncStatus();
assertThrows(ExceptionFactory.indexerNotReady().getClass(), () -> blockApi.block(blockRequest));
}

@Test
void blockTransactionIndexerNotReadyTest() {
BlockTransactionRequest blockTransactionRequest = newBlockTransactionRequest();
Mockito.doThrow(ExceptionFactory.indexerNotReady()).when(networkService).verifySyncStatus();
assertThrows(ExceptionFactory.indexerNotReady().getClass(), () -> blockApi.blockTransaction(blockTransactionRequest));
}

@Test
void blockOfflineModeTest() throws NoSuchFieldException, IllegalAccessException {
//given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,19 @@ void shouldReturnNotSyncedWhenNotReachedTipEvenIfIndexesNotReady() {
class GetSyncStatusTests {

@Test
@DisplayName("Should query latest block and calculate sync status successfully")
void shouldQueryLatestBlockAndCalculateSyncStatus() {
@DisplayName("Should return empty when no background refresh has occurred yet")
void shouldReturnEmptyBeforeAnyRefresh() {
// When
Optional<SyncStatus> resultOpt = syncStatusService.getSyncStatus();

// Then
assertThat(resultOpt).isEmpty();
verify(ledgerBlockService, times(0)).findLatestBlockIdentifier();
}

@Test
@DisplayName("Should return the value last stored by refreshSyncStatus without querying the DB again")
void shouldReturnLastRefreshedValueWithoutQueryingDbAgain() {
// Given
long currentSlot = 1000L;
long latestBlockSlot = 990L;
Expand All @@ -215,15 +226,79 @@ void shouldQueryLatestBlockAndCalculateSyncStatus() {
.thenReturn(true);
when(indexCreationMonitor.isCreatingIndexes()).thenReturn(false);

syncStatusService.refreshSyncStatus();

// When
Optional<SyncStatus> resultOpt = syncStatusService.getSyncStatus();
Optional<SyncStatus> resultOpt2 = syncStatusService.getSyncStatus();

// Then
assertThat(resultOpt).isPresent();
SyncStatus result = resultOpt.get();
assertThat(result.getSynced()).isTrue();
assertThat(result.getStage()).isEqualTo(SyncStage.LIVE.getValue());
assertThat(resultOpt2).isEqualTo(resultOpt);
// getSyncStatus() must never itself trigger a DB query - only refreshSyncStatus() does.
verify(ledgerBlockService, times(1)).findLatestBlockIdentifier();
}
}

@Nested
@DisplayName("refreshSyncStatus tests")
class RefreshSyncStatusTests {

@Test
@DisplayName("Should query latest block, recalculate sync status, and store it for getSyncStatus")
void shouldQueryLatestBlockAndStoreCalculatedSyncStatus() {
// Given
long currentSlot = 1000L;
long latestBlockSlot = 990L;
BlockIdentifierExtended latestBlock = BlockIdentifierExtended.builder()
.slot(latestBlockSlot)
.build();

when(ledgerBlockService.findLatestBlockIdentifier()).thenReturn(latestBlock);
when(offlineSlotService.getCurrentSlotBasedOnTime()).thenReturn(Optional.of(currentSlot));
when(slotRangeChecker.isSlotWithinEpsilon(currentSlot, latestBlockSlot, ALLOWED_SLOTS_DELTA))
.thenReturn(true);
when(indexCreationMonitor.isCreatingIndexes()).thenReturn(false);

// When
syncStatusService.refreshSyncStatus();

// Then
verify(ledgerBlockService, times(1)).findLatestBlockIdentifier();
Optional<SyncStatus> resultOpt = syncStatusService.getSyncStatus();
assertThat(resultOpt).isPresent();
assertThat(resultOpt.get().getSynced()).isTrue();
assertThat(resultOpt.get().getStage()).isEqualTo(SyncStage.LIVE.getValue());
}

@Test
@DisplayName("Should overwrite the previously stored sync status on each call")
void shouldOverwritePreviouslyStoredSyncStatus() {
// Given: first refresh reports SYNCING
BlockIdentifierExtended firstBlock = BlockIdentifierExtended.builder().slot(800L).build();
when(ledgerBlockService.findLatestBlockIdentifier()).thenReturn(firstBlock);
when(offlineSlotService.getCurrentSlotBasedOnTime()).thenReturn(Optional.of(1000L));
when(slotRangeChecker.isSlotWithinEpsilon(1000L, 800L, ALLOWED_SLOTS_DELTA)).thenReturn(false);
when(indexCreationMonitor.isCreatingIndexes()).thenReturn(false);

syncStatusService.refreshSyncStatus();
assertThat(syncStatusService.getSyncStatus().orElseThrow().getStage())
.isEqualTo(SyncStage.SYNCING.getValue());

// When: second refresh reports LIVE
BlockIdentifierExtended secondBlock = BlockIdentifierExtended.builder().slot(1000L).build();
when(ledgerBlockService.findLatestBlockIdentifier()).thenReturn(secondBlock);
when(slotRangeChecker.isSlotWithinEpsilon(1000L, 1000L, ALLOWED_SLOTS_DELTA)).thenReturn(true);

syncStatusService.refreshSyncStatus();

// Then
assertThat(syncStatusService.getSyncStatus().orElseThrow().getStage())
.isEqualTo(SyncStage.LIVE.getValue());
verify(ledgerBlockService, times(2)).findLatestBlockIdentifier();
}
}
}
Loading