mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-21 11:40:01 +00:00
first attempt at keeping computing live leaderboard results in LiveLeaderboardUpdater in the background
This commit is contained in:
1 parent
9a9440018e
commit
20d12939f8
12 files changed
+322
-116
No files matched your search
Regular → Executable
File mode changed.
Regular → Executable
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();
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
-26
@@ -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(){
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+1
-5
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -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();
|
||||
|
||||
|
||||
+5
-6
@@ -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);
|
||||
|
||||
|
||||
+17
-17
@@ -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) {
|
||||
|
||||
Executable
+212
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+37
-58
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user