From bec88a29792311a67935a36b56ddfde90441a09f Mon Sep 17 00:00:00 2001 From: Ben Brooks Date: Mon, 6 Jul 2026 15:56:05 +0100 Subject: [PATCH] [4.1.2 Backport] CBG-5522: Implement ScanResultIterator.Err() in gocb range scan wrapper Cherry-picked from 769dfcaba (CBG-5522, #8403). Co-authored-by: Ben Brooks Co-authored-by: Claude Opus 4.8 (1M context) --- base/collection_rangescan.go | 24 +++++++++++++++++++++--- base/collection_rangescan_test.go | 19 +++++++++++++++++++ go.mod | 4 ++-- go.sum | 8 ++++---- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/base/collection_rangescan.go b/base/collection_rangescan.go index 5c710b1370..8826426c9d 100644 --- a/base/collection_rangescan.go +++ b/base/collection_rangescan.go @@ -84,10 +84,28 @@ func (it *gocbScanResultIterator) Next(_ context.Context) *sgbucket.ScanResultIt return result } +func (it *gocbScanResultIterator) Err() error { + if it.err != nil { + return it.err + } + return it.result.Err() +} + func (it *gocbScanResultIterator) Close(_ context.Context) error { - closeErr := it.result.Close() - if it.err == nil { + if it.err != nil { + // A real error is already recorded (e.g. a content-decode failure); + // release the stream but keep the first error. + _ = it.result.Close() + return it.err + } + // gocb's Close returns the first recorded stream error, or nil after + // cancelling a clean scan. + if closeErr := it.result.Close(); closeErr != nil { it.err = closeErr + return it.err } - return it.err + // Clean end-of-stream: record ErrScanCancelled so a later Err reports it + // (matching Rosmar) rather than leaking gocb's internal ErrRequestCanceled. + it.err = sgbucket.ErrScanCancelled + return nil } diff --git a/base/collection_rangescan_test.go b/base/collection_rangescan_test.go index ce7270d9f5..96a707bba8 100644 --- a/base/collection_rangescan_test.go +++ b/base/collection_rangescan_test.go @@ -33,6 +33,8 @@ func collectScanIDs(t testing.TB, ctx context.Context, rss sgbucket.RangeScanSto for item := iter.Next(ctx); item != nil; item = iter.Next(ctx) { ids = append(ids, item.ID) } + // A clean drain must leave no error, inspectable without closing the iterator. + assert.NoError(t, iter.Err()) sort.Strings(ids) return ids } @@ -82,6 +84,7 @@ func runRangeScanSubtests(t *testing.T, ctx context.Context, writeDS sgbucket.Da assert.NotNil(t, item.Body, "Expected body for key %s", item.ID) assert.NotZero(t, item.Cas, "Expected non-zero CAS for key %s", item.ID) } + assert.NoError(t, iter.Err()) sort.Strings(ids) require.Equal(t, allDocIDs, ids) }) @@ -105,6 +108,7 @@ func runRangeScanSubtests(t *testing.T, ctx context.Context, writeDS sgbucket.Da ids = append(ids, item.ID) assert.Nil(t, item.Body, "Expected nil body for IDsOnly scan, key %s", item.ID) } + assert.NoError(t, iter.Err()) sort.Strings(ids) require.Equal(t, allDocIDs, ids) }) @@ -114,6 +118,21 @@ func runRangeScanSubtests(t *testing.T, ctx context.Context, writeDS sgbucket.Da assert.Empty(t, ids) }) + t.Run("CloseCancellation", func(t *testing.T) { + iter, err := scanStore.Scan(ctx, sgbucket.NewRangeScanForPrefix("doc_"), sgbucket.ScanOptions{IDsOnly: true}) + require.NoError(t, err) + for item := iter.Next(ctx); item != nil; item = iter.Next(ctx) { + require.NotEmpty(t, item.ID) + } + // Clean end-of-stream: no error is reported until Close is called. + require.NoError(t, iter.Err()) + // A clean Close returns nil... + require.NoError(t, iter.Close(ctx)) + // ...but records a cancellation, surfaced by a later Err (gocb parity), + // identically on the Rosmar and gocb-backed implementations. + require.ErrorIs(t, iter.Err(), sgbucket.ErrScanCancelled) + }) + t.Run("PrefixScan", func(t *testing.T) { ids := collectScanIDs(t, ctx, scanStore, sgbucket.NewRangeScanForPrefix("doc_c"), sgbucket.ScanOptions{}) require.Equal(t, []string{"doc_c"}, ids) diff --git a/go.mod b/go.mod index 7ab418fa94..af5d6209c6 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,11 @@ require ( github.com/couchbase/gocb/v2 v2.12.3 github.com/couchbase/gocbcore/v10 v10.9.3 github.com/couchbase/gomemcached v0.2.1 - github.com/couchbase/sg-bucket v0.0.0-20260625135331-d9dd0c058146 + github.com/couchbase/sg-bucket v0.0.0-20260706130600-fc995d3ac4be github.com/couchbasedeps/fast-skiplist v0.0.0-20250722125747-e0dd031fe2ac github.com/couchbaselabs/go-fleecedelta v0.0.0-20220909152808-6d09efa7a338 github.com/couchbaselabs/gocbconnstr v1.0.5 - github.com/couchbaselabs/rosmar v0.0.0-20260625145110-3e0b3eaeb1b1 + github.com/couchbaselabs/rosmar v0.0.0-20260706131717-11d9680fd14d github.com/elastic/gosigar v0.14.4 github.com/felixge/fgprof v0.9.5 github.com/go-jose/go-jose/v4 v4.1.4 diff --git a/go.sum b/go.sum index 3e13b36c1d..3cb8c0bdf0 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/couchbase/goprotostellar v1.0.6-0.20260407143512-d7af25156dcc h1:wQfv github.com/couchbase/goprotostellar v1.0.6-0.20260407143512-d7af25156dcc/go.mod h1:X58ot5FRqlBTBkwG/oI4klunpu4MApjGktheqeRWQw0= github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= -github.com/couchbase/sg-bucket v0.0.0-20260625135331-d9dd0c058146 h1:CNwOVIG5a4efMAbAiwUpN+10cslVUcGQzxHgXHAKt08= -github.com/couchbase/sg-bucket v0.0.0-20260625135331-d9dd0c058146/go.mod h1:njSdhrtd7aq+m6raxcu0JNfpwa6CnB58uhwvnrqzjjM= +github.com/couchbase/sg-bucket v0.0.0-20260706130600-fc995d3ac4be h1:x6hodQB/wfuF8cXDlKl9JFb+jDvMVxrWCHqeEevn4/Y= +github.com/couchbase/sg-bucket v0.0.0-20260706130600-fc995d3ac4be/go.mod h1:njSdhrtd7aq+m6raxcu0JNfpwa6CnB58uhwvnrqzjjM= github.com/couchbase/tools-common/cloud/v8 v8.1.3 h1:+fH2+3E8KV8xyXXzbEJNhosMqh08ahauZWlu0m5qtJA= github.com/couchbase/tools-common/cloud/v8 v8.1.3/go.mod h1:5wEMtM4rX92Zl9ylQKEJFgmYNUyFVoWzXCvo31YNO+U= github.com/couchbase/tools-common/fs v1.0.3 h1:KXhisN+hmp5yicOWkUBNjJcd/WsblHA2SVShjL2eGiY= @@ -68,8 +68,8 @@ github.com/couchbaselabs/gocbconnstr v1.0.5 h1:e0JokB5qbcz7rfnxEhNRTKz8q1svoRvDo github.com/couchbaselabs/gocbconnstr v1.0.5/go.mod h1:KV3fnIKMi8/AzX0O9zOrO9rofEqrRF1d2rG7qqjxC7o= github.com/couchbaselabs/gocbconnstr/v2 v2.0.0 h1:HU9DlAYYWR69jQnLN6cpg0fh0hxW/8d5hnglCXXjW78= github.com/couchbaselabs/gocbconnstr/v2 v2.0.0/go.mod h1:o7T431UOfFVHDNvMBUmUxpHnhivwv7BziUao/nMl81E= -github.com/couchbaselabs/rosmar v0.0.0-20260625145110-3e0b3eaeb1b1 h1:d0YibxqBXLRzlxiiIrdsH5KpWmzb+5eb0nYmPuvqxzQ= -github.com/couchbaselabs/rosmar v0.0.0-20260625145110-3e0b3eaeb1b1/go.mod h1:Opac5JFBaP0y2HKWtha2TV9ciBNnFUJ44SiGW11FuwQ= +github.com/couchbaselabs/rosmar v0.0.0-20260706131717-11d9680fd14d h1:aO+wy8QnEj2XsLI5jk01zXvxSjVbOMbpWeLzTtWdkms= +github.com/couchbaselabs/rosmar v0.0.0-20260706131717-11d9680fd14d/go.mod h1:9tmSd+tF1TDFlrlbUB8Q1XKxOoZyJu97wIQ5pTN6B20= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=