
com.mongodb.internal.operation.ChangeStreamOperation Maven / Gradle / Ivy
Show all versions of mongodb-driver-core Show documentation
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb.internal.operation;
import com.mongodb.CursorType;
import com.mongodb.MongoNamespace;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.changestream.FullDocument;
import com.mongodb.client.model.changestream.FullDocumentBeforeChange;
import com.mongodb.internal.TimeoutContext;
import com.mongodb.internal.async.AsyncBatchCursor;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.binding.AsyncReadBinding;
import com.mongodb.internal.binding.ReadBinding;
import com.mongodb.internal.client.model.changestream.ChangeStreamLevel;
import com.mongodb.lang.Nullable;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.BsonTimestamp;
import org.bson.BsonValue;
import org.bson.RawBsonDocument;
import org.bson.codecs.Decoder;
import org.bson.codecs.RawBsonDocumentCodec;
import java.util.ArrayList;
import java.util.List;
import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.client.cursor.TimeoutMode.CURSOR_LIFETIME;
/**
* An operation that executes an {@code $changeStream} aggregation.
*
* This class is not part of the public API and may be removed or changed at any time
*/
public class ChangeStreamOperation implements AsyncReadOperation>, ReadOperation> {
private static final RawBsonDocumentCodec RAW_BSON_DOCUMENT_CODEC = new RawBsonDocumentCodec();
private final AggregateOperationImpl wrapped;
private final FullDocument fullDocument;
private final FullDocumentBeforeChange fullDocumentBeforeChange;
private final Decoder decoder;
private final ChangeStreamLevel changeStreamLevel;
private BsonDocument resumeAfter;
private BsonDocument startAfter;
private BsonTimestamp startAtOperationTime;
private boolean showExpandedEvents;
public ChangeStreamOperation(final MongoNamespace namespace, final FullDocument fullDocument,
final FullDocumentBeforeChange fullDocumentBeforeChange, final List pipeline, final Decoder decoder) {
this(namespace, fullDocument, fullDocumentBeforeChange, pipeline, decoder, ChangeStreamLevel.COLLECTION);
}
public ChangeStreamOperation(final MongoNamespace namespace, final FullDocument fullDocument,
final FullDocumentBeforeChange fullDocumentBeforeChange, final List pipeline, final Decoder decoder,
final ChangeStreamLevel changeStreamLevel) {
this.wrapped = new AggregateOperationImpl<>(namespace, pipeline, RAW_BSON_DOCUMENT_CODEC, getAggregateTarget(),
getPipelineCreator()).cursorType(CursorType.TailableAwait);
this.fullDocument = notNull("fullDocument", fullDocument);
this.fullDocumentBeforeChange = notNull("fullDocumentBeforeChange", fullDocumentBeforeChange);
this.decoder = notNull("decoder", decoder);
this.changeStreamLevel = notNull("changeStreamLevel", changeStreamLevel);
}
public MongoNamespace getNamespace() {
return wrapped.getNamespace();
}
public Decoder getDecoder() {
return decoder;
}
public FullDocument getFullDocument() {
return fullDocument;
}
public BsonDocument getResumeAfter() {
return resumeAfter;
}
public ChangeStreamOperation resumeAfter(final BsonDocument resumeAfter) {
this.resumeAfter = resumeAfter;
return this;
}
public BsonDocument getStartAfter() {
return startAfter;
}
public ChangeStreamOperation startAfter(final BsonDocument startAfter) {
this.startAfter = startAfter;
return this;
}
public List getPipeline() {
return wrapped.getPipeline();
}
public Integer getBatchSize() {
return wrapped.getBatchSize();
}
public ChangeStreamOperation batchSize(@Nullable final Integer batchSize) {
wrapped.batchSize(batchSize);
return this;
}
public Collation getCollation() {
return wrapped.getCollation();
}
public ChangeStreamOperation collation(final Collation collation) {
wrapped.collation(collation);
return this;
}
public ChangeStreamOperation startAtOperationTime(final BsonTimestamp startAtOperationTime) {
this.startAtOperationTime = startAtOperationTime;
return this;
}
public BsonTimestamp getStartAtOperationTime() {
return startAtOperationTime;
}
public ChangeStreamOperation retryReads(final boolean retryReads) {
wrapped.retryReads(retryReads);
return this;
}
public boolean getRetryReads() {
return wrapped.getRetryReads();
}
@Nullable
public BsonValue getComment() {
return wrapped.getComment();
}
public ChangeStreamOperation comment(final BsonValue comment) {
wrapped.comment(comment);
return this;
}
public boolean getShowExpandedEvents() {
return this.showExpandedEvents;
}
public ChangeStreamOperation showExpandedEvents(final boolean showExpandedEvents) {
this.showExpandedEvents = showExpandedEvents;
return this;
}
/**
* Gets an aggregate operation with consideration for timeout settings.
*
* Change streams act similarly to tailable awaitData cursors, with identical timeoutMS option behavior.
* Key distinctions include:
* - The timeoutMS option must be applied at the start of the aggregate operation for change streams.
* - Change streams support resumption on next() calls. The driver handles automatic resumption for transient errors.
*
*
* As a result, when {@code timeoutContext.hasTimeoutMS()} the CURSOR_LIFETIME setting is utilized to manage the underlying cursor's
* lifespan in change streams.
*
* @param timeoutContext
* @return An AggregateOperationImpl
*/
private AggregateOperationImpl getAggregateOperation(final TimeoutContext timeoutContext) {
if (timeoutContext.hasTimeoutMS()) {
return wrapped.timeoutMode(CURSOR_LIFETIME);
}
return wrapped;
}
@Override
public BatchCursor execute(final ReadBinding binding) {
TimeoutContext timeoutContext = binding.getOperationContext().getTimeoutContext();
CommandBatchCursor cursor = ((CommandBatchCursor) getAggregateOperation(timeoutContext).execute(binding))
.disableTimeoutResetWhenClosing();
return new ChangeStreamBatchCursor<>(ChangeStreamOperation.this, cursor, binding,
setChangeStreamOptions(cursor.getPostBatchResumeToken(), cursor.getOperationTime(),
cursor.getMaxWireVersion(), cursor.isFirstBatchEmpty()), cursor.getMaxWireVersion());
}
@Override
public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback> callback) {
TimeoutContext timeoutContext = binding.getOperationContext().getTimeoutContext();
getAggregateOperation(timeoutContext).executeAsync(binding, (result, t) -> {
if (t != null) {
callback.onResult(null, t);
} else {
AsyncCommandBatchCursor cursor = ((AsyncCommandBatchCursor) assertNotNull(result))
.disableTimeoutResetWhenClosing();
callback.onResult(new AsyncChangeStreamBatchCursor<>(ChangeStreamOperation.this, cursor, binding,
setChangeStreamOptions(cursor.getPostBatchResumeToken(), cursor.getOperationTime(),
cursor.getMaxWireVersion(), cursor.isFirstBatchEmpty()), cursor.getMaxWireVersion()), null);
}
});
}
@Nullable
private BsonDocument setChangeStreamOptions(@Nullable final BsonDocument postBatchResumeToken,
@Nullable final BsonTimestamp operationTime, final int maxWireVersion, final boolean firstBatchEmpty) {
BsonDocument resumeToken = null;
if (startAfter != null) {
resumeToken = startAfter;
} else if (resumeAfter != null) {
resumeToken = resumeAfter;
} else if (startAtOperationTime == null && postBatchResumeToken == null && firstBatchEmpty && maxWireVersion >= 7) {
startAtOperationTime = operationTime;
}
return resumeToken;
}
public void setChangeStreamOptionsForResume(@Nullable final BsonDocument resumeToken, final int maxWireVersion) {
startAfter = null;
if (resumeToken != null) {
startAtOperationTime = null;
resumeAfter = resumeToken;
} else if (startAtOperationTime != null && maxWireVersion >= 7) {
resumeAfter = null;
} else {
resumeAfter = null;
startAtOperationTime = null;
}
}
// Leave as anonymous class so as not to confuse CustomMatchers#compare
private AggregateOperationImpl.AggregateTarget getAggregateTarget() {
return () -> changeStreamLevel == ChangeStreamLevel.COLLECTION
? new BsonString(getNamespace().getCollectionName()) : new BsonInt32(1);
}
// Leave as anonymous class so as not to confuse CustomMatchers#compare
private AggregateOperationImpl.PipelineCreator getPipelineCreator() {
return () -> {
List changeStreamPipeline = new ArrayList<>();
BsonDocument changeStream = new BsonDocument();
if (fullDocument != FullDocument.DEFAULT) {
changeStream.append("fullDocument", new BsonString(fullDocument.getValue()));
}
if (fullDocumentBeforeChange != FullDocumentBeforeChange.DEFAULT) {
changeStream.append("fullDocumentBeforeChange", new BsonString(fullDocumentBeforeChange.getValue()));
}
if (changeStreamLevel == ChangeStreamLevel.CLIENT) {
changeStream.append("allChangesForCluster", BsonBoolean.TRUE);
}
if (showExpandedEvents) {
changeStream.append("showExpandedEvents", BsonBoolean.TRUE);
}
if (resumeAfter != null) {
changeStream.append("resumeAfter", resumeAfter);
}
if (startAfter != null) {
changeStream.append("startAfter", startAfter);
}
if (startAtOperationTime != null) {
changeStream.append("startAtOperationTime", startAtOperationTime);
}
changeStreamPipeline.add(new BsonDocument("$changeStream", changeStream));
changeStreamPipeline.addAll(getPipeline());
return new BsonArray(changeStreamPipeline);
};
}
}