Browse Source

DATAMONGO-2113 - Fix resumeTimestamp conversion for change streams.

We now use the first 32 bits of the timestamp to create the instant and ignore the ordinal value.

Original pull request: #615.
pull/616/head
Christoph Strobl 7 years ago committed by Mark Paluch
parent
commit
4673525462
  1. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java
  2. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  3. 21
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java
  4. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java
  5. 5
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java
  6. 17
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoConvertersUnitTests.java
  7. 5
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTests.java

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

@ -83,7 +83,9 @@ public class ChangeStreamEvent<T> {
*/ */
@Nullable @Nullable
public Instant getTimestamp() { public Instant getTimestamp() {
return raw != null && raw.getClusterTime() != null ? Instant.ofEpochMilli(raw.getClusterTime().getValue()) : null;
return raw != null && raw.getClusterTime() != null
? converter.getConversionService().convert(raw.getClusterTime(), Instant.class) : null;
} }
/** /**

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

@ -1915,7 +1915,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
publisher = options.getResumeToken().map(BsonValue::asDocument).map(publisher::resumeAfter).orElse(publisher); publisher = options.getResumeToken().map(BsonValue::asDocument).map(publisher::resumeAfter).orElse(publisher);
publisher = options.getCollation().map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher); publisher = options.getCollation().map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher);
publisher = options.getResumeTimestamp().map(it -> new BsonTimestamp(it.toEpochMilli())) publisher = options.getResumeTimestamp().map(it -> new BsonTimestamp((int) it.getEpochSecond(), 0))
.map(publisher::startAtOperationTime).orElse(publisher); .map(publisher::startAtOperationTime).orElse(publisher);
publisher = publisher.fullDocument(options.getFullDocumentLookup().orElse(fullDocument)); publisher = publisher.fullDocument(options.getFullDocumentLookup().orElse(fullDocument));

21
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java

@ -19,6 +19,7 @@ import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Currency; import java.util.Currency;
@ -26,6 +27,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.bson.BsonTimestamp;
import org.bson.Document; import org.bson.Document;
import org.bson.types.Binary; import org.bson.types.Binary;
import org.bson.types.Code; import org.bson.types.Code;
@ -86,6 +88,7 @@ abstract class MongoConverters {
converters.add(LongToAtomicLongConverter.INSTANCE); converters.add(LongToAtomicLongConverter.INSTANCE);
converters.add(IntegerToAtomicIntegerConverter.INSTANCE); converters.add(IntegerToAtomicIntegerConverter.INSTANCE);
converters.add(BinaryToByteArrayConverter.INSTANCE); converters.add(BinaryToByteArrayConverter.INSTANCE);
converters.add(BsonTimestampToInstantConverter.INSTANCE);
return converters; return converters;
} }
@ -465,4 +468,22 @@ abstract class MongoConverters {
return source.getData(); return source.getData();
} }
} }
/**
* {@link Converter} implementation converting {@link BsonTimestamp} into {@link Instant}.
*
* @author Christoph Strobl
* @since 2.1.2
*/
@ReadingConverter
enum BsonTimestampToInstantConverter implements Converter<BsonTimestamp, Instant> {
INSTANCE;
@Nullable
@Override
public Instant convert(BsonTimestamp source) {
return Instant.ofEpochSecond(source.getTime(), 0);
}
}
} }

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java

@ -115,7 +115,7 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
.orElseGet(() -> ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT .orElseGet(() -> ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT
: FullDocument.UPDATE_LOOKUP); : FullDocument.UPDATE_LOOKUP);
startAt = changeStreamOptions.getResumeTimestamp().map(Instant::toEpochMilli).map(BsonTimestamp::new) startAt = changeStreamOptions.getResumeTimestamp().map(it -> new BsonTimestamp((int) it.getEpochSecond(), 0))
.orElse(null); .orElse(null);
} }

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

