Skip to content

Commit 2d8de32

Browse files
danieljbruceigorbernstein2leahecole
authored
fix: ensure that during resumption of a scan, rows that have not been observed by the caller are re-requested (#1444)
* fix: dropping buffered rows during a retry of a scan createReadStream() creates a pipeline of streams that converts a stream of row chunks into a stream of logical rows. It also has logic to handle stream resumption when a single attempt fails. The pipeline can be split into 2 parts: the persistent operation stream that the caller sees and the transient per attempt segment. When a retry attempt occurs, the per attempt segment is unpiped from the operation stream and is discarded. Currently this includes any buffered data that each stream might contain. Unfortunately, when constructing the retry request, createReadStream() will use the last row key from the last buffered row. This will cause the buffered rows to be omitted from the operation stream. This PR fixes the missing rows part by only referencing the row keys that were seen by the persistent operation stream when constructing a retry attempt. In other words, this will ensure that we only update the lastSeenRow key once the row has been "committed" to the persistent portion of the pipeline * Add a test that sends rows back # Conflicts: # system-test/read-rows.ts * Create an instance with dummy server. # Conflicts: # system-test/read-rows.ts * Add test parameters for sending back the right chu * Omit server start * Run a test against the server in the old code * Add logging to the chunk transformer * Add logging to indicate that the server received r * chunk transformer logs and other logs * Add a log * Don’t change the old server * Add another guard against the logs * Add setImmediate everywhere that it needs to be * Remove the logging * Remove more logging * Adjust header * Add the high watermarks back in * Remove the at accessor * Eliminate the watermark adjustments * Introduce the watermarks back in * Reduce the number of watermark removals to 1. * Reverted the streamEvents pipeline * Add some comments for introducing the new waterma * Remove comments and console logs. Add TODO. * Add TODO * refactor the test with a helper method * Fix linting issue * Adding a comment about the mock * readable comment change * Update test/readrows.ts Co-authored-by: Leah E. Cole <[email protected]> * It uses 150 rows not 1000 rows * Add a TODO for making more specific typing * Add some TODOs for better factoring * Add interface: server writable stream Use it to replace any --------- Co-authored-by: Igor Berntein <[email protected]> Co-authored-by: Leah E. Cole <[email protected]>
1 parent c86d456 commit 2d8de32

File tree

5 files changed

+418
-15
lines changed

5 files changed

+418
-15
lines changed

src/table.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`);
726726
let filter: {} | null;
727727
const rowsLimit = options.limit || 0;
728728
const hasLimit = rowsLimit !== 0;
729-
let rowsRead = 0;
729+
730730
let numConsecutiveErrors = 0;
731731
let numRequestsMade = 0;
732732
let retryTimer: NodeJS.Timeout | null;
@@ -749,14 +749,22 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`);
749749
let rowStream: Duplex;
750750

751751
let userCanceled = false;
752+
// The key of the last row that was emitted by the per attempt pipeline
753+
// Note: this must be updated from the operation level userStream to avoid referencing buffered rows that will be
754+
// discarded in the per attempt subpipeline (rowStream)
755+
let lastRowKey = '';
756+
let rowsRead = 0;
752757
const userStream = new PassThrough({
753758
objectMode: true,
754-
readableHighWaterMark: 0,
759+
readableHighWaterMark: 0, // We need to disable readside buffering to allow for acceptable behavior when the end user cancels the stream early.
760+
writableHighWaterMark: 0, // We need to disable writeside buffering because in nodejs 14 the call to _transform happens after write buffering. This creates problems for tracking the last seen row key.
755761
transform(row, _encoding, callback) {
756762
if (userCanceled) {
757763
callback();
758764
return;
759765
}
766+
lastRowKey = row.id;
767+
rowsRead++;
760768
callback(null, row);
761769
},
762770
});
@@ -796,7 +804,6 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`);
796804
// cancelled the stream in the middle of a retry
797805
retryTimer = null;
798806

799-
const lastRowKey = chunkTransformer ? chunkTransformer.lastRowKey : '';
800807
// eslint-disable-next-line @typescript-eslint/no-explicit-any
801808
chunkTransformer = new ChunkTransformer({decode: options.decode} as any);
802809

@@ -918,7 +925,6 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`);
918925
) {
919926
return next();
920927
}
921-
rowsRead++;
922928
const row = this.row(rowData.key);
923929
row.data = rowData.data;
924930
next(null, row);

