Skip to content

Commit

Permalink
feat: added batching for leaderboard. (#514)
Browse files Browse the repository at this point in the history
* feat: added batching for leaderboard.

* feat: added inline datum with results.

* feat: rollback improvements.

* fix: long not supported on return values

* fix: performance issue fixes: added tiny caches for tip and transaction details data.

---------

Co-authored-by: Mateusz Czeladka <[email protected]>
  • Loading branch information
matiwinnetou and Mateusz Czeladka authored Nov 24, 2023
1 parent 2602abb commit 28d9e37
Show file tree
Hide file tree
Showing 23 changed files with 777 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public class ChainFollowerClient {
@Value("${ledger.follower.app.base.url}")
private String ledgerFollowerBaseUrl;

public Either<Problem, L1CategoryResults> getVotingResults(String eventId,
String categoryId,
String tallyName
public Either<Problem, L1CategoryResults> getVotingResultsPerCategory(String eventId,
String categoryId,
String tallyName
) {
var url = String.format("%s/api/tally/voting-results/{eventId}/{categoryId}/{tallyName}", ledgerFollowerBaseUrl);

Expand All @@ -49,6 +49,22 @@ public Either<Problem, L1CategoryResults> getVotingResults(String eventId,
}
}

public Either<Problem, List<L1CategoryResults>> getVotingResultsForAllCategories(String eventId,
String tallyName
) {
var url = String.format("%s/api/tally/voting-results/{eventId}/{tallyName}", ledgerFollowerBaseUrl);

try {
return Either.right(Arrays.asList(restTemplate.getForObject(url, L1CategoryResults[].class, eventId, tallyName)));
} catch (HttpClientErrorException e) {
return Either.left(Problem.builder()
.withTitle("CATEGORY_RESULTS_ERROR")
.withDetail("Unable to get category results from chain-tip follower service, reason:" + e.getMessage())
.withStatus(new HttpStatusAdapter(e.getStatusCode()))
.build());
}
}

public Either<Problem, ChainTipResponse> getChainTip() {
var url = String.format("%s/api/blockchain/tip", ledgerFollowerBaseUrl);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class LeaderboardResource {
@Value("${leaderboard.force.results:false}")
private boolean forceLeaderboardResultsAvailability;

@RequestMapping(value = "/event/{eventId}/", method = HEAD, produces = "application/json")
@RequestMapping(value = "/event/{eventId}", method = HEAD, produces = "application/json")
@Timed(value = "resource.leaderboard.high.level.event.available", histogram = true)
public ResponseEntity<?> isHighLevelEventLeaderBoardAvailable(@PathVariable("eventId") String eventId,
@RequestHeader(value = XForceLeaderBoardResults, required = false, defaultValue = "false") boolean forceLeaderboardResults) {
Expand Down Expand Up @@ -189,9 +189,10 @@ public ResponseEntity<?> getEventLeaderBoard(@PathVariable("eventId") String eve
);
}

@Deprecated
@RequestMapping(value = "/{eventId}/{categoryId}", method = GET, produces = "application/json")
@Timed(value = "resource.leaderboard.category", histogram = true)
public ResponseEntity<?> getCategoryLeaderBoard(@PathVariable("eventId") String eventId,
public ResponseEntity<?> getCategoryLeaderBoardPerCategory(@PathVariable("eventId") String eventId,
@PathVariable("categoryId") String categoryId,
@RequestHeader(value = XForceLeaderBoardResults, required = false, defaultValue = "false") boolean forceLeaderboardResults,
@Valid @RequestParam(name = "source") Optional<WinnerLeaderboardSource> winnerLeaderboardSourceM) {
Expand Down Expand Up @@ -234,4 +235,81 @@ public ResponseEntity<?> getCategoryLeaderBoard(@PathVariable("eventId") String
);
}

@RequestMapping(value = "/{eventId}/{categoryId}/results", method = GET, produces = "application/json")
@Timed(value = "resource.leaderboard.category", histogram = true)
public ResponseEntity<?> getCategoryLeaderBoardPerCategoryResults(@PathVariable("eventId") String eventId,
@PathVariable("categoryId") String categoryId,
@RequestHeader(value = XForceLeaderBoardResults, required = false, defaultValue = "false") boolean forceLeaderboardResults,
@Valid @RequestParam(name = "source") Optional<WinnerLeaderboardSource> winnerLeaderboardSourceM) {
var winnerLeaderboardSource = winnerLeaderboardSourceM.orElse(db);

var cacheControl = CacheControl.maxAge(5, MINUTES)
.noTransform()
.mustRevalidate();

var forceLeaderboard = forceLeaderboardResults && forceLeaderboardResultsAvailability;

var categoryLeaderboardE = leaderboardWinnersProvider
.getWinnerLeaderboardSource(winnerLeaderboardSource)
.getCategoryLeaderboard(eventId, categoryId, forceLeaderboard);

return categoryLeaderboardE
.fold(problem -> {
return ResponseEntity
.status(problem.getStatus().getStatusCode())
.body(problem);
},
proposalsInCategoryStatsM -> {
if (proposalsInCategoryStatsM.isEmpty()) {
var problem = Problem.builder()
.withTitle("VOTING_RESULTS_NOT_YET_AVAILABLE")
.withDetail("Leaderboard not yet available for event: " + eventId)
.withStatus(NOT_FOUND)
.build();

return ResponseEntity
.status(problem.getStatus().getStatusCode())
.body(problem);
}

return ResponseEntity
.ok()
.cacheControl(cacheControl)
.body(proposalsInCategoryStatsM.orElseThrow());
}
);
}

@RequestMapping(value = "/{eventId}/results", method = GET, produces = "application/json")
@Timed(value = "resource.leaderboard.category.all", histogram = true)
public ResponseEntity<?> getCategoryLeaderBoardForAllCategoriesResults(@PathVariable("eventId") String eventId,
@RequestHeader(value = XForceLeaderBoardResults, required = false, defaultValue = "false") boolean forceLeaderboardResults,
@Valid @RequestParam(name = "source") Optional<WinnerLeaderboardSource> winnerLeaderboardSourceM) {
var winnerLeaderboardSource = winnerLeaderboardSourceM.orElse(db);

var cacheControl = CacheControl.maxAge(5, MINUTES)
.noTransform()
.mustRevalidate();

var forceLeaderboard = forceLeaderboardResults && forceLeaderboardResultsAvailability;

var categoryLeaderboardE = leaderboardWinnersProvider
.getWinnerLeaderboardSource(winnerLeaderboardSource)
.getAllCategoriesLeaderboard(eventId, forceLeaderboard);

return categoryLeaderboardE
.fold(problem -> {
return ResponseEntity
.status(problem.getStatus().getStatusCode())
.body(problem);
},
allCategoryResults -> {
return ResponseEntity
.ok()
.cacheControl(cacheControl)
.body(allCategoryResults);
}
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,37 @@ public class AbstractWinnersService {
@Autowired
protected ChainFollowerClient chainFollowerClient;


@Transactional(readOnly = true)
public Either<Problem, Boolean> isCategoryLeaderboardAvailable(String event,
boolean forceLeaderboard) {
var eventDetailsE = chainFollowerClient.getEventDetails(event);
if (eventDetailsE.isEmpty()) {
return Either.left(Problem.builder()
.withTitle("ERROR_GETTING_EVENT_DETAILS")
.withDetail("Unable to get event details from chain-tip follower service, event:" + event)
.withStatus(INTERNAL_SERVER_ERROR)
.build()
);
}
var maybeEventDetails = eventDetailsE.get();
if (maybeEventDetails.isEmpty()) {
return Either.left(Problem.builder()
.withTitle("UNRECOGNISED_EVENT")
.withDetail("Unrecognised event, event:" + event)
.withStatus(BAD_REQUEST)
.build()
);
}
var eventDetails = maybeEventDetails.orElseThrow();

return isCategoryLeaderboardAvailable(eventDetails, forceLeaderboard);
}

@Transactional(readOnly = true)
public Either<Problem, Boolean> isCategoryLeaderboardAvailable(String event, String category, boolean forceLeaderboard) {
public Either<Problem, Boolean> isCategoryLeaderboardAvailable(String event,
String category,
boolean forceLeaderboard) {
var eventDetailsE = chainFollowerClient.getEventDetails(event);
if (eventDetailsE.isEmpty()) {
return Either.left(Problem.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cardano.foundation.voting.service.leader_board;

import io.vavr.control.Either;
import org.cardano.foundation.voting.client.ChainFollowerClient;
import org.cardano.foundation.voting.domain.Leaderboard;
import org.cardano.foundation.voting.repository.VoteRepository;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -9,6 +10,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.zalando.problem.Problem;

import java.util.List;
import java.util.Optional;

import static java.util.stream.Collectors.toMap;
Expand All @@ -23,7 +25,9 @@ public class DBLeaderboardWinnersService extends AbstractWinnersService implemen

@Override
@Transactional(readOnly = true)
public Either<Problem, Optional<Leaderboard.ByProposalsInCategoryStats>> getCategoryLeaderboard(String event, String category, boolean forceLeaderboard) {
public Either<Problem, Optional<Leaderboard.ByProposalsInCategoryStats>> getCategoryLeaderboard(String event,
String category,
boolean forceLeaderboard) {
var eventDetailsE = chainFollowerClient.getEventDetails(event);
if (eventDetailsE.isEmpty()) {
return Either.left(Problem.builder()
Expand Down Expand Up @@ -56,6 +60,39 @@ public Either<Problem, Optional<Leaderboard.ByProposalsInCategoryStats>> getCate
}
var categoryDetails = maybeCategory.orElseThrow();

return getCategoryLeaderboard(eventDetails, categoryDetails, forceLeaderboard);
}

@Override
public Either<Problem, List<Leaderboard.ByProposalsInCategoryStats>> getAllCategoriesLeaderboard(String event,
boolean forceLeaderboard) {
var eventDetailsE = chainFollowerClient.getEventDetails(event);
if (eventDetailsE.isEmpty()) {
return Either.left(Problem.builder()
.withTitle("ERROR_GETTING_EVENT_DETAILS")
.withDetail("Unable to get event details from chain-tip follower service, event:" + event)
.withStatus(INTERNAL_SERVER_ERROR)
.build()
);
}
var maybeEventDetails = eventDetailsE.get();
if (maybeEventDetails.isEmpty()) {
return Either.left(Problem.builder()
.withTitle("UNRECOGNISED_EVENT")
.withDetail("Unrecognised event, event:" + event)
.withStatus(BAD_REQUEST)
.build()
);
}
var eventDetails = maybeEventDetails.orElseThrow();

return getCategoryLeaderboardForAllCategories(eventDetails, forceLeaderboard);
}

@Override
public Either<Problem, Optional<Leaderboard.ByProposalsInCategoryStats>> getCategoryLeaderboard(ChainFollowerClient.EventDetailsResponse eventDetails,
ChainFollowerClient.CategoryDetailsResponse categoryDetails,
boolean forceLeaderboard) {
var categoryLeaderboardAvailableE = isCategoryLeaderboardAvailable(eventDetails, forceLeaderboard);
if (categoryLeaderboardAvailableE.isEmpty()) {
return Either.left(categoryLeaderboardAvailableE.getLeft());
Expand All @@ -71,7 +108,7 @@ public Either<Problem, Optional<Leaderboard.ByProposalsInCategoryStats>> getCate
);
}

var votes = voteRepository.getCategoryLevelStats(event, categoryDetails.id());
var votes = voteRepository.getCategoryLevelStats(eventDetails.id(), categoryDetails.id());

var proposalResultsMap = votes.stream()
.collect(toMap(VoteRepository.CategoryLevelStats::getProposalId, v -> {
Expand All @@ -91,8 +128,54 @@ public Either<Problem, Optional<Leaderboard.ByProposalsInCategoryStats>> getCate
return Either.right(Optional.of(Leaderboard.ByProposalsInCategoryStats.builder()
.category(categoryDetails.id())
.proposals(reInitialiseResultsToEmptyIfMissing(categoryDetails, proposalResultsMap, eventDetails))
.build())
);
.build()));
}

public Either<Problem, List<Leaderboard.ByProposalsInCategoryStats>> getCategoryLeaderboardForAllCategories(ChainFollowerClient.EventDetailsResponse eventDetails,
boolean forceLeaderboard) {
var categoryLeaderboardAvailableE = isCategoryLeaderboardAvailable(eventDetails, forceLeaderboard);
if (categoryLeaderboardAvailableE.isEmpty()) {
return Either.left(categoryLeaderboardAvailableE.getLeft());
}

var isCategoryLeaderBoardAvailable = categoryLeaderboardAvailableE.get();
if (!isCategoryLeaderBoardAvailable) {
return Either.left(Problem.builder()
.withTitle("VOTING_RESULTS_NOT_AVAILABLE")
.withDetail("Category level voting results not available until results can be revealed!")
.withStatus(FORBIDDEN)
.build()
);
}

var allResultsForAllCategories = eventDetails.categories()
.stream()
.map(categoryDetails -> {
var categoryStats = voteRepository.getCategoryLevelStats(eventDetails.id(), categoryDetails.id());

var results = categoryStats.stream()
.collect(toMap(VoteRepository.CategoryLevelStats::getProposalId, v -> {
var totalVotesCount = Optional.ofNullable(v.getTotalVoteCount()).orElse(0L);
var totalVotingPower = Optional.ofNullable(v.getTotalVotingPower()).map(String::valueOf).orElse("0");

var b = Leaderboard.Votes.builder();
b.votes(totalVotesCount);

switch (eventDetails.votingEventType()) {
case BALANCE_BASED, STAKE_BASED -> b.votingPower(totalVotingPower);
}

return b.build();
}));

return Leaderboard.ByProposalsInCategoryStats.builder()
.category(categoryDetails.id())
.proposals(reInitialiseResultsToEmptyIfMissing(categoryDetails, results, eventDetails))
.build();
})
.toList();

return Either.right(allResultsForAllCategories);
}

}
Loading

0 comments on commit 28d9e37

Please sign in to comment.