Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Bc/bulk error debug#1330

Closed
schmidt-sebastian wants to merge 36 commits intomasterfrom
bc/bulk-error-debug
Closed

Bc/bulk error debug#1330
schmidt-sebastian wants to merge 36 commits intomasterfrom
bc/bulk-error-debug

Conversation

@schmidt-sebastian
Copy link
Copy Markdown
Contributor

Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> 🦕

@schmidt-sebastian schmidt-sebastian requested review from a team October 15, 2020 03:42
@schmidt-sebastian schmidt-sebastian marked this pull request as draft October 15, 2020 03:42
@google-cla google-cla Bot added the cla: yes This human has signed the Contributor License Agreement. label Oct 15, 2020
@product-auto-label product-auto-label Bot added the api: firestore Issues related to the googleapis/nodejs-firestore API. label Oct 15, 2020
Copy link
Copy Markdown
Contributor Author

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. We are pretty close. Feel free to ignore feedback that is contradicting :)

Comment thread dev/src/bulk-writer.ts Outdated
Comment thread dev/src/bulk-writer.ts Outdated
Comment thread dev/src/util.ts Outdated
Comment on lines +178 to +183
export function voidPromise(promise: Promise<unknown>): Promise<void> {
return promise.then(
() => {},
() => {}
);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scary part of this is that it swallows all errors. This is what you want, but it should apparent from the name.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Comment thread dev/src/write-batch.ts Outdated
*
* @private
*/
_addOperation(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now unused.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted.

Comment thread dev/src/write-batch.ts Outdated
*
* @private
*/
get _lastOp(): PendingWriteOp {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now unused.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted.

Comment thread dev/src/bulk-writer.ts Outdated
});
}

