Skip to content
This repository was archived by the owner on May 16, 2023. It is now read-only.

Commit 74c8785

Browse files
authored
Add Trace Time Interval Warning Retention Policy (#1342)
* Update documentation * Add tests for trace time warning retention policy * Add trace time warning retention policy
1 parent 84cf122 commit 74c8785

File tree

11 files changed

+410
-183
lines changed

11 files changed

+410
-183
lines changed

common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/TraceTimeIntervalWarningRepository.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,22 @@ boolean saveDoNothingOnConflict(@Param("trace_location_id") byte[] traceLocation
2121
@Param("start_interval_number") Integer startIntervalNumber, @Param("period") Integer period,
2222
@Param("transmission_risk_level") Integer transmissionRiskLevel,
2323
@Param("submission_timestamp") Integer submissionTimestamp);
24+
25+
/**
26+
* Counts all entries that have a submission timestamp older than the specified one.
27+
*
28+
* @param submissionTimestamp The submission timestamp up to which entries will be expired.
29+
* @return The number of expired trace time warnings.
30+
*/
31+
@Query("SELECT COUNT(*) FROM trace_time_interval_warning WHERE submission_timestamp<:threshold")
32+
int countOlderThan(@Param("threshold") long submissionTimestamp);
33+
34+
/**
35+
* Deletes all entries that have a submission timestamp older than the specified one.
36+
*
37+
* @param submissionTimestamp The submission timestamp up to which entries will be deleted.
38+
*/
39+
@Modifying
40+
@Query("DELETE FROM trace_time_interval_warning WHERE submission_timestamp<:threshold")
41+
void deleteOlderThan(@Param("threshold") long submissionTimestamp);
2442
}

common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/TraceTimeIntervalWarningService.java

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package app.coronawarn.server.common.persistence.service;
22

