Skip to content

Commit 16ee0e0

Browse files
bpfosterchristophstrobl
authored andcommitted
Add support for collection expiration to @timeseries.
Closes #4099
1 parent da378a3 commit 16ee0e0

File tree

5 files changed

+85
-5
lines changed

5 files changed

+85
-5
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
* @author Christoph Strobl
3939
* @author Mark Paluch
4040
* @author Andreas Zink
41+
* @author Ben Foster
4142
*/
4243
public class CollectionOptions {
4344

@@ -629,13 +630,15 @@ public static class TimeSeriesOptions {
629630

630631
private final GranularityDefinition granularity;
631632

632-
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity) {
633+
private final int expireAfterSeconds;
633634

635+
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity, int expireAfterSeconds) {
634636
Assert.hasText(timeField, "Time field must not be empty or null");
635637

636638
this.timeField = timeField;
637639
this.metaField = metaField;
638640
this.granularity = granularity;
641+
this.expireAfterSeconds = expireAfterSeconds;
639642
}
640643

641644
/**
@@ -647,7 +650,7 @@ private TimeSeriesOptions(String timeField, @Nullable String metaField, Granular
647650
* @return new instance of {@link TimeSeriesOptions}.
648651
*/
649652
public static TimeSeriesOptions timeSeries(String timeField) {
650-
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT);
653+
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT, -1);
651654
}
652655

653656
/**
@@ -660,7 +663,7 @@ public static TimeSeriesOptions timeSeries(String timeField) {
660663
* @return new instance of {@link TimeSeriesOptions}.
661664
*/
662665
public TimeSeriesOptions metaField(String metaField) {
663-
return new TimeSeriesOptions(timeField, metaField, granularity);
666+
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfterSeconds);
664667
}
665668

666669
/**
@@ -671,7 +674,17 @@ public TimeSeriesOptions metaField(String metaField) {
671674
* @see Granularity
672675
*/
673676
public TimeSeriesOptions granularity(GranularityDefinition granularity) {
674-
return new TimeSeriesOptions(timeField, metaField, granularity);
677+
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfterSeconds);
678+
}
679+
680+
/**
681+
* Select the expireAfterSeconds parameter to define automatic removal of documents older than a specified
682+
* number of seconds.
683+
*
684+
* @return new instance of {@link TimeSeriesOptions}.
685+
*/
686+
public TimeSeriesOptions expireAfterSeconds(int expireAfterSeconds) {
687+
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfterSeconds);
675688
}
676689

677690
/**
@@ -697,6 +710,13 @@ public GranularityDefinition getGranularity() {
697710
return granularity;
698711
}
699712

713+
/**
714+
* @return {@literal -1} if not specified
715+
*/
716+
public int getExpireAfterSeconds() {
717+
return expireAfterSeconds;
718+
}
719+
700720
@Override
701721
public String toString() {
702722

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.LinkedHashMap;
2121
import java.util.Map;
2222
import java.util.Optional;
23+
import java.util.concurrent.TimeUnit;
2324

2425
import org.bson.BsonNull;
2526
import org.bson.Document;
@@ -74,6 +75,7 @@
7475
* @author Oliver Gierke
7576
* @author Mark Paluch
7677
* @author Christoph Strobl
78+
* @author Ben Foster
7779
* @since 2.1
7880
* @see MongoTemplate
7981
* @see ReactiveMongoTemplate
@@ -354,6 +356,10 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
354356
options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase()));
355357
}
356358

359+
if (it.getExpireAfterSeconds() >= 0) {
360+
result.expireAfter(it.getExpireAfterSeconds(), TimeUnit.SECONDS);
361+
}
362+
357363
result.timeSeriesOptions(options);
358364
});
359365

@@ -1077,6 +1083,9 @@ public CollectionOptions getCollectionOptions() {
10771083
if (!Granularity.DEFAULT.equals(timeSeries.granularity())) {
10781084
options = options.granularity(timeSeries.granularity());
10791085
}
1086+
if (timeSeries.expireAfterSeconds() >= 0) {
1087+
options = options.expireAfterSeconds(timeSeries.expireAfterSeconds());
1088+
}
10801089
collectionOptions = collectionOptions.timeSeries(options);
10811090
}
10821091

