-
Notifications
You must be signed in to change notification settings - Fork 927
Differentiate between hardAsserts and debugAsserts #2859
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
Conversation
This PR aims to differentiate between asserts that we should handle in production builds (e.g. we received invalid data) and asserts we handle only during testing (our local state is corrupt). Some of the distinctions are really obvious, and other are anything but. In a lot of cases, we actually have multiple asserts for a single condition. I opted to only make the initial assertion a hardAssert (e.g. when we reveive mutation results, we assert multiple times that the number of results matches the mutation count). softAssert is a temporary name that I will rename back to assert once this is done. It is meant to make the decisions here more obvious
cff106e
to
accd742
Compare
Binary Size ReportAffected SDKs
Test Logs |
packages/firestore/src/core/query.ts
Outdated
@@ -449,7 +455,7 @@ export class Query { | |||
} | |||
|
|||
private assertValidBound(bound: Bound): void { | |||
assert( | |||
softAssert( |
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.
Have you confirmed elsewhere that these conditions are actually validated (i.e. that we reject user input that would have failed this assertion?
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.
I have not, but I would assume that our customers would let us know if that was the case.
@@ -72,7 +72,7 @@ export class SchemaConverter implements SimpleDbSchemaConverter { | |||
fromVersion: number, | |||
toVersion: number | |||
): PersistencePromise<void> { | |||
assert( | |||
softAssert( |
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.
The fromVersion
here comes from the IDBVersionChangeEvent
so it's not entirely under our control. Keep this one a hardAssert
?
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.
Done
@@ -302,7 +302,7 @@ export class MemoryMutationQueue implements MutationQueue { | |||
// Find the position of the first batch for removal. This need not be the | |||
// first entry in the queue. | |||
const batchIndex = this.indexOfExistingBatchId(batch.batchId, 'removed'); | |||
assert( | |||
softAssert( |
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.
I'm wary of this one because we don't actually use the batch index to remove the value below.
I'm on the fence as to whether or not to recommend switching back to hardAssert
here. The consequences seem dire (corrupting the mutation queue). On the other hand, this assertion really only mattered when we were transitioning away from held write acks.
Incidentally, we could remove the indexOfExistingBatchId
call too if we had a facility to mark arbitrary code as strippable. Something like:
if (debugBuild) {
const batchIndex = this.indexOfExistingBatchId(batch.batchId, 'removed');
softAssert(...);
}
In this particular case, maybe just inline the call?
Also, the comment above is out of date ("This need not be the first entry in the queue" is no longer true).
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.
I looked through the callsites. The batch ID here and in acknowledgeBadge is from the backend and no other validation is performed. As such, I made this a hardAssert
and changed acknowledgeBatch
to a hardAssert
as well.
@@ -114,7 +114,7 @@ export class Datastore { | |||
const result: MaybeDocument[] = []; | |||
keys.forEach(key => { | |||
const doc = docs.get(key); | |||
assert(!!doc, 'Missing entity in write response for ' + key); | |||
softAssert(!!doc, 'Missing entity in write response for ' + key); |
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.
This seems like a candidate for keeping as a hardAssert
because the docs come from the server response.
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.
Fixed
} | ||
|
||
/** | ||
* Fails if the given assertion condition is false, throwing an Error with the | ||
* given message if it did. | ||
* | ||
* This is stripped out in production builds. |
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.
Maybe clarify that the source is removed? Of particular importance is that any side effects of any code inline in the argument is removed--it's not just that the condition is ignored. Suggestion:
The code of callsites invoking this function are stripped out in production builds. Any side-effects of code within the
softAssert
invocation will not happen in this case.
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.
Done. Thanks for the suggestion.
if (!assertion) { | ||
fail(message); | ||
} | ||
export function softAssert( |
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.
While "soft" vs "hard" is a kind of natural pairing, softAssert
doesn't really suggest that it's going to be stripped out. What do you think of debugAssert
(and leave hardAssert
alone)? FWIW The Firebase C++ SDK calls these "dev assertions".
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.
I wanted to rename these back to "assert" before merging, but debugAssert
is more meaningful. I will rename in a separate commit on this PR.
@@ -53,7 +53,7 @@ import { | |||
WatchTargetChange, | |||
WatchTargetChangeState | |||
} from '../../../src/remote/watch_change'; | |||
import { assert } from '../../../src/util/assert'; | |||
import { softAssert } from '../../../src/util/assert'; |
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 to me like tests should always use hardAssert
so that the condition is always evaluated, even when running against a stripped build.
This is obviously unimportant in unit tests, but it seems like it could have bearing on integration tests, and it seems like being able to make a strong blanket statement like "tests should use hardAssert
seems like it could help keep things simple.
WDYT?
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.
The tests themselves are actually never minified. We run the full tests against the minified build, so even in this mode the "softAsserts" are kept. I think the new name "debugAsserts" conveys that they are run in tests better than "softAsserts" and almost as well as "hardAsserts", so I kept it as for now. I can go either way here...
FWIW, rather than changing these to hardAsserts, we might even want to use failed test expectations.
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.
Makes sense.
Changing to failed test expectations would be fine too, but seems orthogonal to the issue at hand and seems like it could be deferred.
@@ -53,7 +53,7 @@ import { | |||
WatchTargetChange, | |||
WatchTargetChangeState | |||
} from '../../../src/remote/watch_change'; | |||
import { assert } from '../../../src/util/assert'; | |||
import { softAssert } from '../../../src/util/assert'; |
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.
Makes sense.
Changing to failed test expectations would be fine too, but seems orthogonal to the issue at hand and seems like it could be deferred.
/** | ||
* Unconditionally fails, throwing an Error with the given message. | ||
* | ||
* The code of callsites invoking this function are stripped out in production |
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.
Apologies--I totally missed that you're stripping out calls to fail()
as well. There are a few cases where data outside our control results in a failure that would be unhandled. File and line numbers taken from 761d0d3.
- in database.ts:676 (
Transattion.get
):if (!docs || docs.length !== 1)
-- this seems like it could straightforwardly be rewritten as ahardAssert
- in user_data_reader:356 (
parseMergeData
): it seems like this may be validating user input? Possibly this should be converted to a validation error (or possibly this was validated elsewhere) - in query.ts:494 (
Operator.fromString
): the default case seems like it might be validating user input. Oh wait, this is validated at database.ts:1459. This one's OK, I think. - in encoded_resource_path.ts:142, 170 (
decodeResourcePath
): this is decoding a resource path from IndexedDB - in indexeddb_mutation_queue.ts:348 (
getAllMutationBatchesAffectingDocumentKey
): dangling document-references could reflect corruption in IndexedDB - Same on line 480 (
lookupMutationBatches
) - local_serializer.ts:67: (
fromDbRemoteDocument
): borderline case, but yourfromFoo
inserializer.ts
seemed mostly to go towardhardAssert
so I thought I'd flag - values.ts:380: (
canonifyValue
): This is how we process input from the network, so it's possible to get unexpected values here. The otherfail
calls in this file are all default cases forTypeOrder
so I think they're fine. - webchannel_connection.ts:170: RPC error code has a remote source
Actually I'm going to stop--there seem like more and we should at least discuss this. A hardFail
/debugFail
split would definitely address this (or just leaving all fail
calls in too, though that cuts into size savings).
Many of these seem like they could be reformulated in terms of the error handling that's in place, but that seems out of scope for this change.
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.
I reverted back the documentation change for fail
. Let's tackle this one API at a time. If we want to keep fail() but reduce the size impact we can remove the error message, but first I would like to look at the actual size impact of the messages.
So, for now, this PR differentiates between the different assertions, and then I will update the Transformer PR to not stip fail()
.
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.
LGTM
Please update description (softAsserts => debugAsserts).
This PR aims to differentiate between asserts that we should handle in production builds (e.g. we received invalid data) and asserts we handle only during testing (our local state is corrupt). Some of the distinctions are really obvious, and other are anything but. In a lot of cases, we actually have multiple asserts for a single condition. I opted to only make the initial assertion a hardAssert (e.g. when we receive mutation results, we assert multiple times that the number of results matches the mutation count).
Asserts based on User Input are treated as soft asserts as we only use them after validating the input.
softAssert is a temporary name that I will rename back to assert once this is done. It is meant to make the decisions here more obvious.