Skip to content
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

feat(api,shared-data): error codes in PE #12936

Merged
merged 5 commits into from
Jun 21, 2023
Merged

feat(api,shared-data): error codes in PE #12936

merged 5 commits into from
Jun 21, 2023

Conversation

sfoster1
Copy link
Member

@sfoster1 sfoster1 commented Jun 16, 2023

Draft alert!

This is a draft because I definitely need to get some fixes and further tests in the HTTP API layer and test on a robot and so on and so forth, but I wanted people to be able to see it since this does some medium-dubious jsonschema stuff and pokes into some core PE behavior.

Overview

This PR adds our new enumerated error codes to ProtocolEngine. This is the first place we're using them. We add the error codes to the protocol engine internal custom error hierarchies and add logic to capture python exceptions that are not EnumeratedErrors at the three places that we catch exceptions - the core engine action handler, the legacy executor, and the main executor.

The biggest new functionality is error wrapping. The EnumeratedErrors can now catch, wrap, and store sequences of raise from exception contexts that could previously only really be represented as tracebacks. Now the structure of exception raise sequences is preserved all the way up to the ErrorOccurrence model and thus to the client to format as they see fit. While some detail isn't really schema controlled (you'll have a Dict[str, str] and you'll like it!) the structure actually is, as it's just a tree and pydantic and jsonschema do a pretty good job of supporting type recursion in models.

I'm interested to see early reactions from @sanni-t @jbleon95 @TamarZanzouri and @SyntaxColoring ; I think I'm happy with how this turned out, but further testing will tell.

Scope and Followups

This PR is intended to add the enumerated error code handling and transformations to PD and correctly send them over HTTP. There's a followup PR needed to scrape through PE and make sure that everything has a correct error code, and that specific hardware calls that might fail in specific ways and already have specific try/except clauses correctly wrap things in the right error codes and so on. There's also one needed to do that same thing in the other parts of the api subdirectory, particularly hardware_control, and hardware; and finally, something similar's going to need to happen in the robot-server, along with some kind of top-level exception catching there.

Risks

Well, this modifies serialized data, so a lot, but hopefully in as safe a way as we can do it. It shouldn't introduce new errors, just transform how they're handled.

Testing

  • Put it on a robot and cause a bunch of protocol errors!

sfoster1 added 3 commits June 16, 2023 16:11
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!
This is UNEXPECTED_TIP_ATTACH and occurs when there shouldn't be a tip
but there is.
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.
@codecov
Copy link

codecov bot commented Jun 16, 2023

Codecov Report

Merging #12936 (ee9f055) into edge (b4cdcef) will decrease coverage by 0.17%.
The diff coverage is 58.20%.

Impacted file tree graph

@@            Coverage Diff             @@
##             edge   #12936      +/-   ##
==========================================
- Coverage   73.03%   72.87%   -0.17%     
==========================================
  Files        1522     2349     +827     
  Lines       49905    65003   +15098     
  Branches     3040     7363    +4323     