src/util/mock-servers/mock-server.ts

-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export class MockServer {
3737
`localhost:${this.port}`,
3838
grpc.ServerCredentials.createInsecure(),
3939
() => {
40-
server.start();
4140
callback ? callback(portString) : undefined;
4241
}
4342
);

test/readrows.ts

+54-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
import {before, describe, it} from 'mocha';
16-
import {Bigtable, Row, Table} from '../src';
16+
import {Bigtable, protos, Row, Table} from '../src';
1717
import * as assert from 'assert';
1818
import {Transform, PassThrough, pipeline} from 'stream';
1919

@@ -22,7 +22,18 @@ import {MockServer} from '../src/util/mock-servers/mock-server';
2222
import {BigtableClientMockService} from '../src/util/mock-servers/service-implementations/bigtable-client-mock-service';
2323
import {MockService} from '../src/util/mock-servers/mock-service';
2424
import {debugLog, readRowsImpl} from './utils/readRowsImpl';
25-
import {UntypedHandleCall} from '@grpc/grpc-js';
25+
import {ServerWritableStream, UntypedHandleCall} from '@grpc/grpc-js';
26+
import {readRowsImpl2} from './utils/readRowsImpl2';
27+
28+
type PromiseVoid = Promise<void>;
29+
interface ServerImplementationInterface {
30+
(
31+
server: ServerWritableStream<
32+
protos.google.bigtable.v2.IReadRowsRequest,
33+
protos.google.bigtable.v2.IReadRowsResponse
34+
>
35+
): PromiseVoid;
36+
}
2637

2738
describe('Bigtable/ReadRows', () => {
2839
let server: MockServer;
@@ -317,6 +328,47 @@ describe('Bigtable/ReadRows', () => {
317328
});
318329
});
319330

331+
it('should return row data in the right order', done => {
332+
// 150 rows must be enough to reproduce issues with losing the data and to create backpressure
333+
const keyFrom = undefined;
334+
const keyTo = undefined;
335+
// the server will error after sending this chunk (not row)
336+
const errorAfterChunkNo = 100;
337+
const dataResults = [];
338+
339+
// TODO: Do not use `any` here, make it a more specific type and address downstream implications on the mock server.
340+
service.setService({
341+
ReadRows: readRowsImpl2(
342+
keyFrom,
343+
keyTo,
344+
errorAfterChunkNo
345+
) as ServerImplementationInterface,
346+
});
347+
const sleep = (ms: number) => {
348+
return new Promise(resolve => setTimeout(resolve, ms));
349+
};
350+
(async () => {
351+
try {
352+
const stream = table.createReadStream({
353+
start: '00000000',
354+
end: '00000150',
355+
});
356+
357+
for await (const row of stream) {
358+
dataResults.push(row.id);
359+
await sleep(50);
360+
}
361+
const expectedResults = Array.from(Array(150).keys())
362+
.map(i => '00000000' + i.toString())
363+
.map(i => i.slice(-8));
364+
assert.deepStrictEqual(dataResults, expectedResults);
365+
done();
366+
} catch (error) {
367+
done(error);
368+
}
369+
})();
370+
});
371+
320372
after(async () => {
321373
server.shutdown(() => {});
322374
});

test/table.ts

