Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api,shared-data): error codes in PE (#12936)
On the python side of our code, we want our new enumerated exceptions to be gradually integratable, and we also want to make sure that any errors that we didn't yet get the chance to give error codes end up with error codes. To do this in a programmatic way, we can add some automated methods for wrapping python exceptions. All enumerated errors now get to wrap errors. These are optional sequences of more enumerated errors that are considered to have caused the top-level one - in most cases, this will be because the enumerated error explicitly was instantiated to wrap a python exception, but it could also be if it was raised from one. Since we only wrap other enumerated errors, we need a way to make exceptions enumerated errors. A new exception type (but not code - it's just a GeneralError) called PythonException has this capability; it lets you give it BaseExceptions in addition to other EnumeratedErrors, and it's capable of walking the python memory model internals to try and get the other exceptions in a stack of raise from ... raise from ... calls that are reasonably popular in our code. This is functionality that is promoted out of The Dunder Zone in python 3.11, so I feel pretty good using it (this is what ExceptionGroups are). So now, as in the tests, if you catch an exception and give it to a PythonException you bless it with an error code and save all the exceptions and their stack traces for later inspection. Cool! ProtocolEngine is the first place we'll go through and add places that actually use these error codes, since it's in a lovely high-leverage middle spot in our stack. That means we both get to test the upward interface of how these things will be represented in the HTTP API and how they'll be created from lower exceptions. ProtocolEngine already has its own very large and robust set of custom exceptions, which is awesome. We can make them inherit from the enumerated errors pretty easily, but unfortunately we have to add a bunch of stuff to their constructors to pass along things like the ability to wrap other exceptions and so on. Luckily that's just typing. Once we've done that, at the three points we catch all missed exceptions we have to switch over to creating the new style. ProtocolEngineErrors get passed on; uncaught legacy errors get captured as PythonExceptions; and uncaught errors in the normal core do too. Finally, we have to represent this new style of error in the ErrorOccurrence objects. This is the fun part. Previously, we'd added error codes to those objects; this was sort of a big deal because we want them to be required when you make new ErrorOccurrences and when clients look, but we don't want things to break when we deserialize old ones. We can extend that trick pretty easily to add new things. What's not quite as easy is this concept of wrapping errors. Our errors are now essentially trees, and we need tree structure here. Luckily, jsonschema and pydantic are actually pretty good at type-recursive schema and object definitions, so we can plop a list of other error occurrences in there. Now, when we catch one of these errors that's bubbled up from hardware, we give it a name and we capture its entire history in an inspectable way, and I think that's really cool.
- Loading branch information