Skip to content

Conversation

@schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Dec 15, 2020

This is my attempt to simplify some of the callback logic in BulkWriter, and will hopefully allow us add Backoff handling for retries pretty easily.

The overall changes are:

  • There are no more batch queues. Instead, there is only ever a single batch. If this batch reaches capacity, it is send.
  • To allow 'flush' to block, I am instead keeping a list of pending operations.
  • An operation can go from "sent" to "ready_to_send" if the retry mechanism kicks in.

One behavior change is that it is now possible that a retry will get scheduled after other operations in a flush. Let's say we have the calls:

  • Op1
  • Op2
  • Flush
  • Op3

And Op1 fails. The write order could now be: Op1, Op2, Op3, Op1. This breaks a single unit test, but I think it is sane behavior as it allows us to make progress while we wait for more reponses.

@schmidt-sebastian schmidt-sebastian requested review from a team as code owners December 15, 2020 03:32
@product-auto-label product-auto-label bot added the api: firestore Issues related to the googleapis/nodejs-firestore API. label Dec 15, 2020
@google-cla google-cla bot added the cla: yes This human has signed the Contributor License Agreement. label Dec 15, 2020
@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt/simplifybulkwriter branch from 28204df to 2900034 Compare December 15, 2020 16:55
@schmidt-sebastian schmidt-sebastian changed the title Mrschmidt/simplifybulkwriter chore: simplify Bulk Writer Dec 15, 2020
@codecov
Copy link

codecov bot commented Dec 15, 2020

Codecov Report

Merging #1379 (2853274) into master (7ff2345) will decrease coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1379      +/-   ##
==========================================
- Coverage   98.52%   98.51%   -0.01%     
==========================================
  Files          32       32              
  Lines       19464    19341     -123     
  Branches     1374     1357      -17     
==========================================
- Hits        19176    19053     -123     
  Misses        284      284              
  Partials        4        4              
Impacted Files Coverage Δ
dev/src/bulk-writer.ts 100.00% <100.00%> (+0.20%) ⬆️
dev/src/write-batch.ts 100.00% <100.00%> (ø)
dev/src/rate-limiter.ts 98.85% <0.00%> (-1.15%) ⬇️

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 7ff2345...2853274. Read the comment docs.

@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt/simplifybulkwriter branch from 2900034 to b480bf7 Compare December 15, 2020 17:08
@schmidt-sebastian
Copy link
Contributor Author

Updated PR to remove state transitions (they are now handled internally in BulkCommitOperation). Also removed list of pending operations and instead added a pointer to the last scheduled operation.

@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt/simplifybulkwriter branch from ca36340 to ab5c463 Compare December 17, 2020 01:02
Copy link

@thebrianchen thebrianchen left a comment

Choose a reason for hiding this comment

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

Wow, after taking the time to understand this, I am speechless. This is so sleek, so clean. Thanks for putting the time into this!