@@ -1091,7 +1100,8 @@ public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) {
10911100
if (StringUtils.hasText(source.getMetaField())) {
10921101
target = target.metaField(mappedNameOrDefault(source.getMetaField()));
10931102
}
1094-
return target.granularity(source.getGranularity());
1103+
return target.granularity(source.getGranularity())
1104+
.expireAfterSeconds(source.getExpireAfterSeconds());
10951105
}
10961106

10971107
private String mappedNameOrDefault(String name) {

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/TimeSeries.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* Identifies a domain object to be persisted to a MongoDB Time Series collection.
2929
*
3030
* @author Christoph Strobl
31+
* @author Ben Foster
3132
* @since 3.3
3233
* @see <a href="https://docs.mongodb.com/manual/core/timeseries-collections">https://docs.mongodb.com/manual/core/timeseries-collections</a>
3334
*/
@@ -83,4 +84,12 @@
8384
@AliasFor(annotation = Document.class, attribute = "collation")
8485
String collation() default "";
8586

87+
/**
88+
* Configures the number of seconds after which the collection should expire. Defaults to -1 for no expiry.
89+
*
90+
* @return {@literal -1} by default.
91+
* @see <a href=
92+
* "https://www.mongodb.com/docs/manual/core/timeseries/timeseries-automatic-removal/#set-up-automatic-removal-for-time-series-collections--ttl-</a>
93+
*/
94+
int expireAfterSeconds() default -1;
8695
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
* @author Roman Puchkovskiy
142142
* @author Yadhukrishna S Pai
143143
* @author Jakub Zurawa
144+
* @author Ben Foster
144145
*/
145146
@MockitoSettings(strictness = Strictness.LENIENT)
146147
public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@@ -2386,6 +2387,18 @@ void createCollectionShouldSetUpTimeSeries() {
23862387
.granularity(TimeSeriesGranularity.HOURS).toString());
23872388
}
23882389

2390+
@Test // GH-4099
2391+
void createCollectionShouldSetUpTimeSeriesWithExpiration() {
2392+
2393+
template.createCollection(TimeSeriesTypeWithExpire.class);
2394+
2395+
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
2396+
verify(db).createCollection(any(), options.capture());
2397+
2398+
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
2399+
.isEqualTo(60);
2400+
}
2401+
23892402
@Test // GH-3522
23902403
void usedCountDocumentsForEmptyQueryByDefault() {
23912404

@@ -2705,6 +2718,14 @@ static class TimeSeriesType {
27052718
Object meta;
27062719
}
27072720

2721+
@TimeSeries(timeField = "timestamp", expireAfterSeconds = 60)
2722+
static class TimeSeriesTypeWithExpire {
2723+
2724+
String id;
2725+
Instant timestamp;
2726+
}
2727+
2728+
27082729
static class TypeImplementingIterator implements Iterator {
27092730

27102731
@Override

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
* @author Roman Puchkovskiy
133133
* @author Mathieu Ouellet
134134
* @author Yadhukrishna S Pai
135+
* @author Ben Foster
135136
*/
136137
@ExtendWith(MockitoExtension.class)
137138
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -1739,6 +1740,18 @@ public WriteConcern resolve(MongoAction action) {
17391740
verify(collection).withWriteConcern(eq(WriteConcern.UNACKNOWLEDGED));
17401741
}
17411742

1743+
@Test // GH-4099
1744+
void createCollectionShouldSetUpTimeSeriesWithExpiration() {
1745+
1746+
template.createCollection(TimeSeriesTypeWithExpire.class).subscribe();
1747+
1748+
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
1749+
verify(db).createCollection(any(), options.capture());
1750+
1751+
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
1752+
.isEqualTo(60);
1753+
}
1754+
17421755
private void stubFindSubscribe(Document document) {
17431756
stubFindSubscribe(document, new AtomicLong());
17441757
}
@@ -1887,6 +1900,13 @@ static class TimeSeriesType {
18871900
Object meta;
18881901
}
18891902

1903+
@TimeSeries(timeField = "timestamp", expireAfterSeconds = 60)
1904+
static class TimeSeriesTypeWithExpire {
1905+
1906+
String id;
1907+
Instant timestamp;
1908+
}
1909+
18901910
static class ValueCapturingEntityCallback<T> {
18911911

18921912
private final List<T> values = new ArrayList<>(1);

0 commit comments

Comments
 (0)