private wrapRetry(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a method comment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed and done.

Comment thread dev/src/bulk-writer.ts Outdated
private wrapRetry(
promise: Promise<WriteResult>,
documentRef: firestore.DocumentReference,
retryCount: number,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, not sure what this means.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to failedAttempts.

Comment thread dev/src/bulk-writer.ts Outdated
Comment thread dev/src/bulk-writer.ts Outdated
} else {
throw bulkWriterError;
}
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This might be slightly prettier if we used try/catch with async/await here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea. Looks much cleaner and easier to understand.

Comment thread dev/src/bulk-writer.ts Outdated
documentRef: firestore.DocumentReference,
retryCount: number,
operationType: 'create' | 'set' | 'update' | 'delete',
retryFn: () => Promise<WriteResult>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the original function isn't it? Could we just call it in this method ourselves, which would drop the promise argument.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High IQ vision. +10000

@codecov
Copy link
Copy Markdown

codecov Bot commented Oct 21, 2020

Codecov Report

Merging #1330 into master will increase coverage by 0.02%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1330      +/-   ##
==========================================
+ Coverage   98.48%   98.51%   +0.02%     
==========================================
  Files          32       32              
  Lines       19108    19282     +174     
  Branches     1377     1382       +5     
==========================================
+ Hits        18819    18995     +176     
+ Misses        286      284       -2     
  Partials        3        3              
Impacted Files Coverage Δ
dev/src/bulk-writer.ts 99.80% <100.00%> (+0.04%) ⬆️
dev/src/util.ts 98.50% <100.00%> (+0.11%) ⬆️
dev/src/write-batch.ts 100.00% <100.00%> (ø)
dev/src/pool.ts 98.23% <0.00%> (-0.45%) ⬇️
dev/src/watch.ts 99.06% <0.00%> (-0.12%) ⬇️
dev/src/document.ts 98.68% <0.00%> (-0.10%) ⬇️
dev/src/index.ts 97.52% <0.00%> (-0.07%) ⬇️
dev/src/reference.ts 99.89% <0.00%> (+<0.01%) ⬆️
dev/src/v1/firestore_client.ts 98.17% <0.00%> (+0.05%) ⬆️
dev/src/v1/firestore_admin_client.ts 98.84% <0.00%> (+0.05%) ⬆️
... and 4 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6b7371b...9a2a792. Read the comment docs.

Comment thread dev/src/document.ts Outdated
// on end users.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(field: string | FieldPath): any {
console.error('current field', field);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove.

Comment thread dev/src/util.ts Outdated
* This is primarily used to wait for a promise to complete when the result of
* the promise will be discarded.
*/
export function voidPromiseNoThrow(promise: Promise<unknown>): Promise<void> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional suggestion: s/voidPromiseNoThrow/silence?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to silencePromise.

Comment thread dev/src/bulk-writer.ts Outdated
* new operations can be enqueued, except for retry operations scheduled by
* the error handler.
*/
private _closed = false;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/_closed/_closing based on your comment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Comment thread dev/src/bulk-writer.ts
* fails.
*/
private _errorFn: (error: BulkWriterError) => boolean = error => {
const retryCodes = getRetryCodes('batchWrite');
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding logging since the comment thread doesn't show up:

I think we need to add logging, otherwise we have no insight into what happens and why writes get re-scheduled. I am not worried about this being noisy, but we should log to the log function and not to console.log.

Note that a user-provided error handler would also overwrite this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added logger.

Comment thread dev/src/bulk-writer.ts Outdated
* bulkWriter
* .onWriteError((error) => {
* if (
* shouldRetry(error.code) &&
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you just use some hardcoded error check (error.code === GrpcStatus.UNAVAILABLE) in this example?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Comment thread dev/src/bulk-writer.ts Outdated

this.sendReadyBatches();
if (batchQueue === this._retryBatchQueue) {
this._retryBatchQueue.forEach(batch => batch.markReadyToSend());
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is going to send these batches? Don't we need to schedule a commit here so that the retries work even without flush()? If we do so, we might not need to retry in _flush().

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to power all retry batch scheduling to sendBatch(). Now, flush() schedules writes on the batchQueue, then kicks off the retryBatch if there are retries. Subsequent retries are now handled when the first retry batch comes back.

Also, I moved the markReadyToSend() mapping for retryBatches into sendReadyBatches(). There's two call points (flush and sendBatch) that would require a forEach() call, and I got bitten several times by forgetting to mark the batches as ready to send.

Comment thread dev/src/bulk-writer.ts
Comment on lines +890 to +960
documentRef: firestore.DocumentReference,
operationType: 'create' | 'set' | 'update' | 'delete',
operationFn: (bulkCommitBatch: BulkCommitBatch) => Promise<WriteResult>
): Promise<WriteResult> {
this.verifyNotClosed();

// A deferred promise that resolves when operationFn completes.
const operationCompletedDeferred = new Deferred<void>();
this._pendingOps.add(operationCompletedDeferred.promise);

const op = this.runOperationWithRetries(
documentRef,
operationType,
operationFn,
/* failedAttempts= */ 0
);

this.sendReadyBatches();
return op
.then(res => {
operationCompletedDeferred.resolve();
this._pendingOps.delete(operationCompletedDeferred.promise);
return res;
})
.catch(err => {
operationCompletedDeferred.resolve();
this._pendingOps.delete(operationCompletedDeferred.promise);
throw err;
});
}

/**
* Runs the provided operation and retries on failure until the error
* callback says not to.
*/
private async runOperationWithRetries(
documentRef: firestore.DocumentReference,
operationType: 'create' | 'set' | 'update' | 'delete',
operationFn: (bulkCommitBatch: BulkCommitBatch) => Promise<WriteResult>,
failedAttempts: number
): Promise<WriteResult> {
try {
const batchQueue =
failedAttempts > 0 ? this._retryBatchQueue : this._batchQueue;
const bulkCommitBatch = this.getEligibleBatch(batchQueue);
const operationResult = await operationFn(bulkCommitBatch);

this._successFn(documentRef, operationResult);
return operationResult;
} catch (error) {
const bulkWriterError = new BulkWriterError(
error.code,
error.message,
documentRef,
operationType,
failedAttempts
);

const shouldRetry = this._errorFn(bulkWriterError);
if (shouldRetry) {
return this.runOperationWithRetries(
documentRef,
operationType,
operationFn,
failedAttempts + 1
);
} else {
throw bulkWriterError;
}
}
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification suggestion:

Suggested change
documentRef: firestore.DocumentReference,
operationType: 'create' | 'set' | 'update' | 'delete',
operationFn: (bulkCommitBatch: BulkCommitBatch) => Promise<WriteResult>
): Promise<WriteResult> {
this.verifyNotClosed();
// A deferred promise that resolves when operationFn completes.
const operationCompletedDeferred = new Deferred<void>();
this._pendingOps.add(operationCompletedDeferred.promise);
const op = this.runOperationWithRetries(
documentRef,
operationType,
operationFn,
/* failedAttempts= */ 0
);
this.sendReadyBatches();
return op
.then(res => {
operationCompletedDeferred.resolve();
this._pendingOps.delete(operationCompletedDeferred.promise);
return res;
})
.catch(err => {
operationCompletedDeferred.resolve();
this._pendingOps.delete(operationCompletedDeferred.promise);
throw err;
});
}
/**
* Runs the provided operation and retries on failure until the error
* callback says not to.
*/
private async runOperationWithRetries(
documentRef: firestore.DocumentReference,
operationType: 'create' | 'set' | 'update' | 'delete',
operationFn: (bulkCommitBatch: BulkCommitBatch) => Promise<WriteResult>,
failedAttempts: number
): Promise<WriteResult> {
try {
const batchQueue =
failedAttempts > 0 ? this._retryBatchQueue : this._batchQueue;
const bulkCommitBatch = this.getEligibleBatch(batchQueue);
const operationResult = await operationFn(bulkCommitBatch);
this._successFn(documentRef, operationResult);
return operationResult;
} catch (error) {
const bulkWriterError = new BulkWriterError(
error.code,
error.message,
documentRef,
operationType,
failedAttempts
);
const shouldRetry = this._errorFn(bulkWriterError);
if (shouldRetry) {
return this.runOperationWithRetries(
documentRef,
operationType,
operationFn,
failedAttempts + 1
);
} else {
throw bulkWriterError;
}
}
}
private async runOperation(
documentRef: firestore.DocumentReference,
operationType: 'create' | 'set' | 'update' | 'delete',
operationFn: (bulkCommitBatch: BulkCommitBatch) => Promise<WriteResult>
): Promise<WriteResult> {
this.verifyNotClosed();
this.sendReadyBatches();
for (let failedAttempts = 0; ; ++failedAttempts) {
// A deferred promise that resolves when operationFn completes.
const operationCompletedDeferred = new Deferred<void>();
this._pendingOps.add(operationCompletedDeferred.promise);
const batchQueue =
failedAttempts > 0 ? this._retryBatchQueue : this._batchQueue;
const bulkCommitBatch = this.getEligibleBatch(batchQueue);
try {
const operationResult = await operationFn(bulkCommitBatch);
this._successFn(documentRef, operationResult);
return operationResult;
} catch (error) {
const bulkWriterError = new BulkWriterError(
error.code,
error.message,
documentRef,
operationType,
failedAttempts
);
const shouldRetry = this._errorFn(bulkWriterError);
if (!shouldRetry) {
throw bulkWriterError;
}
} finally {
this._pendingOps.delete(operationCompletedDeferred.promise);
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man... I remember staring at my code trying to cut it down, but you've done it!

Had to make a couple of changes

  • move out operationCompletedDeferred to outside of the for-loop. The implementation here resolved the pendingOp after each retry loop.
  • move out verifyNotClosed() so an error is thrown instead of a rejected promise.
  • move sendReadyBatches() into the for loop since it needs to wait for getEligibleBatch() to create the new batch (for the first batch).
  • resolved the deferred promise at the end.

Comment thread dev/system-test/firestore.ts Outdated
Comment thread dev/system-test/firestore.ts Outdated
Comment thread types/firestore.d.ts Outdated
* @param data The object to serialize as the document.
* @returns A promise that resolves with the result
* of the write. Throws an error if the write fails.
* @returns {Promise<WriteResult>} A promise that resolves with the result
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to include the return type in TypeScript. You should keep the type of the rejected result, since that is not apparent here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Comment thread dev/src/bulk-writer.ts Outdated
Copy link
Copy Markdown
Contributor Author

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty good. I think we should verify some of the assumptions we made at API discussion, but we can probably take this back to the original PR

Comment thread dev/src/bulk-writer.ts Outdated
error.code !== undefined &&
retryCodes.includes(error.code) &&
error.failedAttempts < MAX_RETRY_ATTEMPTS;
logger(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As proposed in API meeting, we should move this logging to the callsite. This will also allow to us to log user-customized retries.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved.

Comment thread dev/src/bulk-writer.ts
Comment on lines +708 to +721
let batchQueue = this._batchQueue;
batchQueue.forEach(batch => batch.markReadyToSend());

// Send all scheduled operations on the BatchQueue first.
this.sendReadyBatches(batchQueue);
await Promise.all(this._pendingBatches);

// Afterwards, send all accumulated retry operations. Wait until the
// retryBatchQueue is cleared. This way, operations scheduled after
// flush() will not be sent until the retries are completed.
batchQueue = this._retryBatchQueue;
if (batchQueue.length > 0) {
this.sendReadyBatches(batchQueue);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this logic can be combined. The logic for the two different lists is essentially the same.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could combine it into a two-loop for-loop, but that doesn't feel as readable. Any suggestions?

Comment thread dev/src/bulk-writer.ts Outdated
// Always mark retry batches as READY_TO_SEND.
if (batchQueue === this._retryBatchQueue) {
batchQueue.forEach(batch => batch.markReadyToSend());
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems out of place here. Can we do this at the callsite?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two callsites, which is why i originally moved it into here. Moved it back out.

Comment thread dev/src/bulk-writer.ts
Comment on lines +919 to +922
} finally {
operationCompletedDeferred.resolve();
this._pendingOps.delete(operationCompletedDeferred.promise);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the double try/catch block is a bit harder to parse then just calling these two lines twice.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operationCompletedDeferred must resolve after the error is thrown, or else the flush and write promises resolve out of order. Given that, I don't see any good way to do so other than moving the outer try block into a separate function that encapsulates this one, or having duplicate code to handle this in each operation.

Comment thread dev/test/bulk-writer.ts Outdated
Comment on lines +389 to +392
process.on('unhandledRejection', () => {
errorThrown = true;
unhandledDeferred.resolve();
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's confirm with Yoshi team whether it would be ok to swallow these.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SG, started a conversation with them in the node room.

@schmidt-sebastian
Copy link
Copy Markdown
Contributor Author

Closing this as well are moving back to the original PR.

@thebrianchen thebrianchen deleted the bc/bulk-error-debug branch January 25, 2021 23:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

api: firestore Issues related to the googleapis/nodejs-firestore API. cla: yes This human has signed the Contributor License Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants