Properly return already exists error#2672
Conversation
Hello benzekrimaha,My role is to assist you with the merge of this Available options
Available commands
Status report is not available. |
405953e to
dff71c5
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## development/8.4 #2672 +/- ##
===================================================
+ Coverage 74.40% 74.42% +0.02%
===================================================
Files 229 229
Lines 18533 18536 +3
Branches 3814 3816 +2
===================================================
+ Hits 13790 13796 +6
+ Misses 4738 4735 -3
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
94f6b1f to
0bd1b04
Compare
Request integration branchesWaiting for integration branch creation to be requested by the user. To request integration branches, please comment on this pull request with the following command: Alternatively, the |
0bd1b04 to
db2b9c2
Compare
07aa045 to
1e8bfe1
Compare
delthas
left a comment
There was a problem hiding this comment.
LGTM code-wise, but didnt go far into the whole issue. Let's wait for Francois approve before merging.
| log.debug('createBucket: collection already sharded', { bucketName }); | ||
| }) | ||
| .then(() => cb(null)) | ||
| .catch(err => { |
There was a problem hiding this comment.
not correct: this would incorrectly catch an exception thrown by cb
(not clear if this was prevent before, with the refactoring...)
There was a problem hiding this comment.
The callback is now invoked from the terminal two-argument .then(success, failure), so an exception thrown by cb is not caught and converted into a second callback. createBucket also returns the Promise chain. I also added a regression test asserting the callback is called once when it throws.
| const finishAfterCollectionReady = () => { | ||
| if (!this.shardCollections) { | ||
| return cb(null); | ||
| } | ||
| const cmd = { | ||
| shardCollection: `${this.database}.${bucketName}`, | ||
| key: { _id: 1 }, | ||
| }; | ||
| return this.adminDb!.command(cmd, {}) | ||
| .catch(err => { | ||
| // Concurrent createBucket calls may race on | ||
| // shardCollection after a successful | ||
| // createCollection. | ||
| if (err.codeName !== 'AlreadyInitialized') { | ||
| throw err; | ||
| } | ||
| log.debug('createBucket: collection already sharded', { bucketName }); | ||
| }) | ||
| .then(() => cb(null)) | ||
| .catch(err => { | ||
| log.error('createBucket: enabling sharding', { error: err }); | ||
| return cb(errors.InternalError); | ||
| }); | ||
| }; | ||
|
|
||
| return this.db!.createCollection(bucketName) | ||
| .then(() => finishAfterCollectionReady()) | ||
| .catch(err => { | ||
| // MongoDB returns NamespaceExists (code 48) when | ||
| // the collection already exists, e.g. on | ||
| // concurrent create/drop/create sequences on MPU | ||
| // shadow buckets. The collection being there is | ||
| // the desired outcome, so treat it as success: | ||
| // this mirrors deleteBucket, which ignores | ||
| // NamespaceNotFound when dropping the collection. | ||
| if (err.codeName !== 'NamespaceExists') { | ||
| throw err; | ||
| } | ||
| log.debug('createBucket: collection already exists', { bucketName }); | ||
| }) | ||
| .then(() => { | ||
| if (this.shardCollections) { | ||
| const cmd = { | ||
| shardCollection: `${this.database}.${bucketName}`, | ||
| key: { _id: 1 }, | ||
| }; | ||
| return this.adminDb!.command(cmd, {}) | ||
| .catch(err => { | ||
| // Concurrent createBucket calls may | ||
| // race on shardCollection: sharding | ||
| // an already-sharded collection fails | ||
| // with AlreadyInitialized. The | ||
| // collection is sharded (with the | ||
| // same {_id: 1} key) either way. | ||
| if (err.codeName !== 'AlreadyInitialized') { | ||
| throw err; | ||
| } | ||
| log.debug('createBucket: collection already sharded', { bucketName }); | ||
| }) | ||
| .then(() => cb(null)) | ||
| .catch(err => { | ||
| log.error('createBucket: enabling sharding', { error: err }); | ||
| return cb(errors.InternalError); | ||
| }); | ||
| if (err.codeName === 'NamespaceExists') { | ||
| // Metastore insert succeeded: an orphaned backing | ||
| // collection is not a bucket-level duplicate. | ||
| log.debug('createBucket: collection already exists', { bucketName }); | ||
| return finishAfterCollectionReady(); |
There was a problem hiding this comment.
not clear what this changes: seems to do the same thing, less readable because of the callback preventing from reading sequentially...
There was a problem hiding this comment.
Removed finishAfterCollectionReady and restored the sequential createCollection → optional shardCollection flow. NamespaceExists and AlreadyInitialized are handled directly at their respective operation boundaries.
Context
Concurrent MongoDB
createBucketcalls could silently behave as successfulidempotent writes. This prevented Cloudserver from distinguishing the winning
request from requests that lost the race and could allow a loser to overwrite
the winner's metadata.
This change restores an explicit
BucketAlreadyExistscontract matching theother metadata backends. It must be deployed with
Cloudserver #6222, which
handles this result in normal and MPU shadow-bucket creation paths.
Changes
$setOnInsertfor the metastore upsert so duplicate requests neveroverwrite existing bucket metadata.
BucketAlreadyExistswhen the atomic upsert matches an existingmetastore entry.
BucketAlreadyExistsas success while initializingusersBucket.NamespaceExistsas success after a new metastore insert because anexisting backing collection does not imply an existing bucket.
NamespaceExistsand treatAlreadyInitializedassuccess.
cannot trigger a second callback.
Issue: ARSN-613