3+
import static app.coronawarn.server.common.persistence.domain.validation.ValidSubmissionTimestampValidator.SECONDS_PER_HOUR;
4+
import static java.time.ZoneOffset.UTC;
5+
36
import app.coronawarn.server.common.persistence.domain.TraceTimeIntervalWarning;
47
import app.coronawarn.server.common.persistence.repository.TraceTimeIntervalWarningRepository;
5-
import app.coronawarn.server.common.persistence.service.utils.checkins.CheckinsDateSpecification;
68
import app.coronawarn.server.common.persistence.service.utils.checkins.FakeCheckinsGenerator;
79
import app.coronawarn.server.common.protocols.internal.pt.CheckIn;
810
import com.google.protobuf.ByteString;
911
import java.security.MessageDigest;
1012
import java.security.NoSuchAlgorithmException;
1113
import java.time.Instant;
14+
import java.time.LocalDateTime;
1215
import java.util.ArrayList;
1316
import java.util.Collection;
1417
import java.util.List;
@@ -28,15 +31,16 @@ public class TraceTimeIntervalWarningService {
2831
private static final Logger logger =
2932
LoggerFactory.getLogger(TraceTimeIntervalWarningService.class);
3033

31-
3234
private final TraceTimeIntervalWarningRepository traceTimeIntervalWarningRepo;
3335
private final FakeCheckinsGenerator fakeCheckinsGenerator;
3436
private final MessageDigest hashAlgorithm;
3537

3638
/**
3739
* Constructs the service instance.
40+
*
3841
* @param traceTimeIntervalWarningRepo Repository for {@link TraceTimeIntervalWarning} entities.
39-
* @param fakeCheckinsGenerator Generator of fake data that gets stored side by side with the real checkin data.
42+
* @param fakeCheckinsGenerator Generator of fake data that gets stored side by side with the real checkin
43+
* data.
4044
* @throws NoSuchAlgorithmException In case the MessageDigest used in hashing can not be instantiated.
4145
*/
4246
public TraceTimeIntervalWarningService(
@@ -48,16 +52,17 @@ public TraceTimeIntervalWarningService(
4852
}
4953

5054
/**
51-
* Store the given checkin data as {@link TraceTimeIntervalWarning} entities. Returns the number
52-
* of inserted entities, which is useful for the case where there might be conflicts with the
53-
* table constraints during the db save operations.
55+
* Store the given checkin data as {@link TraceTimeIntervalWarning} entities. Returns the number of inserted entities,
56+
* which is useful for the case where there might be conflicts with the table constraints during the db save
57+
* operations.
5458
*/
5559
@Transactional
56-
public int saveCheckins(List<CheckIn> checkins) {
57-
return saveCheckins(checkins, this::hashLocationId);
60+
public int saveCheckins(List<CheckIn> checkins, int submissionTimestamp) {
61+
return saveCheckins(checkins, this::hashLocationId, submissionTimestamp);
5862
}
5963

60-
private int saveCheckins(List<CheckIn> checkins, Function<ByteString, byte[]> idHashGenerator) {
64+
private int saveCheckins(List<CheckIn> checkins, Function<ByteString, byte[]> idHashGenerator,
65+
int submissionTimestamp) {
6166
int numberOfInsertedTraceWarnings = 0;
6267

6368
for (CheckIn checkin : checkins) {
@@ -66,8 +71,7 @@ private int saveCheckins(List<CheckIn> checkins, Function<ByteString, byte[]> id
6671
.saveDoNothingOnConflict(hashId, checkin.getStartIntervalNumber(),
6772
checkin.getEndIntervalNumber() - checkin.getStartIntervalNumber(),
6873
checkin.getTransmissionRiskLevel(),
69-
CheckinsDateSpecification.HOUR_SINCE_EPOCH_DERIVATION
70-
.apply(Instant.now().getEpochSecond()));
74+
submissionTimestamp);
7175

7276
if (traceWarningInsertedSuccessfully) {
7377
numberOfInsertedTraceWarnings++;
@@ -78,7 +82,7 @@ private int saveCheckins(List<CheckIn> checkins, Function<ByteString, byte[]> id
7882
if (conflictingTraceWarnings > 0) {
7983
logger.warn(
8084
"{} out of {} TraceTimeIntervalWarnings conflicted with existing "
81-
+ "database entries or had errors while storing "
85+
+ "database entries or had errors while storing "
8286
+ "and were ignored.",
8387
conflictingTraceWarnings, checkins.size());
8488
}
@@ -87,18 +91,17 @@ private int saveCheckins(List<CheckIn> checkins, Function<ByteString, byte[]> id
8791
}
8892

8993
/**
90-
* For each checkin in the given list, generate other fake checkin data based on the passed in
91-
* number and store everything as {@link TraceTimeIntervalWarning} entities. Returns the number of
92-
* inserted entities which is useful for the case where there might be conflicts with the table
93-
* constraints during the db save operations.
94+
* For each checkin in the given list, generate other fake checkin data based on the passed in number and store
95+
* everything as {@link TraceTimeIntervalWarning} entities. Returns the number of inserted entities which is useful
96+
* for the case where there might be conflicts with the table constraints during the db save operations.
9497
*/
9598
@Transactional
9699
public int saveCheckinsWithFakeData(List<CheckIn> originalCheckins, int numberOfFakesToCreate,
97-
byte[] pepper) {
100+
byte[] pepper, int submissionTimestamp) {
98101
List<CheckIn> allCheckins = new ArrayList<>(originalCheckins);
99102
allCheckins.addAll(fakeCheckinsGenerator.generateFakeCheckins(originalCheckins,
100103
numberOfFakesToCreate, pepper));
101-
return saveCheckins(allCheckins, this::hashLocationId);
104+
return saveCheckins(allCheckins, this::hashLocationId, submissionTimestamp);
102105
}
103106

104107
/**
@@ -114,4 +117,27 @@ public Collection<TraceTimeIntervalWarning> getTraceTimeIntervalWarnings() {
114117
private byte[] hashLocationId(ByteString locationId) {
115118
return hashAlgorithm.digest(locationId.toByteArray());
116119
}
120+
121+
/**
122+
* Deletes all trace time warning entries which have a submission timestamp that is older than the specified number
123+
* of days.
124+
*
125+
* @param daysToRetain the number of days until which trace time warnings will be retained.
126+
* @throws IllegalArgumentException if {@code daysToRetain} is negative.
127+
*/
128+
@Transactional
129+
public void applyRetentionPolicy(int daysToRetain) {
130+
if (daysToRetain < 0) {
131+
throw new IllegalArgumentException("Number of days to retain must be greater or equal to 0.");
132+
}
133+
134+
long threshold = LocalDateTime
135+
.ofInstant(Instant.now(), UTC)
136+
.minusDays(daysToRetain)
137+
.toEpochSecond(UTC) / SECONDS_PER_HOUR;
138+
int numberOfDeletions = traceTimeIntervalWarningRepo.countOlderThan(threshold);
139+
logger.info("Deleting {} trace time warning(s) with a submission timestamp older than {} day(s) ago.",
140+
numberOfDeletions, daysToRetain);
141+
traceTimeIntervalWarningRepo.deleteOlderThan(threshold);
142+
}
117143
}

common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/TraceTimeIntervalWarningServiceTest.java

Lines changed: 110 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
package app.coronawarn.server.common.persistence.service;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatCode;
5+
import static org.assertj.core.api.Assertions.catchThrowable;
6+
import static org.junit.Assert.assertArrayEquals;
7+
import static org.junit.Assert.assertEquals;
8+
39
import app.coronawarn.server.common.persistence.domain.TraceTimeIntervalWarning;
410
import app.coronawarn.server.common.persistence.repository.TraceTimeIntervalWarningRepository;
511
import app.coronawarn.server.common.persistence.service.utils.checkins.CheckinsDateSpecification;
612
import app.coronawarn.server.common.protocols.internal.pt.CheckIn;
713
import com.google.protobuf.ByteString;
8-
import org.junit.jupiter.api.BeforeEach;
9-
import org.junit.jupiter.api.Test;
10-
import org.springframework.beans.factory.annotation.Autowired;
11-
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
12-
import org.testcontainers.shaded.org.bouncycastle.util.encoders.Hex;
1314
import java.security.MessageDigest;
1415
import java.security.NoSuchAlgorithmException;
1516
import java.security.SecureRandom;
1617
import java.time.Instant;
17-
import java.util.ArrayList;
18+
import java.util.Collection;
1819
import java.util.Collections;
1920
import java.util.Comparator;
2021
import java.util.List;
22+
import java.util.concurrent.TimeUnit;
2123
import java.util.stream.Collectors;
2224
import java.util.stream.StreamSupport;
23-
24-
import static org.junit.Assert.*;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.DisplayName;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.ValueSource;
30+
import org.springframework.beans.factory.annotation.Autowired;
31+
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
32+
import org.testcontainers.shaded.org.bouncycastle.util.encoders.Hex;
2533

2634
@DataJdbcTest
2735
class TraceTimeIntervalWarningServiceTest {
@@ -32,70 +40,35 @@ class TraceTimeIntervalWarningServiceTest {
3240
@Autowired
3341
TraceTimeIntervalWarningRepository traceWarningsRepository;
3442

43+
private int currentTimestamp;
44+
3545
@BeforeEach
3646
public void setup() {
3747
traceWarningsRepository.deleteAll();
48+
currentTimestamp = CheckinsDateSpecification.HOUR_SINCE_EPOCH_DERIVATION.apply(Instant.now().getEpochSecond());
3849
}
3950

4051
@Test
4152
void testStorage() {
4253
List<CheckIn> checkins = getRandomTestData();
43-
traceWarningsService.saveCheckins(checkins);
54+
traceWarningsService.saveCheckins(checkins, currentTimestamp);
4455

4556
List<TraceTimeIntervalWarning> actualTraceWarningsStored =
4657
StreamSupport.stream(traceWarningsRepository.findAll().spliterator(), false)
4758
.collect(Collectors.toList());
4859

49-
assertCheckinsAndWarningsAreEqual(new ArrayList<>(checkins), actualTraceWarningsStored);
60+
assertCheckinsAndWarningsAreEqual(checkins, actualTraceWarningsStored);
5061
}
5162

5263
@Test
5364
void testStorageWithRandomPadding() {
5465
List<CheckIn> checkins = getRandomTestData();
55-
traceWarningsService.saveCheckinsWithFakeData(checkins, 2, randomHashPepper());
66+
traceWarningsService.saveCheckinsWithFakeData(checkins, 2, randomHashPepper(), currentTimestamp);
5667

5768
List<TraceTimeIntervalWarning> actualTraceWarningsStored =
5869
StreamSupport.stream(traceWarningsRepository.findAll().spliterator(), false)
5970
.collect(Collectors.toList());
60-
assertTrue(actualTraceWarningsStored.size() == checkins.size() + checkins.size() * 2);
61-
}
62-
63-
private List<CheckIn> getRandomTestData() {
64-
List<CheckIn> checkins = List.of(
65-
CheckIn.newBuilder().setStartIntervalNumber(0).setEndIntervalNumber(1)
66-
.setTransmissionRiskLevel(1)
67-
.setLocationId(ByteString.copyFromUtf8("uuid1"))
68-
.build(),
69-
CheckIn.newBuilder().setStartIntervalNumber(23).setEndIntervalNumber(30)
70-
.setTransmissionRiskLevel(2)
71-
.setLocationId(ByteString.copyFromUtf8("uuid1"))
72-
.build(),
73-
CheckIn.newBuilder().setStartIntervalNumber(40).setEndIntervalNumber(50)
74-
.setTransmissionRiskLevel(3)
75-
.setLocationId(ByteString.copyFromUtf8("uuid1"))
76-
.build());
77-
return checkins;
78-
}
79-
80-
private void assertCheckinsAndWarningsAreEqual(List<CheckIn> checkins,
81-
List<TraceTimeIntervalWarning> actualTraceWarningsStored) {
82-
83-
assertEquals(checkins.size(), actualTraceWarningsStored.size());
84-
85-
Collections.sort(checkins, Comparator.comparing(CheckIn::getTransmissionRiskLevel));
86-
Collections.sort(actualTraceWarningsStored,
87-
Comparator.comparing(TraceTimeIntervalWarning::getTransmissionRiskLevel));
88-
89-
for (int i = 0; i < checkins.size(); i++) {
90-
CheckIn checkin = checkins.get(i);
91-
TraceTimeIntervalWarning warning = actualTraceWarningsStored.get(i);
92-
assertEquals(checkin.getTransmissionRiskLevel(),
93-
warning.getTransmissionRiskLevel().intValue());
94-
assertEquals(checkin.getStartIntervalNumber(), warning.getStartIntervalNumber().intValue());
95-
assertEquals(checkin.getEndIntervalNumber() - checkin.getStartIntervalNumber(), warning.getPeriod().intValue());
96-
assertArrayEquals(hashLocationId(checkin.getLocationId()),
97-
warning.getTraceLocationId());
98-
}
71+
assertEquals(actualTraceWarningsStored.size(), checkins.size() + checkins.size() * 2);
9972
}
10073

10174
@Test
@@ -115,7 +88,7 @@ void testSortedRetrievalResult() {
11588
CheckinsDateSpecification.HOUR_SINCE_EPOCH_DERIVATION
11689
.apply(Instant.now().getEpochSecond()) - 10);
11790

118-
List<CheckIn> checkins = new ArrayList<>(List.of(
91+
List<CheckIn> checkins = new java.util.ArrayList<>(List.of(
11992
CheckIn.newBuilder().setStartIntervalNumber(56).setEndIntervalNumber(66)
12093
.setTransmissionRiskLevel(3)
12194
.setLocationId(ByteString.copyFromUtf8("sorted-uuid2"))
@@ -128,8 +101,7 @@ void testSortedRetrievalResult() {
128101
// Reverse as we tempered with submission timestamp
129102
Collections.reverse(checkins);
130103

131-
List<TraceTimeIntervalWarning> checkinsFromDB = new ArrayList<>(
132-
traceWarningsService.getTraceTimeIntervalWarnings());
104+
var checkinsFromDB = traceWarningsService.getTraceTimeIntervalWarnings();
133105

134106
assertCheckinsAndWarningsAreEqual(checkins, checkinsFromDB);
135107
}
@@ -149,11 +121,95 @@ public void testHashingOfTraceLocationId() {
149121
assertEquals("0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230", s);
150122
}
151123

124+
@DisplayName("Assert a positive retention period is accepted.")
125+
@ValueSource(ints = {0, 1, Integer.MAX_VALUE})
126+
@ParameterizedTest
127+
void testApplyRetentionPolicyForValidNumberOfDays(int daysToRetain) {
128+
assertThatCode(() -> traceWarningsService.applyRetentionPolicy(daysToRetain))
129+
.doesNotThrowAnyException();
130+
}
131+
132+
@DisplayName("Assert a negative retention period is rejected.")
133+
@ValueSource(ints = {Integer.MIN_VALUE, -1})
134+
@ParameterizedTest
135+
void testApplyRetentionPolicyForNegativeNumberOfDays(int daysToRetain) {
136+
assertThat(catchThrowable(() -> traceWarningsService.applyRetentionPolicy(daysToRetain)))
137+
.isInstanceOf(IllegalArgumentException.class);
138+
}
139+
140+
@Test
141+
void testApplyRetentionPolicyForEmptyDb() {
142+
traceWarningsService.applyRetentionPolicy(1);
143+
var actKeys = traceWarningsService.getTraceTimeIntervalWarnings();
144+
assertThat(actKeys).isEmpty();
145+
}
146+
147+
@Test
148+
void testApplyRetentionPolicyForNotApplicableEntries() {
149+
var expKeys = getRandomTestData();
150+
151+
traceWarningsService.saveCheckins(expKeys, currentTimestamp);
152+
traceWarningsService.applyRetentionPolicy(1);
153+
var actKeys = traceWarningsService.getTraceTimeIntervalWarnings();
154+
155+
assertCheckinsAndWarningsAreEqual(expKeys, actKeys);
156+
}
157+
158+
@Test
159+
void testApplyRetentionPolicyForOneApplicableEntry() {
160+
var keys = getRandomTestData();
161+
162+
traceWarningsService.saveCheckins(keys, currentTimestamp - (int) TimeUnit.DAYS.toHours(1) - 1);
163+
traceWarningsService.applyRetentionPolicy(1);
164+
var actKeys = traceWarningsService.getTraceTimeIntervalWarnings();
165+
166+
assertThat(actKeys).isEmpty();
167+
}
168+
169+
private List<CheckIn> getRandomTestData() {
170+
return List.of(
171+
CheckIn.newBuilder().setStartIntervalNumber(0).setEndIntervalNumber(1)
172+
.setTransmissionRiskLevel(1)
173+
.setLocationId(ByteString.copyFromUtf8("uuid1"))
174+
.build(),
175+
CheckIn.newBuilder().setStartIntervalNumber(23).setEndIntervalNumber(30)
176+
.setTransmissionRiskLevel(2)
177+
.setLocationId(ByteString.copyFromUtf8("uuid1"))
178+
.build(),
179+
CheckIn.newBuilder().setStartIntervalNumber(40).setEndIntervalNumber(50)
180+
.setTransmissionRiskLevel(3)
181+
.setLocationId(ByteString.copyFromUtf8("uuid1"))
182+
.build());
183+
}
184+
185+
private void assertCheckinsAndWarningsAreEqual(Collection<CheckIn> checkins,
186+
Collection<TraceTimeIntervalWarning> actualTraceWarningsStored) {
187+
188+
assertEquals(checkins.size(), actualTraceWarningsStored.size());
189+
190+
var sortedCheckins = checkins.stream()
191+
.sorted(Comparator.comparing(CheckIn::getTransmissionRiskLevel))
192+
.collect(Collectors.toList());
193+
var sortedTraceTimeWarnings = actualTraceWarningsStored.stream()
194+
.sorted(Comparator.comparing(TraceTimeIntervalWarning::getTransmissionRiskLevel))
195+
.collect(Collectors.toList());
196+
197+
for (int i = 0; i < checkins.size(); i++) {
198+
CheckIn checkin = sortedCheckins.get(i);
199+
TraceTimeIntervalWarning warning = sortedTraceTimeWarnings.get(i);
200+
assertEquals(checkin.getTransmissionRiskLevel(),
201+
warning.getTransmissionRiskLevel().intValue());
202+
assertEquals(checkin.getStartIntervalNumber(), warning.getStartIntervalNumber().intValue());
203+
assertEquals(checkin.getEndIntervalNumber() - checkin.getStartIntervalNumber(), warning.getPeriod().intValue());
204+
assertArrayEquals(hashLocationId(checkin.getLocationId()),
205+
warning.getTraceLocationId());
206+
}
207+
}
152208

153209
private byte[] hashLocationId(ByteString locationId) {
154210
try {
155211
return MessageDigest.getInstance("SHA-256").digest(locationId.toByteArray());
156-
} catch (NoSuchAlgorithmException e) {
212+
} catch (NoSuchAlgorithmException ignored) {
157213
}
158214
return new byte[0];
159215
}

0 commit comments

Comments
 (0)