-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: properly handle asynchronous read from stream #1284
Changes from 9 commits
318aeff
2c229de
1924249
99813aa
3353666
3f2385b
bf950d0
fbd43a0
aaff430
1e52970
b02c587
d944d58
69077d9
56d4364
71b67a1
4099b2b
e54f607
63453b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,6 +80,7 @@ export enum RowStateEnum { | |
export class ChunkTransformer extends Transform { | ||
options: TransformOptions; | ||
_destroyed: boolean; | ||
_userCanceled: boolean; | ||
lastRowKey?: Value; | ||
state?: number; | ||
row?: Row; | ||
|
@@ -91,10 +92,15 @@ export class ChunkTransformer extends Transform { | |
super(options); | ||
this.options = options; | ||
this._destroyed = false; | ||
this._userCanceled = false; | ||
this.lastRowKey = undefined; | ||
this.reset(); | ||
} | ||
|
||
get canceled() { | ||
return this._userCanceled; | ||
} | ||
|
||
/** | ||
* called at end of the stream. | ||
* @public | ||
|
@@ -129,10 +135,10 @@ export class ChunkTransformer extends Transform { | |
* @public | ||
* | ||
* @param {object} data readrows response containing array of chunks. | ||
* @param {object} [enc] encoding options. | ||
* @param {object} [_encoding] encoding options. | ||
* @param {callback} next callback will be called once data is processed, with error if any error in processing | ||
*/ | ||
_transform(data: Data, enc: string, next: Function): void { | ||
_transform(data: Data, _encoding: string, next: Function): void { | ||
for (const chunk of data.chunks!) { | ||
switch (this.state) { | ||
case RowStateEnum.NEW_ROW: | ||
|
@@ -147,17 +153,6 @@ export class ChunkTransformer extends Transform { | |
default: | ||
break; | ||
} | ||
if (this._destroyed) { | ||
return; | ||
} | ||
} | ||
if (data.lastScannedRowKey && data.lastScannedRowKey.length > 0) { | ||
this.lastRowKey = Mutation.convertFromBytes( | ||
data.lastScannedRowKey as Bytes, | ||
{ | ||
userOptions: this.options, | ||
} | ||
); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this breaks request resumption logic in makeNewRequest
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is my fault, I forgot to mention one other detail of ReadRows protocol, there are 2 ways that a readrows resumption request can be built:
This is not currently enable on the serverside, but is specified by the protocol There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed this |
||
next(); | ||
} | ||
|
@@ -226,7 +221,14 @@ export class ChunkTransformer extends Transform { | |
chunk.familyName || | ||
chunk.qualifier || | ||
(chunk.value && chunk.value.length !== 0) || | ||
chunk.timestampMicros! > 0; | ||
// if it's a number | ||
(typeof chunk.timestampMicros === 'number' && | ||
chunk.timestampMicros! > 0) || | ||
// if it's an instance of Long | ||
(typeof chunk.timestampMicros === 'object' && | ||
'compare' in chunk.timestampMicros && | ||
typeof chunk.timestampMicros.compare === 'function' && | ||
chunk.timestampMicros.compare(0) === 1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit strange, chunk is a protobuf, so shouldnt it have a stable type? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the specifics of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please leave a comment in the code explaining this |
||
if (chunk.resetRow && containsData) { | ||
this.destroy( | ||
new TransformError({ | ||
|
@@ -452,4 +454,8 @@ export class ChunkTransformer extends Transform { | |
} | ||
this.moveToNextState(chunk); | ||
} | ||
|
||
cancel(): void { | ||
this._userCanceled = true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -745,10 +745,16 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`); | |
filter = Filter.parse(options.filter); | ||
} | ||
|
||
let chunkTransformer: ChunkTransformer; | ||
let rowStream: Duplex; | ||
|
||
const userStream = new PassThrough({objectMode: true}); | ||
const end = userStream.end.bind(userStream); | ||
userStream.end = () => { | ||
rowStream?.unpipe(userStream); | ||
if (chunkTransformer) { | ||
chunkTransformer.cancel(); | ||
} | ||
if (activeRequestStream) { | ||
activeRequestStream.abort(); | ||
} | ||
|
@@ -758,9 +764,6 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`); | |
return end(); | ||
}; | ||
|
||
let chunkTransformer: ChunkTransformer; | ||
let rowStream: Duplex; | ||
|
||
const makeNewRequest = () => { | ||
// Avoid cancelling an expired timer if user | ||
// cancelled the stream in the middle of a retry | ||
|
@@ -882,7 +885,7 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`); | |
const toRowStream = new Transform({ | ||
transform: (rowData, _, next) => { | ||
if ( | ||
chunkTransformer._destroyed || | ||
chunkTransformer.canceled || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like there are only 2 things that care about the cancelled flag:
ChunkTransformer seems like an innocent bystander here. Why not move the userCancelled flag as a local var in createReadStream(), whose scope is shared by the 2 closure that care about the flag? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively, maybe create a subclass for userStream that holds the state There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moving the flag closer to the consumer makes sense to me, I'll try to do it. I only put it here to replace the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(userStream as any)._writableState.ended | ||
) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we expose ChunkTransformer as part of our public api. Which I think is incorrect to begin with. To minimize further leakage of implementation details, can we mark this method as internal?