+22-8
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,20 @@ describe('Bigtable/Table', () => {
11181118
let reqOptsCalls: any[];
11191119
let setTimeoutSpy: sinon.SinonSpy;
11201120

1121+
/*
1122+
setImmediate is required here to correctly mock events as they will
1123+
come in from the request function. It is required for tests to pass,
1124+
but it is not a problem that it is required because we never expect
1125+
a single Node event to emit data and then emit an error. That is,
1126+
a mock without setImmediate around the last error represents a scenario
1127+
that will never happen.
1128+
*/
1129+
function emitRetriableError(stream: Duplex) {
1130+
setImmediate(() => {
1131+
stream.emit('error', makeRetryableError());
1132+
});
1133+
}
1134+
11211135
beforeEach(() => {
11221136
FakeChunkTransformer.prototype._transform = function (
11231137
rows: Row[],
@@ -1262,7 +1276,7 @@ describe('Bigtable/Table', () => {
12621276
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12631277
((stream: any) => {
12641278
stream.push([{key: 'a'}]);
1265-
stream.emit('error', makeRetryableError());
1279+
emitRetriableError(stream);
12661280
}) as {} as EventEmitter,
12671281
((stream: Writable) => {
12681282
stream.end();
@@ -1285,7 +1299,7 @@ describe('Bigtable/Table', () => {
12851299
emitters = [
12861300
((stream: Duplex) => {
12871301
stream.push([{key: 'a'}]);
1288-
stream.emit('error', makeRetryableError());
1302+
emitRetriableError(stream);
12891303
}) as {} as EventEmitter,
12901304
((stream: Writable) => {
12911305
stream.end();
@@ -1310,7 +1324,7 @@ describe('Bigtable/Table', () => {
13101324
((stream: Duplex) => {
13111325
stream.push([{key: 'a'}]);
13121326
stream.push([{key: 'b'}]);
1313-
stream.emit('error', makeRetryableError());
1327+
emitRetriableError(stream);
13141328
}) as {} as EventEmitter,
13151329
((stream: Duplex) => {
13161330
stream.push([{key: 'c'}]);
@@ -1343,7 +1357,7 @@ describe('Bigtable/Table', () => {
13431357
emitters = [
13441358
((stream: Duplex) => {
13451359
stream.push([{key: 'a'}]);
1346-
stream.emit('error', makeRetryableError());
1360+
emitRetriableError(stream);
13471361
}) as {} as EventEmitter,
13481362
((stream: Duplex) => {
13491363
stream.end([{key: 'c'}]);
@@ -1362,7 +1376,7 @@ describe('Bigtable/Table', () => {
13621376
((stream: Duplex) => {
13631377
stream.push([{key: 'a'}]);
13641378
stream.push([{key: 'b'}]);
1365-
stream.emit('error', makeRetryableError());
1379+
emitRetriableError(stream);
13661380
}) as {} as EventEmitter,
13671381
];
13681382

@@ -1381,7 +1395,7 @@ describe('Bigtable/Table', () => {
13811395
emitters = [
13821396
((stream: Duplex) => {
13831397
stream.push([{key: 'a'}]);
1384-
stream.emit('error', makeRetryableError());
1398+
emitRetriableError(stream);
13851399
}) as {} as EventEmitter,
13861400
];
13871401

@@ -1395,7 +1409,7 @@ describe('Bigtable/Table', () => {
13951409
emitters = [
13961410
((stream: Duplex) => {
13971411
stream.push([{key: 'c'}]);
1398-
stream.emit('error', makeRetryableError());
1412+
emitRetriableError(stream);
13991413
}) as {} as EventEmitter,
14001414
];
14011415

@@ -1418,7 +1432,7 @@ describe('Bigtable/Table', () => {
14181432
((stream: Duplex) => {
14191433
stream.push([{key: 'a1'}]);
14201434
stream.push([{key: 'd'}]);
1421-
stream.emit('error', makeRetryableError());
1435+
emitRetriableError(stream);
14221436
}) as {} as EventEmitter,
14231437
];
14241438

0 commit comments

Comments
 (0)