github.com/mdaxf/iac@v0.0.0-20240519030858-58a061660378/vendor_skip/go.mongodb.org/mongo-driver/mongo/change_stream.go (about) 1 // Copyright (C) MongoDB, Inc. 2017-present. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); you may 4 // not use this file except in compliance with the License. You may obtain 5 // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 6 7 package mongo 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "reflect" 14 "strconv" 15 "time" 16 17 "go.mongodb.org/mongo-driver/bson" 18 "go.mongodb.org/mongo-driver/bson/bsoncodec" 19 "go.mongodb.org/mongo-driver/bson/primitive" 20 "go.mongodb.org/mongo-driver/internal" 21 "go.mongodb.org/mongo-driver/mongo/description" 22 "go.mongodb.org/mongo-driver/mongo/options" 23 "go.mongodb.org/mongo-driver/mongo/readconcern" 24 "go.mongodb.org/mongo-driver/mongo/readpref" 25 "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" 26 "go.mongodb.org/mongo-driver/x/mongo/driver" 27 "go.mongodb.org/mongo-driver/x/mongo/driver/operation" 28 "go.mongodb.org/mongo-driver/x/mongo/driver/session" 29 ) 30 31 var ( 32 // ErrMissingResumeToken indicates that a change stream notification from the server did not contain a resume token. 33 ErrMissingResumeToken = errors.New("cannot provide resume functionality when the resume token is missing") 34 // ErrNilCursor indicates that the underlying cursor for the change stream is nil. 35 ErrNilCursor = errors.New("cursor is nil") 36 37 minResumableLabelWireVersion int32 = 9 // Wire version at which the server includes the resumable error label 38 networkErrorLabel = "NetworkError" 39 resumableErrorLabel = "ResumableChangeStreamError" 40 errorCursorNotFound int32 = 43 // CursorNotFound error code 41 42 // Allowlist of error codes that are considered resumable. 43 resumableChangeStreamErrors = map[int32]struct{}{ 44 6: {}, // HostUnreachable 45 7: {}, // HostNotFound 46 89: {}, // NetworkTimeout 47 91: {}, // ShutdownInProgress 48 189: {}, // PrimarySteppedDown 49 262: {}, // ExceededTimeLimit 50 9001: {}, // SocketException 51 10107: {}, // NotPrimary 52 11600: {}, // InterruptedAtShutdown 53 11602: {}, // InterruptedDueToReplStateChange 54 13435: {}, // NotPrimaryNoSecondaryOK 55 13436: {}, // NotPrimaryOrSecondary 56 63: {}, // StaleShardVersion 57 150: {}, // StaleEpoch 58 13388: {}, // StaleConfig 59 234: {}, // RetryChangeStream 60 133: {}, // FailedToSatisfyReadPreference 61 } 62 ) 63 64 // ChangeStream is used to iterate over a stream of events. Each event can be decoded into a Go type via the Decode 65 // method or accessed as raw BSON via the Current field. This type is not goroutine safe and must not be used 66 // concurrently by multiple goroutines. For more information about change streams, see 67 // https://www.mongodb.com/docs/manual/changeStreams/. 68 type ChangeStream struct { 69 // Current is the BSON bytes of the current event. This property is only valid until the next call to Next or 70 // TryNext. If continued access is required, a copy must be made. 71 Current bson.Raw 72 73 aggregate *operation.Aggregate 74 pipelineSlice []bsoncore.Document 75 pipelineOptions map[string]bsoncore.Value 76 cursor changeStreamCursor 77 cursorOptions driver.CursorOptions 78 batch []bsoncore.Document 79 resumeToken bson.Raw 80 err error 81 sess *session.Client 82 client *Client 83 bsonOpts *options.BSONOptions 84 registry *bsoncodec.Registry 85 streamType StreamType 86 options *options.ChangeStreamOptions 87 selector description.ServerSelector 88 operationTime *primitive.Timestamp 89 wireVersion *description.VersionRange 90 } 91 92 type changeStreamConfig struct { 93 readConcern *readconcern.ReadConcern 94 readPreference *readpref.ReadPref 95 client *Client 96 bsonOpts *options.BSONOptions 97 registry *bsoncodec.Registry 98 streamType StreamType 99 collectionName string 100 databaseName string 101 crypt driver.Crypt 102 } 103 104 func newChangeStream(ctx context.Context, config changeStreamConfig, pipeline interface{}, 105 opts ...*options.ChangeStreamOptions) (*ChangeStream, error) { 106 if ctx == nil { 107 ctx = context.Background() 108 } 109 110 cs := &ChangeStream{ 111 client: config.client, 112 bsonOpts: config.bsonOpts, 113 registry: config.registry, 114 streamType: config.streamType, 115 options: options.MergeChangeStreamOptions(opts...), 116 selector: description.CompositeSelector([]description.ServerSelector{ 117 description.ReadPrefSelector(config.readPreference), 118 description.LatencySelector(config.client.localThreshold), 119 }), 120 cursorOptions: config.client.createBaseCursorOptions(), 121 } 122 123 cs.sess = sessionFromContext(ctx) 124 if cs.sess == nil && cs.client.sessionPool != nil { 125 cs.sess = session.NewImplicitClientSession(cs.client.sessionPool, cs.client.id) 126 } 127 if cs.err = cs.client.validSession(cs.sess); cs.err != nil { 128 closeImplicitSession(cs.sess) 129 return nil, cs.Err() 130 } 131 132 cs.aggregate = operation.NewAggregate(nil). 133 ReadPreference(config.readPreference).ReadConcern(config.readConcern). 134 Deployment(cs.client.deployment).ClusterClock(cs.client.clock). 135 CommandMonitor(cs.client.monitor).Session(cs.sess).ServerSelector(cs.selector).Retry(driver.RetryNone). 136 ServerAPI(cs.client.serverAPI).Crypt(config.crypt).Timeout(cs.client.timeout) 137 138 if cs.options.Collation != nil { 139 cs.aggregate.Collation(bsoncore.Document(cs.options.Collation.ToDocument())) 140 } 141 if comment := cs.options.Comment; comment != nil { 142 cs.aggregate.Comment(*comment) 143 144 commentVal, err := marshalValue(comment, cs.bsonOpts, cs.registry) 145 if err != nil { 146 return nil, err 147 } 148 cs.cursorOptions.Comment = commentVal 149 } 150 if cs.options.BatchSize != nil { 151 cs.aggregate.BatchSize(*cs.options.BatchSize) 152 cs.cursorOptions.BatchSize = *cs.options.BatchSize 153 } 154 if cs.options.MaxAwaitTime != nil { 155 cs.cursorOptions.MaxTimeMS = int64(*cs.options.MaxAwaitTime / time.Millisecond) 156 } 157 if cs.options.Custom != nil { 158 // Marshal all custom options before passing to the initial aggregate. Return 159 // any errors from Marshaling. 160 customOptions := make(map[string]bsoncore.Value) 161 for optionName, optionValue := range cs.options.Custom { 162 bsonType, bsonData, err := bson.MarshalValueWithRegistry(cs.registry, optionValue) 163 if err != nil { 164 cs.err = err 165 closeImplicitSession(cs.sess) 166 return nil, cs.Err() 167 } 168 optionValueBSON := bsoncore.Value{Type: bsonType, Data: bsonData} 169 customOptions[optionName] = optionValueBSON 170 } 171 cs.aggregate.CustomOptions(customOptions) 172 } 173 if cs.options.CustomPipeline != nil { 174 // Marshal all custom pipeline options before building pipeline slice. Return 175 // any errors from Marshaling. 176 cs.pipelineOptions = make(map[string]bsoncore.Value) 177 for optionName, optionValue := range cs.options.CustomPipeline { 178 bsonType, bsonData, err := bson.MarshalValueWithRegistry(cs.registry, optionValue) 179 if err != nil { 180 cs.err = err 181 closeImplicitSession(cs.sess) 182 return nil, cs.Err() 183 } 184 optionValueBSON := bsoncore.Value{Type: bsonType, Data: bsonData} 185 cs.pipelineOptions[optionName] = optionValueBSON 186 } 187 } 188 189 switch cs.streamType { 190 case ClientStream: 191 cs.aggregate.Database("admin") 192 case DatabaseStream: 193 cs.aggregate.Database(config.databaseName) 194 case CollectionStream: 195 cs.aggregate.Collection(config.collectionName).Database(config.databaseName) 196 default: 197 closeImplicitSession(cs.sess) 198 return nil, fmt.Errorf("must supply a valid StreamType in config, instead of %v", cs.streamType) 199 } 200 201 // When starting a change stream, cache startAfter as the first resume token if it is set. If not, cache 202 // resumeAfter. If neither is set, do not cache a resume token. 203 resumeToken := cs.options.StartAfter 204 if resumeToken == nil { 205 resumeToken = cs.options.ResumeAfter 206 } 207 var marshaledToken bson.Raw 208 if resumeToken != nil { 209 if marshaledToken, cs.err = bson.Marshal(resumeToken); cs.err != nil { 210 closeImplicitSession(cs.sess) 211 return nil, cs.Err() 212 } 213 } 214 cs.resumeToken = marshaledToken 215 216 if cs.err = cs.buildPipelineSlice(pipeline); cs.err != nil { 217 closeImplicitSession(cs.sess) 218 return nil, cs.Err() 219 } 220 var pipelineArr bsoncore.Document 221 pipelineArr, cs.err = cs.pipelineToBSON() 222 cs.aggregate.Pipeline(pipelineArr) 223 224 if cs.err = cs.executeOperation(ctx, false); cs.err != nil { 225 closeImplicitSession(cs.sess) 226 return nil, cs.Err() 227 } 228 229 return cs, cs.Err() 230 } 231 232 func (cs *ChangeStream) createOperationDeployment(server driver.Server, connection driver.Connection) driver.Deployment { 233 return &changeStreamDeployment{ 234 topologyKind: cs.client.deployment.Kind(), 235 server: server, 236 conn: connection, 237 } 238 } 239 240 func (cs *ChangeStream) executeOperation(ctx context.Context, resuming bool) error { 241 var server driver.Server 242 var conn driver.Connection 243 244 if server, cs.err = cs.client.deployment.SelectServer(ctx, cs.selector); cs.err != nil { 245 return cs.Err() 246 } 247 if conn, cs.err = server.Connection(ctx); cs.err != nil { 248 return cs.Err() 249 } 250 defer conn.Close() 251 cs.wireVersion = conn.Description().WireVersion 252 253 cs.aggregate.Deployment(cs.createOperationDeployment(server, conn)) 254 255 if resuming { 256 cs.replaceOptions(cs.wireVersion) 257 258 csOptDoc, err := cs.createPipelineOptionsDoc() 259 if err != nil { 260 return err 261 } 262 pipIdx, pipDoc := bsoncore.AppendDocumentStart(nil) 263 pipDoc = bsoncore.AppendDocumentElement(pipDoc, "$changeStream", csOptDoc) 264 if pipDoc, cs.err = bsoncore.AppendDocumentEnd(pipDoc, pipIdx); cs.err != nil { 265 return cs.Err() 266 } 267 cs.pipelineSlice[0] = pipDoc 268 269 var plArr bsoncore.Document 270 if plArr, cs.err = cs.pipelineToBSON(); cs.err != nil { 271 return cs.Err() 272 } 273 cs.aggregate.Pipeline(plArr) 274 } 275 276 // If no deadline is set on the passed-in context, cs.client.timeout is set, and context is not already 277 // a Timeout context, honor cs.client.timeout in new Timeout context for change stream operation execution 278 // and potential retry. 279 if _, deadlineSet := ctx.Deadline(); !deadlineSet && cs.client.timeout != nil && !internal.IsTimeoutContext(ctx) { 280 newCtx, cancelFunc := internal.MakeTimeoutContext(ctx, *cs.client.timeout) 281 // Redefine ctx to be the new timeout-derived context. 282 ctx = newCtx 283 // Cancel the timeout-derived context at the end of executeOperation to avoid a context leak. 284 defer cancelFunc() 285 } 286 287 // Execute the aggregate, retrying on retryable errors once (1) if retryable reads are enabled and 288 // infinitely (-1) if context is a Timeout context. 289 var retries int 290 if cs.client.retryReads { 291 retries = 1 292 } 293 if internal.IsTimeoutContext(ctx) { 294 retries = -1 295 } 296 297 var err error 298 AggregateExecuteLoop: 299 for { 300 err = cs.aggregate.Execute(ctx) 301 // If no error or no retries remain, do not retry. 302 if err == nil || retries == 0 { 303 break AggregateExecuteLoop 304 } 305 306 switch tt := err.(type) { 307 case driver.Error: 308 // If error is not retryable, do not retry. 309 if !tt.RetryableRead() { 310 break AggregateExecuteLoop 311 } 312 313 // If error is retryable: subtract 1 from retries, redo server selection, checkout 314 // a connection, and restart loop. 315 retries-- 316 server, err = cs.client.deployment.SelectServer(ctx, cs.selector) 317 if err != nil { 318 break AggregateExecuteLoop 319 } 320 321 conn.Close() 322 conn, err = server.Connection(ctx) 323 if err != nil { 324 break AggregateExecuteLoop 325 } 326 defer conn.Close() 327 328 // Update the wire version with data from the new connection. 329 cs.wireVersion = conn.Description().WireVersion 330 331 // Reset deployment. 332 cs.aggregate.Deployment(cs.createOperationDeployment(server, conn)) 333 default: 334 // Do not retry if error is not a driver error. 335 break AggregateExecuteLoop 336 } 337 } 338 if err != nil { 339 cs.err = replaceErrors(err) 340 return cs.err 341 } 342 343 cr := cs.aggregate.ResultCursorResponse() 344 cr.Server = server 345 346 cs.cursor, cs.err = driver.NewBatchCursor(cr, cs.sess, cs.client.clock, cs.cursorOptions) 347 if cs.err = replaceErrors(cs.err); cs.err != nil { 348 return cs.Err() 349 } 350 351 cs.updatePbrtFromCommand() 352 if cs.options.StartAtOperationTime == nil && cs.options.ResumeAfter == nil && 353 cs.options.StartAfter == nil && cs.wireVersion.Max >= 7 && 354 cs.emptyBatch() && cs.resumeToken == nil { 355 cs.operationTime = cs.sess.OperationTime 356 } 357 358 return cs.Err() 359 } 360 361 // Updates the post batch resume token after a successful aggregate or getMore operation. 362 func (cs *ChangeStream) updatePbrtFromCommand() { 363 // Only cache the pbrt if an empty batch was returned and a pbrt was included 364 if pbrt := cs.cursor.PostBatchResumeToken(); cs.emptyBatch() && pbrt != nil { 365 cs.resumeToken = bson.Raw(pbrt) 366 } 367 } 368 369 func (cs *ChangeStream) storeResumeToken() error { 370 // If cs.Current is the last document in the batch and a pbrt is included, cache the pbrt 371 // Otherwise, cache the _id of the document 372 var tokenDoc bson.Raw 373 if len(cs.batch) == 0 { 374 if pbrt := cs.cursor.PostBatchResumeToken(); pbrt != nil { 375 tokenDoc = bson.Raw(pbrt) 376 } 377 } 378 379 if tokenDoc == nil { 380 var ok bool 381 tokenDoc, ok = cs.Current.Lookup("_id").DocumentOK() 382 if !ok { 383 _ = cs.Close(context.Background()) 384 return ErrMissingResumeToken 385 } 386 } 387 388 cs.resumeToken = tokenDoc 389 return nil 390 } 391 392 func (cs *ChangeStream) buildPipelineSlice(pipeline interface{}) error { 393 val := reflect.ValueOf(pipeline) 394 if !val.IsValid() || !(val.Kind() == reflect.Slice) { 395 cs.err = errors.New("can only marshal slices and arrays into aggregation pipelines, but got invalid") 396 return cs.err 397 } 398 399 cs.pipelineSlice = make([]bsoncore.Document, 0, val.Len()+1) 400 401 csIdx, csDoc := bsoncore.AppendDocumentStart(nil) 402 403 csDocTemp, err := cs.createPipelineOptionsDoc() 404 if err != nil { 405 return err 406 } 407 csDoc = bsoncore.AppendDocumentElement(csDoc, "$changeStream", csDocTemp) 408 csDoc, cs.err = bsoncore.AppendDocumentEnd(csDoc, csIdx) 409 if cs.err != nil { 410 return cs.err 411 } 412 cs.pipelineSlice = append(cs.pipelineSlice, csDoc) 413 414 for i := 0; i < val.Len(); i++ { 415 var elem []byte 416 elem, cs.err = marshal(val.Index(i).Interface(), cs.bsonOpts, cs.registry) 417 if cs.err != nil { 418 return cs.err 419 } 420 421 cs.pipelineSlice = append(cs.pipelineSlice, elem) 422 } 423 424 return cs.err 425 } 426 427 func (cs *ChangeStream) createPipelineOptionsDoc() (bsoncore.Document, error) { 428 plDocIdx, plDoc := bsoncore.AppendDocumentStart(nil) 429 430 if cs.streamType == ClientStream { 431 plDoc = bsoncore.AppendBooleanElement(plDoc, "allChangesForCluster", true) 432 } 433 434 if cs.options.FullDocument != nil && *cs.options.FullDocument != options.Default { 435 plDoc = bsoncore.AppendStringElement(plDoc, "fullDocument", string(*cs.options.FullDocument)) 436 } 437 438 if cs.options.FullDocumentBeforeChange != nil { 439 plDoc = bsoncore.AppendStringElement(plDoc, "fullDocumentBeforeChange", string(*cs.options.FullDocumentBeforeChange)) 440 } 441 442 if cs.options.ResumeAfter != nil { 443 var raDoc bsoncore.Document 444 raDoc, cs.err = marshal(cs.options.ResumeAfter, cs.bsonOpts, cs.registry) 445 if cs.err != nil { 446 return nil, cs.err 447 } 448 449 plDoc = bsoncore.AppendDocumentElement(plDoc, "resumeAfter", raDoc) 450 } 451 452 if cs.options.ShowExpandedEvents != nil { 453 plDoc = bsoncore.AppendBooleanElement(plDoc, "showExpandedEvents", *cs.options.ShowExpandedEvents) 454 } 455 456 if cs.options.StartAfter != nil { 457 var saDoc bsoncore.Document 458 saDoc, cs.err = marshal(cs.options.StartAfter, cs.bsonOpts, cs.registry) 459 if cs.err != nil { 460 return nil, cs.err 461 } 462 463 plDoc = bsoncore.AppendDocumentElement(plDoc, "startAfter", saDoc) 464 } 465 466 if cs.options.StartAtOperationTime != nil { 467 plDoc = bsoncore.AppendTimestampElement(plDoc, "startAtOperationTime", cs.options.StartAtOperationTime.T, cs.options.StartAtOperationTime.I) 468 } 469 470 // Append custom pipeline options. 471 for optionName, optionValue := range cs.pipelineOptions { 472 plDoc = bsoncore.AppendValueElement(plDoc, optionName, optionValue) 473 } 474 475 if plDoc, cs.err = bsoncore.AppendDocumentEnd(plDoc, plDocIdx); cs.err != nil { 476 return nil, cs.err 477 } 478 479 return plDoc, nil 480 } 481 482 func (cs *ChangeStream) pipelineToBSON() (bsoncore.Document, error) { 483 pipelineDocIdx, pipelineArr := bsoncore.AppendArrayStart(nil) 484 for i, doc := range cs.pipelineSlice { 485 pipelineArr = bsoncore.AppendDocumentElement(pipelineArr, strconv.Itoa(i), doc) 486 } 487 if pipelineArr, cs.err = bsoncore.AppendArrayEnd(pipelineArr, pipelineDocIdx); cs.err != nil { 488 return nil, cs.err 489 } 490 return pipelineArr, cs.err 491 } 492 493 func (cs *ChangeStream) replaceOptions(wireVersion *description.VersionRange) { 494 // Cached resume token: use the resume token as the resumeAfter option and set no other resume options 495 if cs.resumeToken != nil { 496 cs.options.SetResumeAfter(cs.resumeToken) 497 cs.options.SetStartAfter(nil) 498 cs.options.SetStartAtOperationTime(nil) 499 return 500 } 501 502 // No cached resume token but cached operation time: use the operation time as the startAtOperationTime option and 503 // set no other resume options 504 if (cs.sess.OperationTime != nil || cs.options.StartAtOperationTime != nil) && wireVersion.Max >= 7 { 505 opTime := cs.options.StartAtOperationTime 506 if cs.operationTime != nil { 507 opTime = cs.sess.OperationTime 508 } 509 510 cs.options.SetStartAtOperationTime(opTime) 511 cs.options.SetResumeAfter(nil) 512 cs.options.SetStartAfter(nil) 513 return 514 } 515 516 // No cached resume token or operation time: set none of the resume options 517 cs.options.SetResumeAfter(nil) 518 cs.options.SetStartAfter(nil) 519 cs.options.SetStartAtOperationTime(nil) 520 } 521 522 // ID returns the ID for this change stream, or 0 if the cursor has been closed or exhausted. 523 func (cs *ChangeStream) ID() int64 { 524 if cs.cursor == nil { 525 return 0 526 } 527 return cs.cursor.ID() 528 } 529 530 // SetBatchSize sets the number of documents to fetch from the database with 531 // each iteration of the ChangeStream's "Next" or "TryNext" method. This setting 532 // only affects subsequent document batches fetched from the database. 533 func (cs *ChangeStream) SetBatchSize(size int32) { 534 // Set batch size on the cursor options also so any "resumed" change stream 535 // cursors will pick up the latest batch size setting. 536 cs.cursorOptions.BatchSize = size 537 cs.cursor.SetBatchSize(size) 538 } 539 540 // Decode will unmarshal the current event document into val and return any errors from the unmarshalling process 541 // without any modification. If val is nil or is a typed nil, an error will be returned. 542 func (cs *ChangeStream) Decode(val interface{}) error { 543 if cs.cursor == nil { 544 return ErrNilCursor 545 } 546 547 dec, err := getDecoder(cs.Current, cs.bsonOpts, cs.registry) 548 if err != nil { 549 return fmt.Errorf("error configuring BSON decoder: %w", err) 550 } 551 return dec.Decode(val) 552 } 553 554 // Err returns the last error seen by the change stream, or nil if no errors has occurred. 555 func (cs *ChangeStream) Err() error { 556 if cs.err != nil { 557 return replaceErrors(cs.err) 558 } 559 if cs.cursor == nil { 560 return nil 561 } 562 563 return replaceErrors(cs.cursor.Err()) 564 } 565 566 // Close closes this change stream and the underlying cursor. Next and TryNext must not be called after Close has been 567 // called. Close is idempotent. After the first call, any subsequent calls will not change the state. 568 func (cs *ChangeStream) Close(ctx context.Context) error { 569 if ctx == nil { 570 ctx = context.Background() 571 } 572 573 defer closeImplicitSession(cs.sess) 574 575 if cs.cursor == nil { 576 return nil // cursor is already closed 577 } 578 579 cs.err = replaceErrors(cs.cursor.Close(ctx)) 580 cs.cursor = nil 581 return cs.Err() 582 } 583 584 // ResumeToken returns the last cached resume token for this change stream, or nil if a resume token has not been 585 // stored. 586 func (cs *ChangeStream) ResumeToken() bson.Raw { 587 return cs.resumeToken 588 } 589 590 // Next gets the next event for this change stream. It returns true if there were no errors and the next event document 591 // is available. 592 // 593 // Next blocks until an event is available, an error occurs, or ctx expires. If ctx expires, the error 594 // will be set to ctx.Err(). In an error case, Next will return false. 595 // 596 // If Next returns false, subsequent calls will also return false. 597 func (cs *ChangeStream) Next(ctx context.Context) bool { 598 return cs.next(ctx, false) 599 } 600 601 // TryNext attempts to get the next event for this change stream. It returns true if there were no errors and the next 602 // event document is available. 603 // 604 // TryNext returns false if the change stream is closed by the server, an error occurs when getting changes from the 605 // server, the next change is not yet available, or ctx expires. If ctx expires, the error will be set to ctx.Err(). 606 // 607 // If TryNext returns false and an error occurred or the change stream was closed 608 // (i.e. cs.Err() != nil || cs.ID() == 0), subsequent attempts will also return false. Otherwise, it is safe to call 609 // TryNext again until a change is available. 610 // 611 // This method requires driver version >= 1.2.0. 612 func (cs *ChangeStream) TryNext(ctx context.Context) bool { 613 return cs.next(ctx, true) 614 } 615 616 func (cs *ChangeStream) next(ctx context.Context, nonBlocking bool) bool { 617 // return false right away if the change stream has already errored or if cursor is closed. 618 if cs.err != nil { 619 return false 620 } 621 622 if ctx == nil { 623 ctx = context.Background() 624 } 625 626 if len(cs.batch) == 0 { 627 cs.loopNext(ctx, nonBlocking) 628 if cs.err != nil { 629 cs.err = replaceErrors(cs.err) 630 return false 631 } 632 if len(cs.batch) == 0 { 633 return false 634 } 635 } 636 637 // successfully got non-empty batch 638 cs.Current = bson.Raw(cs.batch[0]) 639 cs.batch = cs.batch[1:] 640 if cs.err = cs.storeResumeToken(); cs.err != nil { 641 return false 642 } 643 return true 644 } 645 646 func (cs *ChangeStream) loopNext(ctx context.Context, nonBlocking bool) { 647 for { 648 if cs.cursor == nil { 649 return 650 } 651 652 if cs.cursor.Next(ctx) { 653 // non-empty batch returned 654 cs.batch, cs.err = cs.cursor.Batch().Documents() 655 return 656 } 657 658 cs.err = replaceErrors(cs.cursor.Err()) 659 if cs.err == nil { 660 // Check if cursor is alive 661 if cs.ID() == 0 { 662 return 663 } 664 665 // If a getMore was done but the batch was empty, the batch cursor will return false with no error. 666 // Update the tracked resume token to catch the post batch resume token from the server response. 667 cs.updatePbrtFromCommand() 668 if nonBlocking { 669 // stop after a successful getMore, even though the batch was empty 670 return 671 } 672 continue // loop getMore until a non-empty batch is returned or an error occurs 673 } 674 675 if !cs.isResumableError() { 676 return 677 } 678 679 // ignore error from cursor close because if the cursor is deleted or errors we tried to close it and will remake and try to get next batch 680 _ = cs.cursor.Close(ctx) 681 if cs.err = cs.executeOperation(ctx, true); cs.err != nil { 682 return 683 } 684 } 685 } 686 687 func (cs *ChangeStream) isResumableError() bool { 688 commandErr, ok := cs.err.(CommandError) 689 if !ok || commandErr.HasErrorLabel(networkErrorLabel) { 690 // All non-server errors or network errors are resumable. 691 return true 692 } 693 694 if commandErr.Code == errorCursorNotFound { 695 return true 696 } 697 698 // For wire versions 9 and above, a server error is resumable if it has the ResumableChangeStreamError label. 699 if cs.wireVersion != nil && cs.wireVersion.Includes(minResumableLabelWireVersion) { 700 return commandErr.HasErrorLabel(resumableErrorLabel) 701 } 702 703 // For wire versions below 9, a server error is resumable if its code is on the allowlist. 704 _, resumable := resumableChangeStreamErrors[commandErr.Code] 705 return resumable 706 } 707 708 // Returns true if the underlying cursor's batch is empty 709 func (cs *ChangeStream) emptyBatch() bool { 710 return cs.cursor.Batch().Empty() 711 } 712 713 // StreamType represents the cluster type against which a ChangeStream was created. 714 type StreamType uint8 715 716 // These constants represent valid change stream types. A change stream can be initialized over a collection, all 717 // collections in a database, or over a cluster. 718 const ( 719 CollectionStream StreamType = iota 720 DatabaseStream 721 ClientStream 722 )