/**
* @param ref The document reference being written to.
* @param type The type of operation that created this write.
* @param op A closure that encapsulates the API call which adds this write to

Choose a reason for hiding this comment

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

lint fix

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

constructor(
public ref: firestore.DocumentReference<unknown>,
private type: 'create' | 'set' | 'update' | 'delete',
private op: (bulkCommitBatch: BulkCommitBatch) => void,

Choose a reason for hiding this comment

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

nit: add readonly where applicable here and below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (it's applicable everywhere)

* when the batch's response is received. This includes batches from both the
* batchQueue and retryBatchQueue.
* The batch that is currently used to schedule operations. Once this batch
* reached maximum capacity, a new batch is created.

Choose a reason for hiding this comment

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

nit: s/reached/reaches

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// TODO(b/158502664): Use actual delete timestamp.
const DELETE_TIMESTAMP_SENTINEL = Timestamp.fromMillis(0);

const status = (response.status || [])[i];

Choose a reason for hiding this comment

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

In L216, we access status.code, but based on L208, status could be undefined (though it shouldn't normally happen). Should we add an assert statement before accessing status.code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the code is undefined, we pass it as undefined to the customer. That seems to be the reasonable thing to do, since we don't want to make up our own status code. But as you said, the status code should always be set.

/**
* Sets the maximum number of allowed operations in a batch.
* Schedules the provided operations on current BulkCommitBatch.
* Sends the BulkCommitBatch if it reach maximum capacity.

Choose a reason for hiding this comment

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

s/reach/reaches

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

ref,
type,
op,
this._sendFn.bind(this),

Choose a reason for hiding this comment

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

Just curious, is there a behavioral difference between using bind here vs. using a lambda?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I was just hoping prettier wouldn't span this across 6 lines, but I was wrong. Now I am too lazy to go back.

* reference, or creates one if no eligible batches are found.
* Sends the current batch and resets `this._bulkCommitBatch`.
*
* @param flush - If provided, keeps re-sending operations until no more

Choose a reason for hiding this comment

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

nit: remove -.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return this._rateLimiter;
op.run(this._bulkCommitBatch);
this._bulkCommitBatch.processLastOperation(op);
this._lastOp = this._lastOp.then(() => silencePromise(op.promise));

Choose a reason for hiding this comment

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

Can you add a comment here about advancing the pointer? It took me a bit to realize what was happening

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

pendingBatch._opCount
);
logger(
'BulkWriter.sendNextBatch',

Choose a reason for hiding this comment

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

s/sendNextBatch/sendCurrentBatch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Thanks for catching.

Copy link
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 review!

/**
* @param ref The document reference being written to.
* @param type The type of operation that created this write.
* @param op A closure that encapsulates the API call which adds this write to
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

constructor(
public ref: firestore.DocumentReference<unknown>,
private type: 'create' | 'set' | 'update' | 'delete',
private op: (bulkCommitBatch: BulkCommitBatch) => void,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (it's applicable everywhere)

// TODO(b/158502664): Use actual delete timestamp.
const DELETE_TIMESTAMP_SENTINEL = Timestamp.fromMillis(0);

const status = (response.status || [])[i];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the code is undefined, we pass it as undefined to the customer. That seems to be the reasonable thing to do, since we don't want to make up our own status code. But as you said, the status code should always be set.

* when the batch's response is received. This includes batches from both the
* batchQueue and retryBatchQueue.
* The batch that is currently used to schedule operations. Once this batch
* reached maximum capacity, a new batch is created.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

* reference, or creates one if no eligible batches are found.
* Sends the current batch and resets `this._bulkCommitBatch`.
*
* @param flush - If provided, keeps re-sending operations until no more
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

pendingBatch._opCount
);
logger(
'BulkWriter.sendNextBatch',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Thanks for catching.

ref,
type,
op,
this._sendFn.bind(this),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I was just hoping prettier wouldn't span this across 6 lines, but I was wrong. Now I am too lazy to go back.

/**
* Sets the maximum number of allowed operations in a batch.
* Schedules the provided operations on current BulkCommitBatch.
* Sends the BulkCommitBatch if it reach maximum capacity.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return this._rateLimiter;
op.run(this._bulkCommitBatch);
this._bulkCommitBatch.processLastOperation(op);
this._lastOp = this._lastOp.then(() => silencePromise(op.promise));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@schmidt-sebastian schmidt-sebastian merged commit f4899ce into master Dec 18, 2020
@miketierce
Copy link

I can no longer create a new BulkWriter
const bulkWriter = new admin.firestore.BulkWriter()
presents this error
TypeError: Cannot read property '_incrementBulkWritersCount' of undefined

@schmidt-sebastian
Copy link
Contributor Author

@miketierce This PR hasn't been released yet. If the issue persists, can you file an issue in this repo?

cc @thebrianchen

gcf-merge-on-green bot pushed a commit to googleapis/java-firestore that referenced this pull request Feb 5, 2021
This is my attempt to simplify some of the callback logic in BulkWriter, and will hopefully allow us add Backoff handling for retries pretty easily.

The overall changes are:

- There are no more batch queues. Instead, there is only ever a single batch. If this batch reaches capacity, it is send.
- To allow 'flush' to block, I am instead keeping a list of pending operations.

One behavior change is that it is now possible that a retry will get scheduled after other operations in a flush. Let's say we have the calls:

- Op1
- Op2
- Flush
- Op3

And Op1 fails. The write order could now be: Op1, Op2, Op3, Op1. This breaks a single unit test, but I think it is sane behavior as it allows us to make progress while we wait for more reponses.

Port of googleapis/nodejs-firestore#1379
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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.

3 participants