==========================================
+ Hits        36449    47372   +10923     
- Misses      12990    15931    +2941     
- Partials      466     1700    +1234     
Flag Coverage Δ
app 71.72% <ø> (+27.61%) ⬆️
g-code-testing 96.44% <ø> (ø)
notify-server 89.13% <ø> (ø)
protocol-designer 44.91% <ø> (-1.45%) ⬇️
shared-data 76.63% <58.20%> (+0.99%) ⬆️
step-generation 85.53% <ø> (-3.12%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...rc/opentrons/protocol_engine/clients/transports.py 92.85% <ø> (ø)
...entrons/protocol_engine/errors/error_occurrence.py 100.00% <ø> (ø)
...src/opentrons/protocol_engine/errors/exceptions.py 100.00% <ø> (ø)
...rons/protocol_engine/execution/command_executor.py 100.00% <ø> (ø)
...pi/src/opentrons/protocol_engine/state/commands.py 99.46% <ø> (-0.02%) ⬇️
...opentrons/protocol_runner/legacy_command_mapper.py 98.33% <ø> (ø)
api/src/opentrons/protocols/execution/errors.py 100.00% <ø> (ø)
.../python/opentrons_shared_data/errors/exceptions.py 71.87% <56.92%> (+11.40%) ⬆️
...ta/python/opentrons_shared_data/errors/__init__.py 100.00% <100.00%> (ø)
...-data/python/opentrons_shared_data/errors/codes.py 90.19% <100.00%> (+0.19%) ⬆️

... and 863 files with indirect coverage changes

@sfoster1 sfoster1 marked this pull request as draft June 16, 2023 20:29
@sfoster1 sfoster1 requested a review from fsinapi June 16, 2023 20:29
Copy link
Contributor

@fsinapi fsinapi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. I like the wrapping a lot.


base_details = {
k: str(v)
for k, v in inspect.getmembers(exc, _exc_harvest_predicate)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is neat

@sfoster1 sfoster1 marked this pull request as ready for review June 20, 2023 18:09
Copy link
Contributor

@TamarZanzouri TamarZanzouri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very elegant! I love it!

@sfoster1
Copy link
Member Author

{
    "data": [
        {
            "id": "e6875d13-6411-4233-815e-fbde1645f494",
            "status": "completed",
            "result": "not-ok",
            "pipettes": [
                {
                    "id": "43aa69ef-9fcb-4a8d-a605-eb00b46ea711",
                    "pipetteName": "p50_multi_gen3",
                    "mount": "right"
                }
            ],
            "labware": [
                {
                    "id": "fixedTrash",
                    "loadName": "opentrons_1_trash_3200ml_fixed",
                    "definitionUri": "opentrons/opentrons_1_trash_3200ml_fixed/1",
                    "location": {
                        "slotName": "A3"
                    }
                },
                {
                    "id": "052df6d7-e106-4c3c-b008-4d53afc7349e",
                    "loadName": "opentrons_ot3_96_tiprack_50ul",
                    "definitionUri": "opentrons/opentrons_ot3_96_tiprack_50ul/1",
                    "location": {
                        "slotName": "C1"
                    }
                },
                {
                    "id": "c4d562e7-6dce-492b-9c1d-7bde52e9f7c7",
                    "loadName": "nest_96_wellplate_100ul_pcr_full_skirt",
                    "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2",
                    "location": {
                        "slotName": "D2"
                    }
                }
            ],
            "modules": [],
            "commands": [
                {
                    "id": "aa62b242-77aa-41cd-abe4-46aa52e2dd82",
                    "createdAt": "2023-06-20T18:17:13.106187+00:00",
                    "commandType": "loadLabware",
                    "key": "a65f9ba51e49fe483a86eb3577d39ed1",
                    "status": "succeeded",
                    "params": {
                        "location": {
                            "slotName": "C1"
                        },
                        "loadName": "opentrons_ot3_96_tiprack_50ul",
                        "namespace": "opentrons",
                        "version": 1
                    },
                    "result": {
                        "labwareId": "052df6d7-e106-4c3c-b008-4d53afc7349e",
                        "definition": /* elided for readability */
                    },
                    "startedAt": "2023-06-20T18:17:13.106956+00:00",
                    "completedAt": "2023-06-20T18:17:13.191946+00:00"
                },
                {
                    "id": "14e18cdd-51a3-471c-81e7-8d4db68b92c6",
                    "createdAt": "2023-06-20T18:17:13.212726+00:00",
                    "commandType": "loadPipette",
                    "key": "4eca259490405003feb426e37ee37b91",
                    "status": "succeeded",
                    "params": {
                        "pipetteName": "p50_multi_gen3",
                        "mount": "right"
                    },
                    "result": {
                        "pipetteId": "43aa69ef-9fcb-4a8d-a605-eb00b46ea711"
                    },
                    "startedAt": "2023-06-20T18:17:13.243933+00:00",
                    "completedAt": "2023-06-20T18:17:13.248831+00:00"
                },
                {
                    "id": "1415d59d-cea3-4296-9354-92897e11a172",
                    "createdAt": "2023-06-20T18:17:13.258981+00:00",
                    "commandType": "loadLabware",
                    "key": "9b3cec49912e93925c019a164e3d4aa6",
                    "status": "succeeded",
                    "params": {
                        "location": {
                            "slotName": "D2"
                        },
                        "loadName": "nest_96_wellplate_100ul_pcr_full_skirt",
                        "namespace": "opentrons",
                        "version": 2
                    },
                    "result": {
                        "labwareId": "c4d562e7-6dce-492b-9c1d-7bde52e9f7c7",
                        "definition": /* elided for readability */
                    },
                    "startedAt": "2023-06-20T18:17:13.259928+00:00",
                    "completedAt": "2023-06-20T18:17:13.366491+00:00"
                },
                {
                    "id": "2623e20d-6428-4efd-a855-459919f3bd75",
                    "createdAt": "2023-06-20T18:17:13.389492+00:00",
                    "commandType": "pickUpTip",
                    "key": "0f79a97d73a9955bf3929be4043a70b9",
                    "status": "succeeded",
                    "params": {
                        "labwareId": "052df6d7-e106-4c3c-b008-4d53afc7349e",
                        "wellName": "A1",
                        "wellLocation": {
                            "origin": "top",
                            "offset": {
                                "x": 0,
                                "y": 0,
                                "z": 0
                            }
                        },
                        "pipetteId": "43aa69ef-9fcb-4a8d-a605-eb00b46ea711"
                    },
                    "result": {
                        "position": {
                            "x": 14.38,
                            "y": 181.1,
                            "z": 99
                        },
                        "tipVolume": 50,
                        "tipLength": 47.4,
                        "tipDiameter": 5.58
                    },
                    "startedAt": "2023-06-20T18:17:13.390351+00:00",
                    "completedAt": "2023-06-20T18:17:13.397476+00:00"
                }
            ],
            "errors": [
                {
                    "id": "8168a21d-1001-4fc3-84ad-d2c0d9294cc2",
                    "errorType": "ExceptionInProtocolError",
                    "createdAt": "2023-06-20T18:17:13.594951+00:00",
                    "detail": "ValidationError [line 22]: 1 validation error for AspirateParams\nvolume\n  ensure this value is greater than 0 (type=value_error.number.not_gt; limit_value=0)",
                    "errorCode": "4000",
                    "errorInfo": {},
                    "wrappedErrors": [
                        {
                            "id": "8168a21d-1001-4fc3-84ad-d2c0d9294cc2",
                            "errorType": "PythonException",
                            "createdAt": "2023-06-20T18:17:13.594951+00:00",
                            "detail": "pydantic.error_wrappers.ValidationError: 1 validation error for AspirateParams\nvolume\n  ensure this value is greater than 0 (type=value_error.number.not_gt; limit_value=0)\n",
                            "errorCode": "4000",
                            "errorInfo": {
                                "args": "([ErrorWrapper(exc=NumberNotGtError(), loc=('volume',))], <class 'opentrons.protocol_engine.commands.aspirate.AspirateParams'>)",
                                "model": "<class 'opentrons.protocol_engine.commands.aspirate.AspirateParams'>",
                                "raw_errors": "[ErrorWrapper(exc=NumberNotGtError(), loc=('volume',))]",
                                "traceback": "  File \"/opt/opentrons-robot-server/opentrons/protocols/execution/execute_python.py\", line 60, in run_python\n    exec(\"run(__context)\", new_globs)\n\n  File \"<string>\", line 1, in <module>\n\n  File \"error.py\", line 22, in run\n\n  File \"/opt/opentrons-robot-server/opentrons/protocols/api_support/util.py\", line 361, in _check_version_wrapper\n    return decorated_obj(*args, **kwargs)\n\n  File \"/opt/opentrons-robot-server/opentrons/protocol_api/instrument_context.py\", line 223, in aspirate\n    self._core.aspirate(\n\n  File \"/opt/opentrons-robot-server/opentrons/protocol_api/core/engine/instrument.py\", line 130, in aspirate\n    self._engine_client.aspirate(\n\n  File \"/opt/opentrons-robot-server/opentrons/protocol_engine/clients/sync_client.py\", line 254, in aspirate\n    params=commands.AspirateParams(\n\n  File \"/opt/opentrons-robot-server/pydantic/main.py\", line 406, in __init__\n    raise validation_error\n",
                                "class": "ValidationError"
                            },
                            "wrappedErrors": []
                        }
                    ]
                }
            ],
            "liquids": []
        }
    ],
    "meta": {
        "cursor": 0,
        "totalLength": 1
    }
}

@sfoster1
Copy link
Member Author

That's the result of uploading a protocol that does aspirate(-1)

@sfoster1
Copy link
Member Author

Screenshot 2023-06-20 at 2 24 53 PM
and it still gets rendered ok without further app changes

@sfoster1 sfoster1 merged commit 5271d4f into edge Jun 21, 2023
@sfoster1 sfoster1 deleted the add-errors-to-pe branch June 21, 2023 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants