Skip to content

Commit 7940d20

Browse files
author
Will Banfield
committed
MGO-140 implement Next() and resume logic for ChangeStream
1 parent 31a6d57 commit 7940d20

File tree

2 files changed

+204
-11
lines changed

2 files changed

+204
-11
lines changed

changestreams.go

Lines changed: 184 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
package mgo
22

3+
import (
4+
"fmt"
5+
"reflect"
6+
"sync"
7+
8+
"gopkg.in/mgo.v2/bson"
9+
)
10+
311
type ChangeStream struct {
412
iter *Iter
513
options ChangeStreamOptions
614
pipeline interface{}
15+
resumeToken *bson.Raw
16+
collection *Collection
717
readPreference *ReadPreference
18+
err error
19+
m sync.Mutex
820
}
921

1022
type ChangeStreamOptions struct {
@@ -27,6 +39,74 @@ type ChangeStreamOptions struct {
2739
Collation *Collation
2840
}
2941

42+
// Next retrieves the next document from the change stream, blocking if necessary.
43+
// Next returns true if a document was successfully unmarshalled into result,
44+
// and false if an error occured. When Next returns false, the Err method should
45+
// be called to check what error occurred during iteration.
46+
//
47+
// For example:
48+
//
49+
// pipeline := []bson.M{}
50+
//
51+
// changeStream := collection.Watch(pipeline, ChangeStreamOptions{})
52+
// for changeStream.Next(&changeDoc) {
53+
// fmt.Printf("Change: %v\n", changeDoc)
54+
// }
55+
//
56+
// if err := changeStream.Close(); err != nil {
57+
// return err
58+
// }
59+
//
60+
// If the pipeline used removes the _id field from the result, Next will error
61+
// because the _id field is needed to resume iteration when an error occurs.
62+
//
63+
func (changeStream *ChangeStream) Next(result interface{}) bool {
64+
// the err field is being constantly overwritten and we don't want the user to
65+
// attempt to read it at this point so we lock.
66+
changeStream.m.Lock()
67+
68+
defer changeStream.m.Unlock()
69+
70+
// if we are in a state of error, then don't continue.
71+
if changeStream.err != nil {
72+
return false
73+
}
74+
75+
var err error
76+
77+
// attempt to fetch the change stream result.
78+
err = changeStream.fetchResultSet(result)
79+
if err == nil {
80+
return true
81+
}
82+
83+
// check if the error is resumable
84+
if !isResumableError(err) {
85+
// error is not resumable, give up and return it to the user.
86+
changeStream.err = err
87+
return false
88+
}
89+
90+
// try to resume.
91+
err = changeStream.resume()
92+
if err != nil {
93+
// we've not been able to successfully resume and should only try once,
94+
// so we give up.
95+
changeStream.err = err
96+
return false
97+
}
98+
99+
// we've successfully resumed the changestream.
100+
// try to fetch the next result.
101+
err = changeStream.fetchResultSet(result)
102+
if err != nil {
103+
changeStream.err = err
104+
return false
105+
}
106+
107+
return true
108+
}
109+
30110
func constructChangeStreamPipeline(pipeline interface{},
31111
options ChangeStreamOptions) interface{} {
32112
pipelinev := reflect.ValueOf(pipeline)
@@ -38,21 +118,21 @@ func constructChangeStreamPipeline(pipeline interface{},
38118

39119
// construct the options to be used by the change notification
40120
// pipeline stage.
41-
changeNotificationStageOptions := bson.M{}
121+
changeStreamStageOptions := bson.M{}
42122

43123
if options.FullDocument != "" {
44-
changeNotificationStageOptions["fullDocument"] = options.FullDocument
124+
changeStreamStageOptions["fullDocument"] = options.FullDocument
45125
}
46126
if options.ResumeAfter != nil {
47-
changeNotificationStageOptions["resumeAfter"] = options.ResumeAfter
127+
changeStreamStageOptions["resumeAfter"] = options.ResumeAfter
48128
}
49-
changeNotificationStage := bson.M{"$changeNotification": changeNotificationStageOptions}
129+
changeStreamStage := bson.M{"$changeStream": changeStreamStageOptions}
50130

51131
pipeOfInterfaces := make([]interface{}, pipelinev.Len()+1)
52132

53133
// insert the change notification pipeline stage at the beginning of the
54134
// aggregation.
55-
pipeOfInterfaces[0] = changeNotificationStage
135+
pipeOfInterfaces[0] = changeStreamStage
56136

57137
// convert the passed in slice to a slice of interfaces.
58138
for i := 0; i < pipelinev.Len(); i++ {
@@ -61,3 +141,102 @@ func constructChangeStreamPipeline(pipeline interface{},
61141
var pipelineAsInterface interface{} = pipeOfInterfaces
62142
return pipelineAsInterface
63143
}
144+
145+
func (changeStream *ChangeStream) resume() error {
146+
// copy the information for the new socket.
147+
148+
// Copy() destroys the sockets currently associated with this session
149+
// so future uses will acquire a new socket against the newly selected DB.
150+
newSession := changeStream.iter.session.Copy()
151+
152+
// fetch the cursor from the iterator and use it to run a killCursors
153+
// on the connection.
154+
cursorId := changeStream.iter.op.cursorId
155+
err := runKillCursorsOnSession(newSession, cursorId)
156+
if err != nil {
157+
return err
158+
}
159+
160+
// change out the old connection to the database with the new connection.
161+
changeStream.collection.Database.Session = newSession
162+
163+
// make a new pipeline containing the resume token.
164+
changeStreamPipeline := constructChangeStreamPipeline(changeStream.pipeline, changeStream.options)
165+
166+
// generate the new iterator with the new connection.
167+
newPipe := changeStream.collection.Pipe(changeStreamPipeline)
168+
changeStream.iter = newPipe.Iter()
169+
changeStream.iter.isChangeStream = true
170+
171+
return nil
172+
}
173+
174+
// fetchResumeToken unmarshals the _id field from the document, setting an error
175+
// on the changeStream if it is unable to.
176+
func (changeStream *ChangeStream) fetchResumeToken(rawResult *bson.Raw) error {
177+
changeStreamResult := struct {
178+
ResumeToken *bson.Raw `bson:"_id,omitempty"`
179+
}{}
180+
181+
err := rawResult.Unmarshal(&changeStreamResult)
182+
if err != nil {
183+
return err
184+
}
185+
186+
if changeStreamResult.ResumeToken == nil {
187+
return fmt.Errorf("resume token missing from result")
188+
}
189+
190+
changeStream.resumeToken = changeStreamResult.ResumeToken
191+
return nil
192+
}
193+
194+
func (changeStream *ChangeStream) fetchResultSet(result interface{}) error {
195+
rawResult := bson.Raw{}
196+
197+
// fetch the next set of documents from the cursor.
198+
gotNext := changeStream.iter.Next(&rawResult)
199+
200+
err := changeStream.iter.Err()
201+
if err != nil {
202+
return err
203+
}
204+
205+
if !gotNext && err == nil {
206+
// If the iter.Err() method returns nil despite us not getting a next batch,
207+
// it is becuase iter.Err() silences this case.
208+
return ErrNotFound
209+
}
210+
211+
// grab the resumeToken from the results
212+
if err := changeStream.fetchResumeToken(&rawResult); err != nil {
213+
return err
214+
}
215+
216+
// put the raw results into the data structure the user provided.
217+
if err := rawResult.Unmarshal(result); err != nil {
218+
return err
219+
}
220+
return nil
221+
}
222+
223+
func isResumableError(err error) bool {
224+
_, isQueryError := err.(*QueryError)
225+
// if it is not a database error OR it is a database error,
226+
// but the error is a notMaster error
227+
return !isQueryError || isNotMasterError(err)
228+
}
229+
230+
func runKillCursorsOnSession(session *Session, cursorId int64) error {
231+
socket, err := session.acquireSocket(true)
232+
if err != nil {
233+
return err
234+
}
235+
err = socket.Query(&killCursorsOp{[]int64{cursorId}})
236+
if err != nil {
237+
return err
238+
}
239+
socket.Release()
240+
241+
return nil
242+
}

session.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ type Iter struct {
138138
docsBeforeMore int
139139
timeout time.Duration
140140
timedout bool
141-
findCmd bool
141+
isFindCmd bool
142+
isChangeStream bool
142143
}
143144

144145
var (
@@ -993,6 +994,11 @@ func isAuthError(err error) bool {
993994
return ok && e.Code == 13
994995
}
995996

997+
func isNotMasterError(err error) bool {
998+
e, ok := err.(*QueryError)
999+
return ok && strings.Contains(e.Message, "not master")
1000+
}
1001+
9961002
func (db *Database) runUserCmd(cmdName string, user *User) error {
9971003
cmd := make(bson.D, 0, 16)
9981004
cmd = append(cmd, bson.DocElem{cmdName, user.Username})
@@ -2382,7 +2388,7 @@ func (c *Collection) NewIter(session *Session, firstBatch []bson.Raw, cursorId i
23822388
}
23832389

23842390
if socket.ServerInfo().MaxWireVersion >= 4 && c.FullName != "admin.$cmd" {
2385-
iter.findCmd = true
2391+
iter.isFindCmd = true
23862392
}
23872393

23882394
iter.gotReply.L = &iter.m
@@ -3550,7 +3556,7 @@ func (q *Query) Iter() *Iter {
35503556
op.replyFunc = iter.op.replyFunc
35513557

35523558
if prepareFindOp(socket, &op, limit) {
3553-
iter.findCmd = true
3559+
iter.isFindCmd = true
35543560
}
35553561

35563562
iter.server = socket.Server()
@@ -3780,7 +3786,12 @@ func (iter *Iter) Next(result interface{}) bool {
37803786
iter.m.Lock()
37813787
iter.timedout = false
37823788
timeout := time.Time{}
3789+
3790+
// check should we expect more data.
37833791
for iter.err == nil && iter.docData.Len() == 0 && (iter.docsToReceive > 0 || iter.op.cursorId != 0) {
3792+
// we should expect more data.
3793+
3794+
// If we have yet to receive data, increment the timer until we timeout.
37843795
if iter.docsToReceive == 0 {
37853796
if iter.timeout >= 0 {
37863797
if timeout.IsZero() {
@@ -3792,6 +3803,7 @@ func (iter *Iter) Next(result interface{}) bool {
37923803
return false
37933804
}
37943805
}
3806+
// run a getmore to fetch more data.
37953807
iter.getMore()
37963808
if iter.err != nil {
37973809
break
@@ -3800,6 +3812,7 @@ func (iter *Iter) Next(result interface{}) bool {
38003812
iter.gotReply.Wait()
38013813
}
38023814

3815+
// We have data from the getMore.
38033816
// Exhaust available data before reporting any errors.
38043817
if docData, ok := iter.docData.Pop().([]byte); ok {
38053818
close := false
@@ -3815,6 +3828,7 @@ func (iter *Iter) Next(result interface{}) bool {
38153828
}
38163829
}
38173830
if iter.op.cursorId != 0 && iter.err == nil {
3831+
// we still have a live cursor and currently expect data.
38183832
iter.docsBeforeMore--
38193833
if iter.docsBeforeMore == -1 {
38203834
iter.getMore()
@@ -4004,7 +4018,7 @@ func (iter *Iter) getMore() {
40044018
}
40054019
}
40064020
var op interface{}
4007-
if iter.findCmd {
4021+
if iter.isFindCmd || iter.isChangeStream {
40084022
op = iter.getMoreCmd()
40094023
} else {
40104024
op = &iter.op
@@ -4608,7 +4622,7 @@ func (iter *Iter) replyFunc() replyFunc {
46084622
} else {
46094623
iter.err = ErrNotFound
46104624
}
4611-
} else if iter.findCmd {
4625+
} else if iter.isFindCmd {
46124626
debugf("Iter %p received reply document %d/%d (cursor=%d)", iter, docNum+1, int(op.replyDocs), op.cursorId)
46134627
var findReply struct {
46144628
Ok bool
@@ -4620,7 +4634,7 @@ func (iter *Iter) replyFunc() replyFunc {
46204634
iter.err = err
46214635
} else if !findReply.Ok && findReply.Errmsg != "" {
46224636
iter.err = &QueryError{Code: findReply.Code, Message: findReply.Errmsg}
4623-
} else if len(findReply.Cursor.FirstBatch) == 0 && len(findReply.Cursor.NextBatch) == 0 {
4637+
} else if !iter.isChangeStream && len(findReply.Cursor.FirstBatch) == 0 && len(findReply.Cursor.NextBatch) == 0 {
46244638
iter.err = ErrNotFound
46254639
} else {
46264640
batch := findReply.Cursor.FirstBatch

0 commit comments

Comments
 (0)