1
1
//
2
2
// DISCLAIMER
3
3
//
4
- // Copyright 2017 ArangoDB GmbH, Cologne, Germany
4
+ // Copyright 2023 ArangoDB GmbH, Cologne, Germany
5
5
//
6
6
// Licensed under the Apache License, Version 2.0 (the "License");
7
7
// you may not use this file except in compliance with the License.
17
17
//
18
18
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19
19
//
20
- // Author Ewout Prangsma
21
- //
22
20
23
21
package driver
24
22
@@ -37,19 +35,30 @@ func newCursor(data cursorData, endpoint string, db *database, allowDirtyReads b
37
35
if db == nil {
38
36
return nil , WithStack (InvalidArgumentError {Message : "db is nil" })
39
37
}
40
- return & cursor {
38
+
39
+ c := & cursor {
41
40
cursorData : data ,
42
41
endpoint : endpoint ,
43
42
db : db ,
44
43
conn : db .conn ,
45
44
allowDirtyReads : allowDirtyReads ,
46
- }, nil
45
+ }
46
+
47
+ if data .NextBatchID != "" {
48
+ c .retryData = & retryData {
49
+ cursorID : data .ID ,
50
+ currentBatchID : "1" ,
51
+ }
52
+ }
53
+
54
+ return c , nil
47
55
}
48
56
49
57
type cursor struct {
50
58
cursorData
51
59
endpoint string
52
60
resultIndex int
61
+ retryData * retryData
53
62
db * database
54
63
conn Connection
55
64
closed int32
@@ -58,6 +67,11 @@ type cursor struct {
58
67
lastReadWasDirty bool
59
68
}
60
69
70
+ type retryData struct {
71
+ cursorID string
72
+ currentBatchID string
73
+ }
74
+
61
75
// CursorStats TODO: all these int64 should be changed into uint64
62
76
type cursorStats struct {
63
77
// The total number of data-modification operations successfully executed.
@@ -86,11 +100,11 @@ type cursorStats struct {
86
100
CursorsRearmed uint64 `json:"cursorsRearmed,omitempty"`
87
101
// CacheHits the total number of index entries read from in-memory caches for indexes of type edge or persistent.
88
102
// This value will only be non-zero when reading from indexes that have an in-memory cache enabled,
89
- // and when the query allows using the in-memory cache (i.e. using equality lookups on all index attributes).
103
+ // and when the query allows using the in-memory cache (i.e., using equality lookups on all index attributes).
90
104
CacheHits uint64 `json:"cacheHits,omitempty"`
91
105
// CacheMisses the total number of cache read attempts for index entries that could not be served from in-memory caches for indexes of type edge or persistent.
92
106
// This value will only be non-zero when reading from indexes that have an in-memory cache enabled,
93
- // the query allows using the in-memory cache (i.e. using equality lookups on all index attributes) and the looked up values are not present in the cache.
107
+ // the query allows using the in-memory cache (i.e., using equality lookups on all index attributes), and the looked- up values are not present in the cache.
94
108
CacheMisses uint64 `json:"cacheMisses,omitempty"`
95
109
}
96
110
@@ -163,13 +177,14 @@ type cursorPlanNodes map[string]interface{}
163
177
type cursorProfile map [string ]interface {}
164
178
165
179
type cursorData struct {
166
- Key string `json:"_key,omitempty"`
167
- Count int64 `json:"count,omitempty"` // the total number of result documents available (only available if the query was executed with the count attribute set)
168
- ID string `json:"id"` // id of temporary cursor created on the server (optional, see above)
169
- Result []* RawObject `json:"result,omitempty"` // an array of result documents (might be empty if query has no results)
170
- HasMore bool `json:"hasMore,omitempty"` // A boolean indicator whether there are more results available for the cursor on the server
171
- Extra cursorExtra `json:"extra"`
172
- Cached bool `json:"cached,omitempty"`
180
+ Key string `json:"_key,omitempty"`
181
+ Count int64 `json:"count,omitempty"` // the total number of result documents available (only available if the query was executed with the count attribute set)
182
+ ID string `json:"id"` // id of temporary cursor created on the server (optional, see above)
183
+ Result []* RawObject `json:"result,omitempty"` // an array of result documents (might be empty if the query has no results)
184
+ HasMore bool `json:"hasMore,omitempty"` // A boolean indicator whether there are more results available for the cursor on the server
185
+ Extra cursorExtra `json:"extra"`
186
+ Cached bool `json:"cached,omitempty"`
187
+ NextBatchID string `json:"nextBatchId,omitempty"`
173
188
ArangoError
174
189
}
175
190
@@ -178,22 +193,22 @@ func (c *cursor) relPath() string {
178
193
return path .Join (c .db .relPath (), "_api" , "cursor" )
179
194
}
180
195
181
- // Name returns the name of the collection.
196
+ // HasMore Name returns the name of the collection.
182
197
func (c * cursor ) HasMore () bool {
183
198
return c .resultIndex < len (c .Result ) || c .cursorData .HasMore
184
199
}
185
200
186
201
// Count returns the total number of result documents available.
187
202
// A valid return value is only available when the cursor has been created with a context that was
188
- // prepare with `WithQueryCount`.
203
+ // prepared with `WithQueryCount`.
189
204
func (c * cursor ) Count () int64 {
190
205
return c .cursorData .Count
191
206
}
192
207
193
208
// Close deletes the cursor and frees the resources associated with it.
194
209
func (c * cursor ) Close () error {
195
210
if c == nil {
196
- // Avoid panics in the case that someone defer's a close before checking that the cursor is not nil.
211
+ // Avoid panics in the case that someone defers a close before checking that the cursor is not nil.
197
212
return nil
198
213
}
199
214
if c := atomic .LoadInt32 (& c .closed ); c != 0 {
@@ -224,28 +239,60 @@ func (c *cursor) Close() error {
224
239
}
225
240
226
241
// ReadDocument reads the next document from the cursor.
227
- // The document data is stored into result, the document meta data is returned.
242
+ // The document data is stored into the result, the document metadata is returned.
228
243
// If the cursor has no more documents, a NoMoreDocuments error is returned.
229
244
func (c * cursor ) ReadDocument (ctx context.Context , result interface {}) (DocumentMeta , error ) {
245
+ return c .readDocument (ctx , result , "" )
246
+ }
247
+
248
+ // RetryReadDocument reads the last document from the cursor once more time
249
+ // It can be used e.g., in case of network error during ReadDocument
250
+ // It requires 'driver.WithQueryAllowRetry' to be set to true on the Context during Cursor creation.
251
+ func (c * cursor ) RetryReadDocument (ctx context.Context , result interface {}) (DocumentMeta , error ) {
252
+ if c .resultIndex > 0 {
253
+ c .resultIndex --
254
+ }
255
+ return c .readDocument (ctx , result , c .retryData .currentBatchID )
256
+ }
257
+
258
+ func (c * cursor ) readDocument (ctx context.Context , result interface {}, retryBatchID string ) (DocumentMeta , error ) {
230
259
// Force use of initial endpoint
231
260
ctx = WithEndpoint (ctx , c .endpoint )
232
261
233
- if c .resultIndex >= len (c .Result ) && c .cursorData .HasMore {
234
- // This is required since we are interested if this was a dirty read
262
+ if c .resultIndex >= len (c .Result ) && ( c .cursorData .HasMore || retryBatchID != "" ) {
263
+ // This is required since we are interested if this was a dirty read,
235
264
// but we do not want to trash the users bool reference.
236
265
var wasDirtyRead bool
237
- fetchctx := ctx
266
+ fetchCtx := ctx
238
267
if c .allowDirtyReads {
239
- fetchctx = WithAllowDirtyReads (ctx , & wasDirtyRead )
268
+ fetchCtx = WithAllowDirtyReads (ctx , & wasDirtyRead )
269
+ }
270
+
271
+ p := path .Join (c .relPath (), c .cursorData .ID )
272
+
273
+ // If we have a NextBatchID, use it
274
+ if c .NextBatchID != "" {
275
+ p = path .Join (c .relPath (), c .cursorData .ID , c .NextBatchID )
240
276
}
241
277
242
- // Fetch next batch
243
- req , err := c .conn .NewRequest ("PUT" , path .Join (c .relPath (), c .cursorData .ID ))
278
+ // We have to retry the batch instead of fetching the next one
279
+ if retryBatchID != "" {
280
+ p = path .Join (c .relPath (), c .retryData .cursorID , retryBatchID )
281
+ }
282
+
283
+ // Update currentBatchID before fetching the next batch (no retry case)
284
+ if c .NextBatchID != "" && retryBatchID == "" {
285
+ c .retryData .currentBatchID = c .NextBatchID
286
+ }
287
+
288
+ // Fetch the next batch
289
+ req , err := c .conn .NewRequest ("POST" , p )
244
290
if err != nil {
245
291
return DocumentMeta {}, WithStack (err )
246
292
}
247
- cs := applyContextSettings (fetchctx , req )
248
- resp , err := c .conn .Do (fetchctx , req )
293
+
294
+ cs := applyContextSettings (fetchCtx , req )
295
+ resp , err := c .conn .Do (fetchCtx , req )
249
296
if err != nil {
250
297
return DocumentMeta {}, WithStack (err )
251
298
}
@@ -295,7 +342,7 @@ func (c *cursor) ReadDocument(ctx context.Context, result interface{}) (Document
295
342
return meta , nil
296
343
}
297
344
298
- // Return execution statistics for this cursor. This might not
345
+ // Statistics Return execution statistics for this cursor. This might not
299
346
// be valid if the cursor has been created with a context that was
300
347
// prepared with `WithStream`
301
348
func (c * cursor ) Statistics () QueryStatistics {
@@ -306,40 +353,40 @@ func (c *cursor) Extra() QueryExtra {
306
353
return c .cursorData .Extra
307
354
}
308
355
309
- // the total number of data-modification operations successfully executed.
356
+ // WritesExecuted the total number of data-modification operations successfully executed.
310
357
func (cs cursorStats ) WritesExecuted () int64 {
311
358
return cs .WritesExecutedInt
312
359
}
313
360
314
- // The total number of data-modification operations that were unsuccessful
361
+ // WritesIgnored The total number of data-modification operations that were unsuccessful
315
362
func (cs cursorStats ) WritesIgnored () int64 {
316
363
return cs .WritesIgnoredInt
317
364
}
318
365
319
- // The total number of documents iterated over when scanning a collection without an index.
366
+ // ScannedFull The total number of documents iterated over when scanning a collection without an index.
320
367
func (cs cursorStats ) ScannedFull () int64 {
321
368
return cs .ScannedFullInt
322
369
}
323
370
324
- // The total number of documents iterated over when scanning a collection using an index.
371
+ // ScannedIndex The total number of documents iterated over when scanning a collection using an index.
325
372
func (cs cursorStats ) ScannedIndex () int64 {
326
373
return cs .ScannedIndexInt
327
374
}
328
375
329
- // the total number of documents that were removed after executing a filter condition in a FilterNode
376
+ // Filtered the total number of documents that were removed after executing a filter condition in a FilterNode
330
377
func (cs cursorStats ) Filtered () int64 {
331
378
return cs .FilteredInt
332
379
}
333
380
334
- // Returns the numer of results before the last LIMIT in the query was applied.
381
+ // FullCount Returns the number of results before the last LIMIT in the query was applied.
335
382
// A valid return value is only available when the has been created with a context that was
336
- // prepared with `WithFullCount`. Additionally this will also not return a valid value if
383
+ // prepared with `WithFullCount`. Additionally, this will also not return a valid value if
337
384
// the context was prepared with `WithStream`.
338
385
func (cs cursorStats ) FullCount () int64 {
339
386
return cs .FullCountInt
340
387
}
341
388
342
- // query execution time (wall-clock time). value will be set from the outside
389
+ // ExecutionTime query execution time (wall-clock time). value will be set from the outside
343
390
func (cs cursorStats ) ExecutionTime () time.Duration {
344
391
return time .Duration (cs .ExecutionTimeInt * float64 (time .Second ))
345
392
}
0 commit comments