1
1
package mgo
2
2
3
+ import (
4
+ "fmt"
5
+ "reflect"
6
+ "sync"
7
+
8
+ "gopkg.in/mgo.v2/bson"
9
+ )
10
+
3
11
type ChangeStream struct {
4
12
iter * Iter
5
13
options ChangeStreamOptions
6
14
pipeline interface {}
15
+ resumeToken * bson.Raw
16
+ collection * Collection
7
17
readPreference * ReadPreference
18
+ err error
19
+ m sync.Mutex
8
20
}
9
21
10
22
type ChangeStreamOptions struct {
@@ -27,6 +39,74 @@ type ChangeStreamOptions struct {
27
39
Collation * Collation
28
40
}
29
41
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
+
30
110
func constructChangeStreamPipeline (pipeline interface {},
31
111
options ChangeStreamOptions ) interface {} {
32
112
pipelinev := reflect .ValueOf (pipeline )
@@ -38,21 +118,21 @@ func constructChangeStreamPipeline(pipeline interface{},
38
118
39
119
// construct the options to be used by the change notification
40
120
// pipeline stage.
41
- changeNotificationStageOptions := bson.M {}
121
+ changeStreamStageOptions := bson.M {}
42
122
43
123
if options .FullDocument != "" {
44
- changeNotificationStageOptions ["fullDocument" ] = options .FullDocument
124
+ changeStreamStageOptions ["fullDocument" ] = options .FullDocument
45
125
}
46
126
if options .ResumeAfter != nil {
47
- changeNotificationStageOptions ["resumeAfter" ] = options .ResumeAfter
127
+ changeStreamStageOptions ["resumeAfter" ] = options .ResumeAfter
48
128
}
49
- changeNotificationStage := bson.M {"$changeNotification " : changeNotificationStageOptions }
129
+ changeStreamStage := bson.M {"$changeStream " : changeStreamStageOptions }
50
130
51
131
pipeOfInterfaces := make ([]interface {}, pipelinev .Len ()+ 1 )
52
132
53
133
// insert the change notification pipeline stage at the beginning of the
54
134
// aggregation.
55
- pipeOfInterfaces [0 ] = changeNotificationStage
135
+ pipeOfInterfaces [0 ] = changeStreamStage
56
136
57
137
// convert the passed in slice to a slice of interfaces.
58
138
for i := 0 ; i < pipelinev .Len (); i ++ {
@@ -61,3 +141,102 @@ func constructChangeStreamPipeline(pipeline interface{},
61
141
var pipelineAsInterface interface {} = pipeOfInterfaces
62
142
return pipelineAsInterface
63
143
}
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
+ }
0 commit comments