Repository navigation
[BIGTABLE] Insert callback called multiple times if err #1846
Description
Activity
- addedapi: bigtableIssues related to the Bigtable API.Issues related to the Bigtable API.
on Nov 30, 2016 - addedtype: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
on Nov 30, 2016 Thanks for reporting, we'll put out a patch soon.
Fix sent in #1847.
The fix turned out to be incorrect and the issue persists.
It looks like the gRPC stream emits both
metadataanderror, which triggers two event handlers that retry-request is listening for to decide if it should retry the request: https://github.com/stephenplusplus/retry-request/blob/85ee18dfc48e3bff174a4711440f55ed4748dc0f/index.js#L83-L84metadatais what we've been using to say "nothing obvious went wrong with the request", but it looks like we're going to need to beef up that logic, since an error can still emerge.A solution is to wait after receiving the metadata event before giving retry-request the all clear, but it would introduce an undesirable delay at the start of each request. That would look like:
var waitForErrorTimeout; var retryOpts = { retries: this.maxRetries, objectMode: objectMode, shouldRetryFn: GrpcService.shouldRetryRequest_, request: function() { return service[protoOpts.method](reqOpts, self.grpcMetadata, grpcOpts) .on('metadata', function() { // retry-request requires a server response before it starts emitting // data. The closest mechanism grpc provides is a metadata event, but // this does not provide any kind of response status. So we're faking // it here with code `0` which translates to HTTP 200. // // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1444#discussion_r71812636 var self = this; waitForErrorTimeout = setTimeout(function() { var grcpStatus = GrpcService.decorateStatus_({ code: 0 }); self.emit('response', grcpStatus); }, 1000); }); } }; return retryRequest(null, retryOpts) .on('error', function(err) { clearTimeout(waitForErrorTimeout); var grpcError = GrpcService.decorateError_(err); stream.destroy(grpcError || err); }) .pipe(stream);
@callmehiphop any other ideas on how we can handle this?
@stephenplusplus I'm not too sure, maybe some one on the gRPC side of things would have insight to help us.
/cc @murgatroid99
I think it's worth clarifying what those emitted events mean. The "metadata" event is emitted unconditionally at the beginning of any call that the server accepts and starts handling, whether or not the call eventually succeeds. "error" is emitted if the call fails in any way.
There is also the "status" event, which is emitted unconditionally when the call finishes, with a code indicating success or failure. You could do something like
call.on('status', function(status) { if (status.code == grpc.status.OK) { self.emit(response); } });
Receiving an OK status code should be mutually exclusive with seeing an "error" event.
@murgatroid99 thank you for the explanation. After digging in further, it looks like the gRPC stream will emit both an
errorand anendevent. While the stream is technically "ended" after an error occurs-- maybe more accurately, "closed"-- theendevent should not be emitted.require('fs') .createReadStream('non-existent-file') .on('error', function(err) { console.log('emitted') }) .on('end', function() { console.log('not emitted') }) .on('data', function() {}) // to drain the data require('request') .get('http://www.non-exitent-url.com') .on('error', function(err) { console.log('emitted') }) .on('end', function() { console.log('not emitted') }) .on('data', function() {}) // to drain the data
This is the root of our problem when trying to use
retry-request. It's expecting the stream to either emiterrororend, but not both. The stream will emitendfirst, thenerror.I haven't looked deeply into the implementation in gRPC, but making a generalization; if this is the process:
- allow the stream the user is holding to complete its lifecycle
- do some post-processing
- emit
status - if there was an error, emit
error
Consider using an intermediary stream, such as a
throughstream or a Transform stream, then registering aprefinishevent handler tocork()the stream while you do the post processing. If after determining there was no error, you can calluncork()on the through stream, which will allow the end event to fire.Posted to a new issue in the gRPC repo: grpc/grpc#8954
Going to close this and follow over on grpc/grpc#8954 for developments.
- added a commit that references this issue
on Mar 11, 2026
Environment details
Steps to reproduce
I use "insert" function to insert an array of 2 entries, for the example I force error with "wrong" timestamp (yes timestamp defined in milliseconds is invalid...)
In source file "table.js" I just add some logs to find source of error, callback is called multiple times, first time with end event, and last with error event
If I use promise syntax I just never see that an error occurred because promise is resolved with end event without error