first attempt at keeping computing live leaderboard results in LiveLeaderboardUpdater in the background

This commit is contained in:
Axel Uhl committed 2012-07-23 02:20:55 +02:00
1 parent 9a9440018e
commit 20d12939f8
12 files changed
+322 -116

No files matched your search

File mode changed.
File mode changed.
@@ -240,4 +240,10 @@ public interface Leaderboard extends Named {
void addRaceColumnListener(RaceColumnListener listener);
void removeRaceColumnListener(RaceColumnListener listener);
/**
* For this leaderboard computes the delay to live of the {@link TrackedRace} linked to any of the leaderboard's
* columns that has the latest start date. If no tracked race is linked to the leaderboard, <code>null</code> is returned.
*/
long getDelayToLiveInMillis();
}
@@ -508,4 +508,21 @@ public abstract class AbstractLeaderboardImpl implements Leaderboard, RaceColumn
}
}
@Override
public long getDelayToLiveInMillis() {
TimePoint startOfLatestRace = null;
Long delayToLiveInMillisForLatestRace = null;
for (RaceColumn raceColumn : getRaceColumns()) {
for (Fleet fleet : raceColumn.getFleets()) {
TrackedRace trackedRace = raceColumn.getTrackedRace(fleet);
if (trackedRace != null) {
if (startOfLatestRace == null
|| (trackedRace.getStartOfRace() != null && trackedRace.getStartOfRace().compareTo(startOfLatestRace) > 0)) {
delayToLiveInMillisForLatestRace = trackedRace.getDelayToLiveInMillis();
}
}
}
}
return delayToLiveInMillisForLatestRace;
}
}
@@ -162,34 +162,32 @@ public class GwtTestCaseColumnToggling extends GWTTestCase {
private void getLeaderboard(){
ArrayList<String> al = new ArrayList<String>();
al.add(COLUMN1_NAME);
service.getLeaderboardByName(LEADERBOARD_NAME, new Date(), al, /* waitForLatestManeuverAnalysis */ true,
new AsyncCallback<LeaderboardDTO>() {
service.getLeaderboardByName(LEADERBOARD_NAME, new Date(), al, new AsyncCallback<LeaderboardDTO>() {
@Override
public void onFailure(Throwable caught) {
fail("Failed to get leaderboard.");
finishTest();
}
@Override
public void onFailure(Throwable caught) {
fail("Failed to get leaderboard.");
finishTest();
@Override
public void onSuccess(LeaderboardDTO result) {
System.out.println("Got leaderboard.");
leaderboard = result;
leaderboardPanel.updateLeaderboard(leaderboard);
for (int i = 0; i < leaderboardPanel.getLeaderboardTable()
.getColumnCount(); i++) {
Column<LeaderboardRowDTO, ?> c = leaderboardPanel.getLeaderboardTable().getColumn(i);
if (c instanceof ExpandableSortableColumn<?>) {
@SuppressWarnings("unchecked")
ExpandableSortableColumn<LeaderboardRowDTO> myRc = (ExpandableSortableColumn<LeaderboardRowDTO>) c;
rc = myRc;
rc.setEnableLegDrillDown(true);
}
@Override
public void onSuccess(LeaderboardDTO result) {
System.out.println("Got leaderboard.");
leaderboard = result;
leaderboardPanel.updateLeaderboard(leaderboard);
for (int i = 0; i < leaderboardPanel.getLeaderboardTable()
.getColumnCount(); i++) {
Column<LeaderboardRowDTO, ?> c = leaderboardPanel.getLeaderboardTable().getColumn(i);
if (c instanceof ExpandableSortableColumn<?>) {
@SuppressWarnings("unchecked")
ExpandableSortableColumn<LeaderboardRowDTO> myRc = (ExpandableSortableColumn<LeaderboardRowDTO>) c;
rc = myRc;
rc.setEnableLegDrillDown(true);
}
}
removeColumnAndAssert();
}
});
}
removeColumnAndAssert();
}
});
}
private void removeColumnAndAssert(){
@@ -81,7 +81,7 @@ public class TestColumnSwapping {
sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true);
sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false);
try {
lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null, /* waitForLatestManeuverAnalysis */ true);
lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null);
} catch (Exception e) {
// e.printStackTrace();
fail(e.getLocalizedMessage());
@@ -116,7 +116,7 @@ public class TestColumnSwapping {
public void testColumnSwappingFabian() {
service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3");
try {
leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* waitForLatestManeuverAnalysis */ true);
leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
@@ -12,24 +12,20 @@ public class GetLeaderboardByNameAction extends DefaultAsyncAction<LeaderboardDT
private final String leaderboardName;
private final Date date;
private final Collection<String> namesOfRacesForWhichToLoadLegDetails;
private final boolean waitForLatestManeuverAnalysis;
public GetLeaderboardByNameAction(SailingServiceAsync sailingService, String leaderboardName, Date date,
final Collection<String> namesOfRacesForWhichToLoadLegDetails, boolean waitForLatestManeuverAnalysis,
AsyncCallback<LeaderboardDTO> callback) {
final Collection<String> namesOfRacesForWhichToLoadLegDetails, AsyncCallback<LeaderboardDTO> callback) {
super(callback);
this.sailingService = sailingService;
this.leaderboardName = leaderboardName;
this.date = date;
this.namesOfRacesForWhichToLoadLegDetails = namesOfRacesForWhichToLoadLegDetails;
this.waitForLatestManeuverAnalysis = waitForLatestManeuverAnalysis;
}
@Override
public void execute(AsyncActionsExecutor asyncActionsExecutor) {
sailingService
.getLeaderboardByName(leaderboardName, date, namesOfRacesForWhichToLoadLegDetails,
waitForLatestManeuverAnalysis,
(AsyncCallback<LeaderboardDTO>) getWrapperCallback(asyncActionsExecutor));
}
@@ -108,8 +108,7 @@ public interface SailingService extends RemoteService {
public List<String> getLeaderboardNames() throws Exception;
LeaderboardDTO getLeaderboardByName(String leaderboardName, Date date,
Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails, boolean waitForLatestAnalyses)
throws Exception;
Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails) throws Exception;
List<StrippedLeaderboardDTO> getLeaderboards();
@@ -166,6 +166,10 @@ public interface SailingServiceAsync {
* filled in. The column details are filled for the races whose named are provided in
* <code>namesOfRacesForWhichToLoadLegDetails</code>.
*
* @param date
* the time point for the leaderboard data to retrieve, or <code>null</code> for "live mode" which means
* that the server will produce whatever it has ready for the currently active live delay set on the
* server; in live mode, data served may be taken from caches that lag up to a few seconds.
* @param namesOfRaceColumnsForWhichToLoadLegDetails
* if <code>null</code>, no {@link LeaderboardEntryDTO#legDetails leg details} will be present in the
* result ({@link LeaderboardEntryDTO#legDetails} will be <code>null</code> for all
@@ -173,15 +177,10 @@ public interface SailingServiceAsync {
* list will contain one entry per leg of the race {@link Course} for those race columns whose
* {@link RaceColumn#getType() name} is contained in <code>namesOfRacesForWhichToLoadLegDetails</code>.
* For all other columns, {@link LeaderboardEntryDTO#legDetails} is <code>null</code>.
* @param waitForLatestAnalyses
* if <code>false</code>, this method is allowed to read the maneuver analysis and other expensive
* analysis results such as the cross track error from a cache that may not reflect all data already
* received; otherwise, the method will always block for the latest cache updates to have happened before
* returning.
*/
void getLeaderboardByName(String leaderboardName, Date date,
Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails,
boolean waitForLatestAnalyses, AsyncCallback<LeaderboardDTO> callback);
AsyncCallback<LeaderboardDTO> callback);
void getLeaderboardNames(AsyncCallback<List<String>> callback);
@@ -578,23 +578,23 @@ public class LeaderboardPanel extends FormPanel implements TimeListener, PlaySta
if (getLeaderboard().getLegCount(getRaceColumnName()) != -1) {
callWhenExpansionDataIsLoaded.run();
} else {
getSailingService().getLeaderboardByName(getLeaderboardName(), getLeaderboardDisplayDate(),
getSailingService().getLeaderboardByName(getLeaderboardName(),
timer.getPlayMode() == PlayModes.Live ? null : getLeaderboardDisplayDate(),
/* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(),
/* waitForLatestAnalyses */ timer.getPlayMode() != PlayModes.Live,
new AsyncCallback<LeaderboardDTO>() {
@Override
public void onSuccess(LeaderboardDTO result) {
updateLeaderboard(result);
callWhenExpansionDataIsLoaded.run();
}
new AsyncCallback<LeaderboardDTO>() {
@Override
public void onSuccess(LeaderboardDTO result) {
updateLeaderboard(result);
callWhenExpansionDataIsLoaded.run();
}
@Override
public void onFailure(Throwable caught) {
getErrorReporter().reportError(
stringMessages.errorTryingToObtainLeaderboardContents(caught.getMessage()),
/* silentMode */ timer.getPlayMode() == PlayModes.Live);
}
});
@Override
public void onFailure(Throwable caught) {
getErrorReporter().reportError(
stringMessages.errorTryingToObtainLeaderboardContents(caught.getMessage()),
/* silentMode */ timer.getPlayMode() == PlayModes.Live);
}
});
}
}
@@ -1240,9 +1240,9 @@ public class LeaderboardPanel extends FormPanel implements TimeListener, PlaySta
private void loadCompleteLeaderboard(Date date) {
if (needsDataLoading()) {
GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, getLeaderboardName(), date,
GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, getLeaderboardName(),
timer.getPlayMode() == PlayModes.Live ? null : date,
/* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(),
/* waitForLatestAnalyses */ timer.getPlayMode() != PlayModes.Live,
new AsyncCallback<LeaderboardDTO>() {
@Override
public void onSuccess(LeaderboardDTO result) {
@@ -0,0 +1,212 @@
package com.sap.sailing.gwt.ui.server;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.common.NoWindException;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.gwt.ui.shared.LeaderboardDTO;
/**
* Computing a leaderboard live, particularly when the fleet tracked is large, requires considerable resources.
* Live tracking events such as new GPS fixes and wind data make caching hard because caches need to be invalidated
* frequently. Instead of computing a {@link LeaderboardDTO} on the fly when a client asks for it, in live mode it
* is more adequate to keep updating a ("whiteboard") cache with results which can be served to a client in live mode.
* <p>
*
* This updater, when run, keeps updating its {@link #getLiveLeaderboard live leaderboard}. It is specific to a single
* leaderboard name and accumulates a set of race columns for which to obtain the details. It stops when no client has been asking
* for the live leaderboard's contents for more then {@link #UPDATE_TIMEOUT_IN_MILLIS} milliseconds. It is automatically re-started
* by the next request received.<p>
*
* Clients pass their requested set of expanded columns on to this updater when they fetch the current live result from this
* cache. Each column requested by a client will continue to be updated by this updater into the live leaderboard result
* for a {@link #UPDATE_TIMEOUT_IN_MILLIS specific time}.<p>
*
* The updater keeps computing updates until {@link #UPDATE_TIMEOUT_IN_MILLIS} after the last client request for the
* leaderboard for which this updater is responsible has been received. Then it terminates in a "synchronized way." It can
* be re-activated. Re-activating it works using the same synchronization facility so that no two threads for this updater
* are running at the same time.
*
* @author Axel Uhl (D043530)
*
*/
public class LiveLeaderboardUpdater implements Runnable {
private static final Logger logger = Logger.getLogger(LiveLeaderboardUpdater.class.getName());
/**
* For how long shall the updater keep pre-computing results after the last request has been received; currently 60s
*/
private static final long UPDATE_TIMEOUT_IN_MILLIS = 60000l;
private final String leaderboardName;
private final SailingServiceImpl sailingService;
/**
* Live leaderboard results containing column details for all column names in
* {@link #columnNamesForWhichCurrentLiveLeaderboardHasTheDetails}. May be <code>null</code> if
* {@link #columnNamesForWhichCurrentLiveLeaderboardHasTheDetails} is empty in case no leaderboard with no column
* details has been computed yet. Vice versa, may contain a non-<code>null</code> result even if
* {@link #columnNamesForWhichCurrentLiveLeaderboardHasTheDetails} is empty because clients may only be interested
* currently in a live leaderboard with no column details.
*/
private LeaderboardDTO currentLiveLeaderboard;
/**
* Column names for which {@link #currentLiveLeaderboard} has the details
*/
private final Set<String> columnNamesForWhichCurrentLiveLeaderboardHasTheDetails;
private boolean running;
/**
* For each String from <code>namesOfRaceColumnsForWhichToLoadLegDetails</code> passed to
* {@link #getLiveLeaderboard(Collection)}, records the time of the last request here when the
* {@link #currentLiveLeaderboard} has been updated with results containing this column details. This updater will
* stop computing the details for that column if the time for the update calculation is more than
* {@link #UPDATE_TIMEOUT_IN_MILLIS} milliseconds after the time point recorded here.<p>
*/
private final Map<String, TimePoint> timePointOfLastRequestForColumnDetails;
/**
* As soon as the first {@link #getLiveLeaderboard(Collection)} request has been received, this field tells the
* "validity time point" (as opposed to the request time point; so not when the request was received but for which
* time point the request was asking the data) for which the last general request asked the leaderboard contents,
* regardless the combination of column details requested. This is used to decide when to stop a thread running this
* updater. See also {@link #run}.
*/
private TimePoint lastRequest;
private int cacheHitCount;
private int cacheMissCount;
public LiveLeaderboardUpdater(String leaderboardName, SailingServiceImpl sailingService) {
this.leaderboardName = leaderboardName;
this.sailingService = sailingService;
this.timePointOfLastRequestForColumnDetails = new HashMap<String, TimePoint>();
this.columnNamesForWhichCurrentLiveLeaderboardHasTheDetails = new HashSet<String>();
}
private Leaderboard getLeaderboard() {
return sailingService.getService().getLeaderboardByName(leaderboardName);
}
public LeaderboardDTO getLiveLeaderboard(Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails) throws NoWindException {
final MillisecondsTimePoint now = MillisecondsTimePoint.now();
LeaderboardDTO result = null;
synchronized (this) {
if (columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.containsAll(namesOfRaceColumnsForWhichToLoadLegDetails)) {
result = currentLiveLeaderboard;
cacheHitCount++;
}
}
if (result == null) { // current cache doesn't have the column details requested; re-calculate
cacheMissCount++;
TimePoint timePoint = now.minus(getLeaderboard().getDelayToLiveInMillis());
result = sailingService.computeLeaderboardByName(leaderboardName, timePoint, namesOfRaceColumnsForWhichToLoadLegDetails,
/* waitForLatestAnalyses */ false);
updateRequestTimes(namesOfRaceColumnsForWhichToLoadLegDetails, result, timePoint);
updateCacheContents(namesOfRaceColumnsForWhichToLoadLegDetails, result);
}
ensureRunning();
logger.info(""+LiveLeaderboardUpdater.class.getSimpleName()+" cache hits/misses: "+cacheHitCount+"/"+cacheMissCount);
return result;
}
private synchronized void ensureRunning() {
if (!running) {
start();
}
}
private synchronized void start() {
running = true;
new Thread(this, "LiveLeaderboardUpdater for leaderboard "+getLeaderboard().getName()).start();
}
/**
* Updates {@link #lastRequest} with <code>timePoint</code> and the {@link #timePointOfLastRequestForColumnDetails
* latest request times} for each of the column names in <code>namesOfRaceColumnsForWhichToLoadLegDetails</code> to
* <code>timePoint</code> if <code>timePoint</code> is newer than the last request time for the column details so
* far. Stores <code>result</code> as the {@link #currentLiveLeaderboard cache value} if
* <code>namesOfRaceColumnsForWhichToLoadLegDetails</code> contains all column names already in
* {@link #timePointOfLastRequestForColumnDetails}'s key set.
* <p>
*
* This method assumes that <code>namesOfRaceColumnsForWhichToLoadLegDetails</code> tells the column names for which
* <code>result</code> has the details.
*/
private synchronized void updateRequestTimes(Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails,
LeaderboardDTO result, TimePoint timePoint) {
lastRequest = timePoint;
for (String nameOfRaceColumn : namesOfRaceColumnsForWhichToLoadLegDetails) {
if (!timePointOfLastRequestForColumnDetails.containsKey(nameOfRaceColumn) ||
timePoint.after(timePointOfLastRequestForColumnDetails.get(nameOfRaceColumn))) {
timePointOfLastRequestForColumnDetails.put(nameOfRaceColumn, timePoint);
}
}
}
private synchronized void updateCacheContents(Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails,
LeaderboardDTO result) {
columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.clear();
columnNamesForWhichCurrentLiveLeaderboardHasTheDetails.addAll(namesOfRaceColumnsForWhichToLoadLegDetails);
currentLiveLeaderboard = result;
}
/**
* Keeps computing the leaderboard for the column names that appear as key in {@link #timePointOfLastRequestForColumnDetails}
* until no request has happened
*/
@Override
public void run() {
assert running;
TimePoint timePoint = MillisecondsTimePoint.now().minus(getLeaderboard().getDelayToLiveInMillis());
while (running) {
try {
final Set<String> namesOfRaceColumnsForWhichToLoadLegDetails = getColumnNamesForWhichToFetchDetails(timePoint);
LeaderboardDTO newCacheValue = sailingService.computeLeaderboardByName(leaderboardName, timePoint,
namesOfRaceColumnsForWhichToLoadLegDetails, /* waitForLatestAnalyses */ false);
updateCacheContents(namesOfRaceColumnsForWhichToLoadLegDetails, newCacheValue);
} catch (NoWindException e) {
logger.info("Unable to update cached leaderboard results for leaderboard "+leaderboardName+": "+e.getMessage());
logger.throwing(LiveLeaderboardUpdater.class.getName(), "run", e);
try {
Thread.sleep(1000); // avoid running into the same NoWindException too quickly
} catch (InterruptedException e1) {
logger.throwing(LiveLeaderboardUpdater.class.getName(), "run", e1);
}
}
timePoint = MillisecondsTimePoint.now().minus(getLeaderboard().getDelayToLiveInMillis());
synchronized (this) {
if (timePoint.asMillis() - lastRequest.asMillis() >= UPDATE_TIMEOUT_IN_MILLIS) {
running = false;
break; // make sure no-one sets running to true again while outside the synchronized block and before re-evaluating the while condition
}
}
}
}
/**
* Determines those column names from {@link #timePointOfLastRequestForColumnDetails}'s keys for which the last
* request is less then {@link #UPDATE_TIMEOUT_IN_MILLIS} milliseconds before <code>timePoint</code>
*/
private Set<String> getColumnNamesForWhichToFetchDetails(TimePoint timePoint) {
Set<String> result = new HashSet<String>();
TimePoint expiredIfBefore = timePoint.minus(UPDATE_TIMEOUT_IN_MILLIS);
for (Map.Entry<String, TimePoint> e : timePointOfLastRequestForColumnDetails.entrySet()) {
if (e.getValue().after(expiredIfBefore)) {
result.add(e.getKey());
}
}
return result;
}
}
@@ -243,18 +243,14 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
private final Executor executor;
private final Executor computeLeadearboardByNameExecutor;
/**
* In live operations, {@link #getLeaderboardByName(String, Date, Collection, boolean)} is the application's
* bottleneck. When two clients ask the same data for the same leaderboard with their
* <code>waitForLatestAnalyses</code> parameters set to <code>false</code>, expansion state and (quantized) time
* stamp, no two computations should be spawned for the two clients. Instead, if the computation is still running,
* all clients asking the same wait for the single result. Results are cached in this LRU-based evicting cache.
* {@link #getLeaderboardByName(String, Date, Collection)} is the application's bottleneck. When two clients ask the
* same data for the same leaderboard with their <code>waitForLatestAnalyses</code> parameters set to
* <code>false</code>, expansion state and (quantized) time stamp, no two computations should be spawned for the two
* clients. Instead, if the computation is still running, all clients asking the same wait for the single result.
* Results are cached in this LRU-based evicting cache.
*/
private final Map<Triple<String, Date, Collection<String>>, FutureTask<LeaderboardDTO>> leaderboardByNameCache;
private int leaderboardByNameCacheHitCount;
private int leaderboardByNameCacheMissCount;
private final Map<String, LiveLeaderboardUpdater> leaderboardByNameLiveUpdaters;
/**
* This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor}
@@ -289,16 +285,8 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
swissTimingFactory = SwissTimingFactory.INSTANCE;
countryCodeFactory = com.sap.sailing.domain.common.CountryCodeFactory.INSTANCE;
executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
computeLeadearboardByNameExecutor = Executors.newFixedThreadPool(10*Runtime.getRuntime().availableProcessors());
raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
leaderboardByNameCache = new LinkedHashMap<Triple<String,Date,Collection<String>>, FutureTask<LeaderboardDTO>>() {
private static final long serialVersionUID = 3775119859130148488L;
private static final int MAX_ENTRIES = 100;
@Override
protected boolean removeEldestEntry(Map.Entry<Triple<String,Date,Collection<String>>, FutureTask<LeaderboardDTO>> eldest) {
return size() > MAX_ENTRIES;
}
};
leaderboardByNameLiveUpdaters = new HashMap<String, LiveLeaderboardUpdater>();
}
public void destroy() {
@@ -371,48 +359,45 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
return result;
}
/**
* If <code>date</code> is <code>null</code>, the {@link LiveLeaderboardUpdater} for the
* <code>leaderboardName</code> requested is obtained or created if it doesn't exist yet. The request is then passed
* on to the live leaderboard updater which will respond with its live {@link LeaderboardDTO} if it has at least the
* columns requested as per <code>namesOfRaceColumnsForWhichToLoadLegDetails</code>. Otherwise, the updater will add
* the missing columns to its profile and start a synchronous computation for the requesting client, the result of
* which will be used as live leaderboard cache update.
* <p>
*
* Otherwise, the leaderboard is {@link #computeLeaderboardByName(String, Date, Collection, boolean) computed}
* synchronously on the fly.
*/
@Override
public LeaderboardDTO getLeaderboardByName(final String leaderboardName, final Date date,
final Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails, final boolean waitForLatestAnalyses)
final Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails)
throws NoWindException, InterruptedException, ExecutionException {
LeaderboardDTO result;
boolean liveMode = date == null;
long startOfRequestHandling = System.currentTimeMillis();
Triple<String, Date, Collection<String>> key = new Triple<String, Date, Collection<String>>(
leaderboardName, date, namesOfRaceColumnsForWhichToLoadLegDetails);
FutureTask<LeaderboardDTO> future = null;
boolean cacheHit = false;
synchronized (leaderboardByNameCache) {
if (!waitForLatestAnalyses) {
future = leaderboardByNameCache.get(key);
if (liveMode) {
LiveLeaderboardUpdater liveLeaderboardUpdater;
synchronized (leaderboardByNameLiveUpdaters) {
liveLeaderboardUpdater = leaderboardByNameLiveUpdaters.get(leaderboardName);
if (liveLeaderboardUpdater == null) {
liveLeaderboardUpdater = new LiveLeaderboardUpdater(leaderboardName, this);
leaderboardByNameLiveUpdaters.put(leaderboardName, liveLeaderboardUpdater);
}
}
if (future == null) {
future = new FutureTask<LeaderboardDTO>(new Callable<LeaderboardDTO>() {
@Override
public LeaderboardDTO call() throws Exception {
LeaderboardDTO result = computeLeaderboardByName(leaderboardName, date,
namesOfRaceColumnsForWhichToLoadLegDetails, waitForLatestAnalyses);
return result;
}
});
computeLeadearboardByNameExecutor.execute(future);
leaderboardByNameCache.put(key, future);
} else {
cacheHit = true;
}
}
if (cacheHit) {
leaderboardByNameCacheHitCount++;
logger.info("Cache hit in getLeaderboardByName("+leaderboardName+", "+date+", "+namesOfRaceColumnsForWhichToLoadLegDetails+")");
result = liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails);
} else {
leaderboardByNameCacheMissCount++;
result = computeLeaderboardByName(leaderboardName, new MillisecondsTimePoint(date), namesOfRaceColumnsForWhichToLoadLegDetails,
/* waitForLatestAnalyses */ true); // in replay mode we'de like to know things exactly and can afford it
}
logger.info("getLeaderboardByName cache hit vs. miss: "+leaderboardByNameCacheHitCount+"/"+leaderboardByNameCacheMissCount);
LeaderboardDTO result = future.get();
logger.fine("getLeaderboardByName("+leaderboardName+", "+date+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+
(System.currentTimeMillis()-startOfRequestHandling)+"ms");
return result;
}
private LeaderboardDTO computeLeaderboardByName(String leaderboardName, Date date,
protected LeaderboardDTO computeLeaderboardByName(String leaderboardName, final TimePoint timePoint,
final Collection<String> namesOfRaceColumnsForWhichToLoadLegDetails, final boolean waitForLatestAnalyses)
throws NoWindException {
long startOfRequestHandling = System.currentTimeMillis();
@@ -422,12 +407,9 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
result = new LeaderboardDTO(leaderboard.getScoreCorrection().getTimePointOfLastCorrectionsValidity()==null ?
null : leaderboard.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(),
leaderboard.getScoreCorrection()==null?null:leaderboard.getScoreCorrection().getComment());
final TimePoint timePoint = new MillisecondsTimePoint(date);
result.competitors = new ArrayList<CompetitorDTO>();
result.name = leaderboard.getName();
result.competitorDisplayNames = new HashMap<CompetitorDTO, String>();
TimePoint startOfLatestRace = null;
Long delayToLiveInMillisForLatestRace = null;
for (RaceColumn raceColumn : leaderboard.getRaceColumns()) {
RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace());
for (Fleet fleet : raceColumn.getFleets()) {
@@ -435,9 +417,6 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
TrackedRace trackedRace = raceColumn.getTrackedRace(fleet);
final FleetDTO fleetDTO = createFleetDTO(fleet);
if (trackedRace != null) {
if (startOfLatestRace == null || (trackedRace.getStartOfRace() != null && trackedRace.getStartOfRace().compareTo(startOfLatestRace) > 0)) {
delayToLiveInMillisForLatestRace = trackedRace.getDelayToLiveInMillis();
}
raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta()
.getName(), trackedRace.getRace().getName());
}
@@ -446,14 +425,14 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
}
result.setCompetitorsFromBestToWorst(raceColumnDTO, getCompetitorDTOList(leaderboard.getCompetitorsFromBestToWorst(raceColumn, timePoint)));
}
result.setDelayToLiveInMillisForLatestRace(delayToLiveInMillisForLatestRace);
result.setDelayToLiveInMillisForLatestRace(leaderboard.getDelayToLiveInMillis());
result.rows = new HashMap<CompetitorDTO, LeaderboardRowDTO>();
result.hasCarriedPoints = leaderboard.hasCarriedPoints();
result.discardThresholds = leaderboard.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces();
// Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep invalidating
// the ranks cache in TrackedLegImpl. Then problem then is that the sorting based on wind data is repeated for each
// competitor, leading to square effort. We therefore need to compute the leg ranks for those race where leg details
// are requested only once and pass them into getLeaderboardEntryDTO TODO
// are requested only once and pass them into getLeaderboardEntryDTO
final Map<Leg, LinkedHashMap<Competitor, Integer>> legRanksCache = new HashMap<Leg, LinkedHashMap<Competitor,Integer>>();
for (final RaceColumn raceColumn : leaderboard.getRaceColumns()) {
// if details for the column are requested, cache the leg's ranks
@@ -511,7 +490,7 @@ public class SailingServiceImpl extends RemoteServiceServlet implements SailingS
}
}
}
logger.fine("computeLeaderboardByName("+leaderboardName+", "+date+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+
logger.fine("computeLeaderboardByName("+leaderboardName+", "+timePoint+", "+namesOfRaceColumnsForWhichToLoadLegDetails+") took "+
(System.currentTimeMillis()-startOfRequestHandling)+"ms");
return result;
}