Skip to content

Commit ce6c119

Browse files
authored
Improve Time-Series Bucketing Scalability (#1137)
Introduce customizable bucketMaxSpan and bucketRounding options for Time-Series collections, providing users with more control over bucketing behaviours. JAVA-4888
1 parent 2468e98 commit ce6c119

File tree

8 files changed

+268
-4
lines changed

8 files changed

+268
-4
lines changed

driver-core/src/main/com/mongodb/client/model/TimeSeriesGranularity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public enum TimeSeriesGranularity {
2929
/**
3030
* Seconds-level granularity.
3131
* <p>
32-
* If granularity of a time-series collection is unspecified, this is the default value.
32+
* This is the default value.
3333
* </p>
3434
*/
3535
SECONDS,

driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818

1919
import com.mongodb.lang.Nullable;
2020

21+
import java.util.concurrent.TimeUnit;
22+
23+
import static com.mongodb.assertions.Assertions.isTrue;
24+
import static com.mongodb.assertions.Assertions.isTrueArgument;
2125
import static com.mongodb.assertions.Assertions.notNull;
2226

2327
/**
@@ -31,6 +35,8 @@ public final class TimeSeriesOptions {
3135
private final String timeField;
3236
private String metaField;
3337
private TimeSeriesGranularity granularity;
38+
private Long bucketMaxSpanSeconds;
39+
private Long bucketRoundingSeconds;
3440

3541
/**
3642
* Construct a new instance.
@@ -92,24 +98,121 @@ public TimeSeriesGranularity getGranularity() {
9298
/**
9399
* Sets the granularity of the time-series data.
94100
* <p>
95-
* The default value is {@link TimeSeriesGranularity#SECONDS}.
101+
* The default value is {@link TimeSeriesGranularity#SECONDS} if neither {@link #bucketMaxSpan(Long, TimeUnit)} nor
102+
* {@link #bucketRounding(Long, TimeUnit)} is set. If any of these bucketing options are set, the granularity parameter cannot be set.
96103
* </p>
97104
*
98105
* @param granularity the time-series granularity
99106
* @return this
100107
* @see #getGranularity()
101108
*/
102109
public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granularity) {
110+
isTrue("granularity is not allowed when bucketMaxSpan is set", bucketMaxSpanSeconds == null);
111+
isTrue("granularity is not allowed when bucketRounding is set", bucketRoundingSeconds == null);
103112
this.granularity = granularity;
104113
return this;
105114
}
106115

116+
/**
117+
* Returns the maximum time span between measurements in a bucket.
118+
*
119+
* @param timeUnit the time unit.
120+
* @return time span between measurements, or {@code null} if not set.
121+
* @since 4.10
122+
* @mongodb.server.release 6.3
123+
* @see #bucketMaxSpan(Long, TimeUnit)
124+
*/
125+
@Nullable
126+
public Long getBucketMaxSpan(final TimeUnit timeUnit) {
127+
notNull("timeUnit", timeUnit);
128+
if (bucketMaxSpanSeconds == null) {
129+
return null;
130+
}
131+
return timeUnit.convert(bucketMaxSpanSeconds, TimeUnit.SECONDS);
132+
}
133+
134+
/**
135+
* Sets the maximum time span between measurements in a bucket.
136+
* <p>
137+
* The value of {@code bucketMaxSpan} must be the same as {@link #bucketRounding(Long, TimeUnit)}, which also means that the options
138+
* must either be both set or both unset. If you set the {@code bucketMaxSpan} parameter, you can't set the granularity parameter.
139+
* </p>
140+
*
141+
* @param bucketMaxSpan time span between measurements. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)},
142+
* the value must be &gt;= 1. {@code null} can be provided to unset any previously set value.
143+
* @param timeUnit the time unit.
144+
* @return this
145+
* @since 4.10
146+
* @mongodb.server.release 6.3
147+
* @see #getBucketMaxSpan(TimeUnit)
148+
*/
149+
public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final TimeUnit timeUnit) {
150+
notNull("timeUnit", timeUnit);
151+
if (bucketMaxSpan == null) {
152+
this.bucketMaxSpanSeconds = null;
153+
} else {
154+
isTrue("bucketMaxSpan is not allowed when granularity is set", granularity == null);
155+
long seconds = TimeUnit.SECONDS.convert(bucketMaxSpan, timeUnit);
156+
isTrueArgument("bucketMaxSpan, after conversion to seconds, must be >= 1", seconds > 0);
157+
this.bucketMaxSpanSeconds = seconds;
158+
}
159+
return this;
160+
}
161+
162+
/**
163+
* Returns the time interval that determines the starting timestamp for a new bucket.
164+
*
165+
* @param timeUnit the time unit.
166+
* @return the time interval, or {@code null} if not set.
167+
* @since 4.10
168+
* @mongodb.server.release 6.3
169+
* @see #bucketRounding(Long, TimeUnit)
170+
*/
171+
@Nullable
172+
public Long getBucketRounding(final TimeUnit timeUnit) {
173+
notNull("timeUnit", timeUnit);
174+
if (bucketRoundingSeconds == null) {
175+
return null;
176+
}
177+
return timeUnit.convert(bucketRoundingSeconds, TimeUnit.SECONDS);
178+
}
179+
180+
/**
181+
* Specifies the time interval that determines the starting timestamp for a new bucket.
182+
* <p>
183+
* The value of {@code bucketRounding} must be the same as {@link #bucketMaxSpan(Long, TimeUnit)}, which also means that the options
184+
* must either be both set or both unset. If you set the {@code bucketRounding} parameter, you can't set the granularity parameter.
185+
* </p>
186+
*
187+
* @param bucketRounding time interval. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)},
188+
* the value must be &gt;= 1. {@code null} can be provided to unset any previously set value.
189+
* @param timeUnit the time unit.
190+
* @return this
191+
* @since 4.10
192+
* @mongodb.server.release 6.3
193+
* @see #getBucketRounding(TimeUnit)
194+
*/
195+
public TimeSeriesOptions bucketRounding(@Nullable final Long bucketRounding, final TimeUnit timeUnit) {
196+
notNull("timeUnit", timeUnit);
197+
if (bucketRounding == null) {
198+
this.bucketRoundingSeconds = null;
199+
} else {
200+
isTrue("bucketRounding is not allowed when granularity is set", granularity == null);
201+
long seconds = TimeUnit.SECONDS.convert(bucketRounding, timeUnit);
202+
isTrueArgument("bucketRounding, after conversion to seconds, must be >= 1", seconds > 0);
203+
this.bucketRoundingSeconds = seconds;
204+
}
205+
return this;
206+
}
207+
107208
@Override
108209
public String toString() {
109210
return "TimeSeriesOptions{"
110211
+ "timeField='" + timeField + '\''
111212
+ ", metaField='" + metaField + '\''
112213
+ ", granularity=" + granularity
214+
+ ", bucketMaxSpanSeconds=" + bucketMaxSpanSeconds
215+
+ ", bucketRoundingSeconds=" + bucketRoundingSeconds
113216
+ '}';
114217
}
115218
}

driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
import org.bson.BsonArray;
3535
import org.bson.BsonBoolean;
3636
import org.bson.BsonDocument;
37+
import org.bson.BsonInt64;
3738
import org.bson.BsonString;
3839

3940
import java.util.ArrayDeque;
4041
import java.util.Deque;
4142
import java.util.List;
43+
import java.util.concurrent.TimeUnit;
4244
import java.util.function.Supplier;
4345

4446
import static com.mongodb.assertions.Assertions.notNull;
@@ -347,6 +349,14 @@ private BsonDocument getCreateCollectionCommand() {
347349
if (granularity != null) {
348350
timeSeriesDocument.put("granularity", new BsonString(getGranularityAsString(granularity)));
349351
}
352+
Long bucketMaxSpan = timeSeriesOptions.getBucketMaxSpan(TimeUnit.SECONDS);
353+
if (bucketMaxSpan != null){
354+
timeSeriesDocument.put("bucketMaxSpanSeconds", new BsonInt64(bucketMaxSpan));
355+
}
356+
Long bucketRounding = timeSeriesOptions.getBucketRounding(TimeUnit.SECONDS);
357+
if (bucketRounding != null){
358+
timeSeriesDocument.put("bucketRoundingSeconds", new BsonInt64(bucketRounding));
359+
}
350360
document.put("timeseries", timeSeriesDocument);
351361
}
352362
if (changeStreamPreAndPostImagesOptions != null) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.client.model;
18+
19+
import com.mongodb.lang.Nullable;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
25+
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.stream.Stream;
28+
29+
import static org.junit.jupiter.api.Assertions.assertAll;
30+
import static org.junit.jupiter.api.Assertions.assertThrows;
31+
import static org.junit.jupiter.params.provider.Arguments.arguments;
32+
33+
class TimeSeriesOptionsTest {
34+
35+
private TimeSeriesOptions timeSeriesOptions;
36+
37+
@BeforeEach
38+
void setUp() {
39+
timeSeriesOptions = new TimeSeriesOptions("test");
40+
}
41+
42+
@Test
43+
void shouldThrowErrorWhenGranularityIsAlreadySet() {
44+
//given
45+
timeSeriesOptions.granularity(TimeSeriesGranularity.SECONDS);
46+
47+
//when & then
48+
assertAll(
49+
() -> assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketRounding(1L, TimeUnit.SECONDS)),
50+
() -> assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketMaxSpan(1L, TimeUnit.SECONDS))
51+
);
52+
}
53+
54+
@Test
55+
void shouldThrowErrorWhenGetWithNullParameter() {
56+
assertAll(
57+
() -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketMaxSpan(null)),
58+
() -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketRounding(null))
59+
);
60+
}
61+
62+
@ParameterizedTest
63+
@MethodSource("args")
64+
void shouldThrowErrorWhenInvalidArgumentProvided(@Nullable final Long valueToSet, @Nullable final TimeUnit timeUnit) {
65+
assertAll(
66+
() -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketRounding(valueToSet, timeUnit)),
67+
() -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketMaxSpan(valueToSet, timeUnit))
68+
);
69+
}
70+
71+
private static Stream<Arguments> args() {
72+
return Stream.of(
73+
arguments(1L, null),
74+
arguments(null, null),
75+
arguments(1L, TimeUnit.MILLISECONDS)
76+
);
77+
}
78+
}

driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,71 @@
250250
]
251251
}
252252
]
253+
},
254+
{
255+
"description": "createCollection with bucketing options",
256+
"runOnRequirements": [
257+
{
258+
"minServerVersion": "6.3"
259+
}
260+
],
261+
"operations": [
262+
{
263+
"name": "dropCollection",
264+
"object": "database0",
265+
"arguments": {
266+
"collection": "test"
267+
}
268+
},
269+
{
270+
"name": "createCollection",
271+
"object": "database0",
272+
"arguments": {
273+
"collection": "test",
274+
"timeseries": {
275+
"timeField": "time",
276+
"bucketMaxSpanSeconds": 3600,
277+
"bucketRoundingSeconds": 3600
278+
}
279+
}
280+
},
281+
{
282+
"name": "assertCollectionExists",
283+
"object": "testRunner",
284+
"arguments": {
285+
"databaseName": "ts-tests",
286+
"collectionName": "test"
287+
}
288+
}
289+
],
290+
"expectEvents": [
291+
{
292+
"client": "client0",
293+
"events": [
294+
{
295+
"commandStartedEvent": {
296+
"command": {
297+
"drop": "test"
298+
},
299+
"databaseName": "ts-tests"
300+
}
301+
},
302+
{
303+
"commandStartedEvent": {
304+
"command": {
305+
"create": "test",
306+
"timeseries": {
307+
"timeField": "time",
308+
"bucketMaxSpanSeconds": 3600,
309+
"bucketRoundingSeconds": 3600
310+
}
311+
},
312+
"databaseName": "ts-tests"
313+
}
314+
}
315+
]
316+
}
317+
]
253318
}
254319
]
255320
}

driver-kotlin-coroutine/src/test/kotlin/com/mongodb/kotlin/client/coroutine/ExtensionMethodsTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class ExtensionMethodsTest {
3535
"FindOneAndReplaceOptions",
3636
"FindOneAndUpdateOptions",
3737
"IndexOptions",
38-
"TransactionOptions")
38+
"TransactionOptions",
39+
"TimeSeriesOptions")
3940

4041
ClassGraph().enableClassInfo().enableMethodInfo().acceptPackages("com.mongodb").scan().use { scanResult ->
4142
val optionsClassesWithTimeUnit =

driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/ExtensionMethodsTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class ExtensionMethodsTest {
3535
"FindOneAndReplaceOptions",
3636
"FindOneAndUpdateOptions",
3737
"IndexOptions",
38-
"TransactionOptions")
38+
"TransactionOptions",
39+
"TimeSeriesOptions")
3940

4041
ClassGraph().enableClassInfo().enableMethodInfo().acceptPackages("com.mongodb").scan().use { scanResult ->
4142
val optionsClassesWithTimeUnit =

driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,12 @@ private TimeSeriesOptions createTimeSeriesOptions(final BsonDocument timeSeriesD
12021202
case "metaField":
12031203
options.metaField(cur.getValue().asString().getValue());
12041204
break;
1205+
case "bucketMaxSpanSeconds":
1206+
options.bucketMaxSpan(cur.getValue().asInt32().longValue(), TimeUnit.SECONDS);
1207+
break;
1208+
case "bucketRoundingSeconds":
1209+
options.bucketRounding(cur.getValue().asInt32().longValue(), TimeUnit.SECONDS);
1210+
break;
12051211
case "granularity":
12061212
options.granularity(createTimeSeriesGranularity(cur.getValue().asString().getValue()));
12071213
break;

0 commit comments

Comments
 (0)