Repository navigation
test(bigtable): stabilize flaky backup and client-side metrics tests - #9761
Conversation
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a retry mechanism (withBackupQuotaRetry) and robust resource cleanup in system tests to handle backup quota limitations, alongside adjustments to metric interval validation in client-side metrics tests. Feedback on these changes highlights three key areas: first, reverting the interval check in the production exporter leaves it vulnerable to export failures, so the strictly-greater-than adjustment should be implemented there rather than only in tests; second, the retry helper should validate that the caught error is a truthy object to prevent potential TypeError crashes; and third, manually constructing the table instance in the restore test may be masking a production bug in backup.restore() where the returned table is not correctly associated with the target instance.
| return { | ||
| endTime: { | ||
| seconds: endSec, | ||
| nanos: endNanos, | ||
| seconds: dataPoint.endTime[0], | ||
| nanos: dataPoint.endTime[1], | ||
| }, | ||
| startTime: { | ||
| seconds: startSec, | ||
| nanos: startNanos, | ||
| seconds: dataPoint.startTime[0], | ||
| nanos: dataPoint.startTime[1], | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Reverting the interval check entirely from the production exporter leaves production environments vulnerable to metric export failures if startTime >= endTime occurs (e.g., due to clock drift, container virtualization, or high-resolution timer limitations). Since Cloud Monitoring requires startTime < endTime for cumulative/delta metrics, the previous fix of setting them equal was indeed insufficient, but the correct solution is to apply the strictly-greater-than adjustment (adding 1ms/1,000,000 nanos) in the production exporter itself, rather than only in the system tests.
let endSec = dataPoint.endTime[0];
let endNanos = dataPoint.endTime[1];
const startSec = dataPoint.startTime[0];
const startNanos = dataPoint.startTime[1];
if (startSec > endSec || (startSec === endSec && startNanos >= endNanos)) {
const adjustedNanos = startNanos + 1000000;
if (adjustedNanos >= 1000000000) {
endSec = startSec + 1;
endNanos = adjustedNanos - 1000000000;
} else {
endSec = startSec;
endNanos = adjustedNanos;
}
}
return {
endTime: {
seconds: endSec,
nanos: endNanos,
},
startTime: {
seconds: startSec,
nanos: startNanos,
},
};There was a problem hiding this comment.
I don't think I agree with this. https://github.com/googleapis/google-cloud-node/pull/9717/changes#r4401545583 offers this solution, but failed exporter calls are silent so it doesn't bombard the user with errors and we don't want hidden code somewhere artificially manipulating the data.
| const serviceErr = err as Partial<ServiceError>; | ||
| const isQuotaError = | ||
| serviceErr.code === 8 || | ||
| /RESOURCE_EXHAUSTED|Quota exceeded/i.test(serviceErr.message ?? ''); |
There was a problem hiding this comment.
If the caught error err is null or not an object, casting it directly and accessing serviceErr.code or serviceErr.message will throw a TypeError, crashing the test runner. To ensure robust error handling and prevent TypeErrors, validate that err is truthy and is an object upfront before performing property checks.
| const serviceErr = err as Partial<ServiceError>; | |
| const isQuotaError = | |
| serviceErr.code === 8 || | |
| /RESOURCE_EXHAUSTED|Quota exceeded/i.test(serviceErr.message ?? ''); | |
| if (!err || typeof err !== 'object') { | |
| return; | |
| } | |
| const serviceErr = err as Partial<ServiceError>; | |
| const isQuotaError = | |
| serviceErr.code === 8 || | |
| /RESOURCE_EXHAUSTED|Quota exceeded/i.test(serviceErr.message ?? ''); |
References
- Always validate and reject missing or falsy arguments upfront before performing property checks, to prevent TypeErrors (such as when 'typeof null' evaluates to 'object').
| const [tExists] = await DIFF_INSTANCE.table( | ||
| restoreTableIdFromCluster, | ||
| ).exists(); |
There was a problem hiding this comment.
If table.exists() was failing or checking the wrong instance, it suggests that backup.restore() might have a bug where the returned Table instance is associated with the source instance instead of the target DIFF_INSTANCE passed in the options. Working around this in the test by manually constructing the table via DIFF_INSTANCE.table(...) hides this potential production bug. Please verify if backup.restore() correctly associates the returned Table with the target instance when options.instance is provided, and if not, fix it in the production implementation of backup.restore().
There was a problem hiding this comment.
Code Review
This pull request simplifies metric interval retrieval, introduces a retry mechanism (withBackupQuotaRetry) to handle quota exhaustion errors during backup operations in system tests, and ensures proper cleanup of test resources. It also adds metric interval validation to client-side metrics tests and updates unit tests to use sandboxed timers. Review feedback suggests adding a .js extension to a new import for ESM compatibility, adding a safety guard to prevent potential TypeErrors when checking service errors, and removing a redundant 60-second delay in the test setup to reduce overall test execution time.
| import * as proxyquire from 'proxyquire'; | ||
| import * as sinon from 'sinon'; | ||
| import {Mutation} from '../src/mutation.js'; | ||
| import '../src/table'; |
| const serviceErr = err as Partial<ServiceError>; | ||
| const isQuotaError = | ||
| serviceErr.code === 8 || | ||
| /RESOURCE_EXHAUSTED|Quota exceeded/i.test(serviceErr.message ?? ''); |
There was a problem hiding this comment.
To prevent potential runtime TypeErrors during error handling in tests, add a safety guard to ensure serviceErr is not null or undefined before accessing its properties.
const serviceErr = err as Partial<ServiceError> | null | undefined;
const isQuotaError =
serviceErr &&
(serviceErr.code === 8 ||
/RESOURCE_EXHAUSTED|Quota exceeded/i.test(serviceErr.message ?? ''));References
- Always validate and reject missing or falsy arguments upfront before performing property checks, to prevent TypeErrors (such as when 'typeof null' evaluates to 'object').
| await new Promise(resolve => | ||
| setTimeout(resolve, BACKUP_QUOTA_COOLDOWN_MS), | ||
| ); |
There was a problem hiding this comment.
Since withBackupQuotaRetry is already implemented to dynamically handle quota errors with a 60-second cooldown, this unconditional 60-second delay in the before block (combined with the existing 60-second delay in beforeEach) is redundant. It unnecessarily adds 2-3 minutes of idle wait time to the test suite. Consider removing this unconditional delay and relying on the dynamic retry helper.
| endSec = startSec; | ||
| endNanos = startNanos; | ||
| } | ||
|
|
There was a problem hiding this comment.
Note: The changes here are actually just reverting changes that were applied in 1ea27c9#diff-39c2f3c691ce7c9e2f54ab8c9020dff4aef632d80a11aa13fe19074c889aaad5R51-R60. We do not want the client side metrics exporter artificially warping collected client side metrics data which is what this commit did.
If we can change the tests instead then that would offer a much better solution.
There was a problem hiding this comment.
And this PR attempts to change the tests instead to solve the Flakey test problem.
|
Summary of the errors observed in
|
Description
Stabilizes flaky Bigtable tests and reverts exporter interval check from 1ea27c9.
Impact
Eliminates flaky Bigtable unit and system test failures without modifying production logic.
Testing
GIT_DIFF_ARG="upstream/main...HEAD -- :!packages" node ./bin/linter.mjs --strictpasses with zero errors.handwritten/bigtableunit tests pass across 5 runs on both Node.js and Bun (>= 95%confidence) as well asci/run_conditional_tests.sh --strict(875 passing).