@ -29,6 +29,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -1355,7 +1356,7 @@ public class ReactiveMongoTemplateTests {
} }
} }
@Test // DATAMONGO-2012 @Test // DATAMONGO-2012, DATAMONGO-2113
public void resumesAtTimestampCorrectly() throws InterruptedException { public void resumesAtTimestampCorrectly() throws InterruptedException {
Assumptions.assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue(); Assumptions.assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue();
@ -1372,7 +1373,7 @@ public class ReactiveMongoTemplateTests {
Person person2 = new Person("Data", 37); Person person2 = new Person("Data", 37);
Person person3 = new Person("MongoDB", 39); Person person3 = new Person("MongoDB", 39);
StepVerifier.create(template.save(person1)).expectNextCount(1).verifyComplete(); StepVerifier.create(template.save(person1).delayElement(Duration.ofSeconds(1))).expectNextCount(1).verifyComplete();
StepVerifier.create(template.save(person2)).expectNextCount(1).verifyComplete(); StepVerifier.create(template.save(person2)).expectNextCount(1).verifyComplete();
Thread.sleep(500); // just give it some time to link receive all events Thread.sleep(500); // just give it some time to link receive all events

17
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoConvertersUnitTests.java

@ -19,10 +19,15 @@ import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Currency; import java.util.Currency;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.assertj.core.data.TemporalUnitLessThanOffset;
import org.bson.BsonTimestamp;
import org.bson.Document;
import org.junit.Test; import org.junit.Test;
import org.springframework.data.geo.Box; import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle; import org.springframework.data.geo.Circle;
@ -32,14 +37,14 @@ import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.core.convert.MongoConverters.AtomicIntegerToIntegerConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.AtomicIntegerToIntegerConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.AtomicLongToLongConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.AtomicLongToLongConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.BsonTimestampToInstantConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.CurrencyToStringConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.CurrencyToStringConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.IntegerToAtomicIntegerConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.IntegerToAtomicIntegerConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.LongToAtomicLongConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.LongToAtomicLongConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter;
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToCurrencyConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.StringToCurrencyConverter;
import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.test.util.Assertions;
import org.bson.Document;
/** /**
* Unit tests for {@link MongoConverters}. * Unit tests for {@link MongoConverters}.
@ -145,4 +150,12 @@ public class MongoConvertersUnitTests {
public void convertsIntegerToAtomicIntegerCorrectly() { public void convertsIntegerToAtomicIntegerCorrectly() {
assertThat(IntegerToAtomicIntegerConverter.INSTANCE.convert(100), is(instanceOf(AtomicInteger.class))); assertThat(IntegerToAtomicIntegerConverter.INSTANCE.convert(100), is(instanceOf(AtomicInteger.class)));
} }
@Test // DATAMONGO-2113
public void convertsBsonTimestampToInstantCorrectly() {
Assertions.assertThat(BsonTimestampToInstantConverter.INSTANCE.convert(new BsonTimestamp(6615900307735969796L)))
.isCloseTo(Instant.ofEpochSecond(1540384327), new TemporalUnitLessThanOffset(100, ChronoUnit.MILLIS));
}
} }

5
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTests.java

@ -405,7 +405,7 @@ public class ChangeStreamTests {
.append("user_name", "jellyBelly").append("age", 8).append("_class", User.class.getName())); .append("user_name", "jellyBelly").append("age", 8).append("_class", User.class.getName()));
} }
@Test // DATAMONGO-2012 @Test // DATAMONGO-2012, DATAMONGO-2113
public void resumeAtTimestampCorrectly() throws InterruptedException { public void resumeAtTimestampCorrectly() throws InterruptedException {
CollectingMessageListener<ChangeStreamDocument<Document>, User> messageListener1 = new CollectingMessageListener<>(); CollectingMessageListener<ChangeStreamDocument<Document>, User> messageListener1 = new CollectingMessageListener<>();
@ -415,6 +415,9 @@ public class ChangeStreamTests {
awaitSubscription(subscription1); awaitSubscription(subscription1);
template.save(jellyBelly); template.save(jellyBelly);
Thread.sleep(1000); // cluster timestamp is in seconds, so we need to wait at least one.
template.save(sugarSplashy); template.save(sugarSplashy);
awaitMessages(messageListener1, 12); awaitMessages(messageListener1, 12);

Loading…
Cancel
Save