mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-17 11:19:15 +00:00
bug6226: test introduction of comparison pre and after behaviour for storing competitors
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
package com.sap.sailing.mongodb.test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.bson.BsonMaximumSizeExceededException;
|
||||
import com.mongodb.client.MongoClient;import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.impl.CompetitorImpl;
|
||||
import com.sap.sailing.domain.common.ManeuverType;
|
||||
import com.sap.sailing.domain.common.RegattaNameAndRaceName;
|
||||
import com.sap.sailing.domain.common.Tack;
|
||||
import com.sap.sailing.domain.maneuverhash.ManeuverRaceFingerprint;
|
||||
import com.sap.sailing.domain.persistence.impl.MongoObjectFactoryImpl;
|
||||
import com.sap.sailing.domain.base.Course;
|
||||
import com.sap.sailing.domain.tracking.Maneuver;
|
||||
import com.sap.sailing.domain.tracking.ManeuverCurveBoundaries;
|
||||
import com.sap.sailing.domain.tracking.impl.ManeuverCurveBoundariesImpl;
|
||||
import com.sap.sailing.domain.tracking.impl.ManeuverWithMainCurveBoundariesImpl;
|
||||
import com.sap.sse.common.impl.DegreeBearingImpl;
|
||||
import com.sap.sse.common.impl.DegreePosition;
|
||||
import com.sap.sse.common.impl.KnotSpeedImpl;
|
||||
import com.sap.sse.common.impl.KnotSpeedWithBearingImpl;
|
||||
import com.sap.sse.common.impl.MillisecondsTimePoint;
|
||||
import com.sap.sse.mongodb.MongoDBConfiguration;
|
||||
|
||||
/**
|
||||
* Regression test for bug 6226: BsonMaximumSizeExceededException when storing maneuvers for
|
||||
* large races with many competitors.
|
||||
*
|
||||
* The old implementation stored all competitors' maneuvers in a single MongoDB document, which
|
||||
* exceeded the 16MB BSON limit for races with ~163 competitors. The fix stores one document per
|
||||
* competitor instead.
|
||||
*
|
||||
* This test uses a synthetic dataset (200 competitors x 500 maneuvers each) to trigger the
|
||||
* original issue and verify that the new implementation handles it correctly.
|
||||
*/
|
||||
public class ManeuverStorageLimitTest {
|
||||
private static final int COMPETITOR_COUNT = 200;
|
||||
private static final int MANEUVERS_PER_COMPETITOR = 500;
|
||||
|
||||
private MongoDBConfiguration dbConfiguration;
|
||||
private MongoDatabase database;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
dbConfiguration = MongoDBConfiguration.getDefaultTestConfiguration();
|
||||
final MongoClient mongoClient = MongoClients.create(dbConfiguration.getMongoClientURI());
|
||||
database = mongoClient.getDatabase(dbConfiguration.getDatabaseName());
|
||||
database.drop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the new per-competitor document storage does not exceed MongoDB's 16MB limit
|
||||
* even for a large race (200 competitors, 500 maneuvers each).
|
||||
* Verifies document count equals competitor count after storing.
|
||||
*/
|
||||
@Test
|
||||
public void testNewImplementationStoresOneDocumentPerCompetitor() {
|
||||
final RegattaNameAndRaceName raceIdentifier = new RegattaNameAndRaceName("505 Pre-Worlds 2014 synthetic", "Race 1");
|
||||
final Map<Competitor, List<Maneuver>> maneuvers = buildSyntheticManeuvers(COMPETITOR_COUNT, MANEUVERS_PER_COMPETITOR);
|
||||
final ManeuverRaceFingerprint fingerprint = buildMockFingerprint();
|
||||
final Course course = mock(Course.class);
|
||||
new MongoObjectFactoryImpl(database).storeManeuvers(raceIdentifier, fingerprint, course, maneuvers);
|
||||
final MongoCollection<Document> collection = database.getCollection("MANEUVERS");
|
||||
final long documentCount = collection.countDocuments();
|
||||
assertEquals(COMPETITOR_COUNT, documentCount,
|
||||
"Expected one document per competitor in the MANEUVERS collection");
|
||||
}
|
||||
|
||||
/**
|
||||
* Demonstrates that the old single-document implementation fails with MongoWriteException
|
||||
* (BsonMaximumSizeExceededException) for the same large dataset.
|
||||
*
|
||||
* The old implementation called replaceOne with a single document containing all competitors,
|
||||
* which exceeds MongoDB's 16MB BSON limit for COMPETITOR_COUNT=200, MANEUVERS_PER_COMPETITOR=500,
|
||||
* causing a BsonMaximumSizeExceededException.
|
||||
*/
|
||||
@Test
|
||||
public void testOldImplementationFailsWithBsonLimitExceeded() {
|
||||
final RegattaNameAndRaceName raceIdentifier = new RegattaNameAndRaceName("505 Pre-Worlds 2014 synthetic", "Race 1");
|
||||
final Map<Competitor, List<Maneuver>> maneuvers = buildSyntheticManeuvers(COMPETITOR_COUNT, MANEUVERS_PER_COMPETITOR);
|
||||
final ManeuverRaceFingerprint fingerprint = buildMockFingerprint();
|
||||
final Course course = mock(Course.class);
|
||||
assertThrows(BsonMaximumSizeExceededException.class, () ->
|
||||
new OldMongoObjectFactoryForBug6226Test(database).storeManeuvers(raceIdentifier, fingerprint, course, maneuvers),
|
||||
"Old implementation must throw BsonMaximumSizeExceededException due to BSON 16MB limit"
|
||||
);
|
||||
}
|
||||
|
||||
private ManeuverRaceFingerprint buildMockFingerprint() {
|
||||
final ManeuverRaceFingerprint fingerprint = mock(ManeuverRaceFingerprint.class);
|
||||
final JSONObject fingerprintJson = new JSONObject();
|
||||
fingerprintJson.put("DUMMY", "fingerprint");
|
||||
when(fingerprint.toJson()).thenReturn(fingerprintJson);
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
private Map<Competitor, List<Maneuver>> buildSyntheticManeuvers(final int competitorCount, final int maneuversPerCompetitor) {
|
||||
final Map<Competitor, List<Maneuver>> result = new HashMap<>();
|
||||
for (int i = 0; i < competitorCount; i++) {
|
||||
final Competitor competitor = mock(CompetitorImpl.class);
|
||||
when(competitor.getId()).thenReturn("competitor-" + i);
|
||||
result.put(competitor, buildManeuverList(maneuversPerCompetitor));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Maneuver> buildManeuverList(final int count) {
|
||||
final List<Maneuver> result = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
result.add(buildManeuver(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Maneuver buildManeuver(final int index) {
|
||||
final long baseMillis = 1_400_000_000_000L + index * 30_000L;
|
||||
final ManeuverCurveBoundaries curveBoundaries = new ManeuverCurveBoundariesImpl(
|
||||
new MillisecondsTimePoint(baseMillis),
|
||||
new MillisecondsTimePoint(baseMillis + 15_000L),
|
||||
new KnotSpeedWithBearingImpl(6.0, new DegreeBearingImpl(45.0)),
|
||||
new KnotSpeedWithBearingImpl(5.5, new DegreeBearingImpl(315.0)),
|
||||
90.0,
|
||||
new KnotSpeedImpl(4.0),
|
||||
new KnotSpeedImpl(6.5));
|
||||
return new ManeuverWithMainCurveBoundariesImpl(
|
||||
ManeuverType.TACK,
|
||||
Tack.STARBOARD,
|
||||
new DegreePosition(47.5 + index * 0.001, 9.0 + index * 0.001),
|
||||
new MillisecondsTimePoint(baseMillis + 7_500L),
|
||||
curveBoundaries,
|
||||
curveBoundaries,
|
||||
5.0,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.sap.sailing.mongodb.test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import com.mongodb.client.model.ReplaceOptions;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.Course;
|
||||
import com.sap.sailing.domain.common.RaceIdentifier;
|
||||
import com.sap.sailing.domain.maneuverhash.ManeuverRaceFingerprint;
|
||||
import com.sap.sailing.domain.persistence.FieldNames;
|
||||
import com.sap.sailing.domain.persistence.impl.CollectionNames;
|
||||
import com.sap.sailing.domain.tracking.Maneuver;
|
||||
|
||||
/**
|
||||
* Reproduces the pre-bug-6226-fix behaviour of {@code MongoObjectFactoryImpl.storeManeuvers()}:
|
||||
* all competitors' maneuvers were packed into a single MongoDB document, keyed only by
|
||||
* (EVENT_NAME, RACE_NAME). For races with many competitors this document easily exceeds MongoDB's
|
||||
* 16 MB BSON limit, causing a {@code MongoWriteException}.
|
||||
*
|
||||
* This class exists solely to let {@link ManeuverStorageLimitTest} assert that the old code path
|
||||
* fails with the expected exception. It must never be used in production.
|
||||
*/
|
||||
class OldMongoObjectFactoryForBug6226Test {
|
||||
private final MongoDatabase database;
|
||||
|
||||
OldMongoObjectFactoryForBug6226Test(final MongoDatabase database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
void storeManeuvers(final RaceIdentifier raceIdentifier, final ManeuverRaceFingerprint fingerprint,
|
||||
final Course course, final Map<Competitor, List<Maneuver>> maneuvers) {
|
||||
final MongoCollection<Document> maneuverCollection = database.getCollection(CollectionNames.MANEUVERS.name());
|
||||
final Document query = new Document();
|
||||
query.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName());
|
||||
query.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName());
|
||||
final Document result = new Document();
|
||||
final Document fingerprintDoc = Document.parse(fingerprint.toJson().toString());
|
||||
result.put(FieldNames.MANEUVER_FINGERPRINT.name(), fingerprintDoc);
|
||||
result.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName());
|
||||
result.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName());
|
||||
final List<Document> allCompetitorDocs = buildAllCompetitorManeuverDocs(maneuvers);
|
||||
result.put(FieldNames.MANEUVERS.name(), allCompetitorDocs);
|
||||
maneuverCollection.replaceOne(query, result, new ReplaceOptions().upsert(true));
|
||||
}
|
||||
|
||||
private List<Document> buildAllCompetitorManeuverDocs(final Map<Competitor, List<Maneuver>> maneuvers) {
|
||||
final List<Document> result = new ArrayList<>();
|
||||
for (final Entry<Competitor, List<Maneuver>> entry : maneuvers.entrySet()) {
|
||||
final Document competitorDoc = new Document();
|
||||
competitorDoc.put(FieldNames.COMPETITOR_ID.name(), entry.getKey().getId());
|
||||
final List<Document> maneuverList = new ArrayList<>();
|
||||
if (entry.getValue() != null) {
|
||||
for (final Maneuver maneuver : entry.getValue()) {
|
||||
maneuverList.add(buildManeuverDoc(maneuver));
|
||||
}
|
||||
}
|
||||
competitorDoc.put(FieldNames.MANEUVERS.name(), maneuverList);
|
||||
result.add(competitorDoc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Document buildManeuverDoc(final Maneuver maneuver) {
|
||||
final Document doc = new Document();
|
||||
doc.put(FieldNames.SIMPLE_CLASS_NAME.name(), maneuver.getClass().getSimpleName());
|
||||
doc.put(FieldNames.TYPE.name(), maneuver.getType().name());
|
||||
doc.put(FieldNames.TACK.name(), maneuver.getNewTack() == null ? null : maneuver.getNewTack().name());
|
||||
doc.put(FieldNames.POSITION_LAT_RAD.name(), maneuver.getPosition().getLatRad());
|
||||
doc.put(FieldNames.POSITION_LNG_RAD.name(), maneuver.getPosition().getLngRad());
|
||||
doc.put(FieldNames.TIMEPOINT.name(), maneuver.getTimePoint().asMillis());
|
||||
doc.put(FieldNames.MAIN_CURVE_BOUNDARIES.name(), buildCurveBoundariesDoc(maneuver.getMainCurveBoundaries()));
|
||||
doc.put(FieldNames.MANEUVER_CURVE_WITH_STABLE_SPEED_AND_COURSE_BOUNDERIES.name(),
|
||||
buildCurveBoundariesDoc(maneuver.getManeuverCurveWithStableSpeedAndCourseBoundaries()));
|
||||
doc.put(FieldNames.MAX_TURNING_RATE_IN_DEGREE_PER_SECOUND.name(), maneuver.getMaxTurningRateInDegreesPerSecond());
|
||||
doc.put(FieldNames.INDEX_OF_PASSED_WAYPOINT.name(), -1);
|
||||
doc.put(FieldNames.TIME_AS_MILLIS.name(), maneuver.getDuration().asMillis());
|
||||
doc.put(FieldNames.MANEUVER_LOSS.name(), null);
|
||||
return doc;
|
||||
}
|
||||
|
||||
private Document buildCurveBoundariesDoc(final com.sap.sailing.domain.tracking.ManeuverCurveBoundaries f) {
|
||||
final Document d = new Document();
|
||||
d.put(FieldNames.MANEUVER_TIMEPOINT_BEFORE.name(), f.getTimePointBefore().asMillis());
|
||||
d.put(FieldNames.MANEUVER_TIMEPOINT_AFTER.name(), f.getTimePointAfter().asMillis());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_BEFORE_DEGREES.name(), f.getSpeedWithBearingBefore().getBearing().getDegrees());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_BEFORE_SPEED.name(), f.getSpeedWithBearingBefore().getKnots());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_AFTER_DEGREES.name(), f.getSpeedWithBearingAfter().getBearing().getDegrees());
|
||||
d.put(FieldNames.MANEUVER_SPEED_WITH_BEARING_AFTER_SPEED_IN_KNOTS.name(), f.getSpeedWithBearingAfter().getKnots());
|
||||
d.put(FieldNames.MANEUVER_DIRECTION_CHANGE_IN_DEGREES.name(), f.getDirectionChangeInDegrees());
|
||||
d.put(FieldNames.MANEUVER_LOWEST_SPEED_IN_KNOTS.name(), f.getLowestSpeed().getKnots());
|
||||
d.put(FieldNames.MANEUVER_HIGHEST_SPEED_IN_KNOTS.name(), f.getHighestSpeed().getKnots());
|
||||
return d;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,16 @@
|
||||
<div class="mainContent">
|
||||
<h2 class="releaseHeadline">Release Notes - Administration Console</h2>
|
||||
<div class="innerContent">
|
||||
<h2 class="articleSubheadline">June 2026</h2>
|
||||
<ul class="bulletList">
|
||||
<li>Fixed a server crash occurring when computing maneuvers for races with a large number of competitors
|
||||
(e.g. 163 competitors in 505 Pre-Worlds 2014). All competitors' maneuvers were previously stored in a
|
||||
single MongoDB document, which exceeded MongoDB's 16 MB BSON document size limit and caused the
|
||||
server to crash with a <tt>BsonMaximumSizeExceededException</tt>. Maneuvers are now stored in one
|
||||
document per competitor, keeping each document well within the size limit regardless of fleet size.
|
||||
See also <a href="https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=6226">bug 6226</a>.
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="articleSubheadline">April 2026</h2>
|
||||
<ul class="bulletList">
|
||||
<li>Tables with multi-selection support now have a select/de-select all checkbox in the table header.
|
||||
|
||||
Reference in New Issue
Block a user