Skip to content

Add return_exceptions argument to modal.FunctionCall.gather. #3014

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

danielshaar
Copy link
Contributor

Describe your changes

This is possible in the async context by using asyncio.gather directly, but we want to expose this in a sync way for users as well.

Changelog

  • Adds a return_exceptions argument to modal.FunctionCall.gather to allow gathering failed and successful results from a collection of FunctionCall objects.

@danielshaar danielshaar requested a review from mwaskom April 9, 2025 14:59
Copy link
Contributor

@mwaskom mwaskom left a comment

Choose a reason for hiding this comment

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

Would be great if we had a test but not sure how much effort it would be to update the relevant fixtures to support it. (Our remote function call mocking is pretty thin right now).

@danielshaar danielshaar force-pushed the dshaar/gather-return-exceptions branch from 776baf1 to 055e9d5 Compare May 16, 2025 17:14
with app.run(client=client):
results = FunctionCall.gather(failure_modal.spawn(), failure_modal.spawn(), return_exceptions=True)
for result in results:
assert isinstance(result, UserCodeException)
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this UserCodeException type?

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 a synchronicity-internal type ("User" in this case is user of synchronicity, i.e. modal sdk devs - not user of Modal) that isn't intended to leak into user code.

Internally in synchronicity it's used to wrap exceptions when they first happen and then unwrap them when they are raised to users. This is used a a shortcut to remove as much synchronicity context leak as possible in tracebacks etc. since those parts are usually not relevant to users.

However in this case, since we never raise the exceptions from the synchronized function (Function.gather(return_exceptions=True) in this case) it never triggers the unwrapping code in synchronicity and the internal type "leaks", since it's a return value instead.

I'm not sure what would be the cleanest fix tbh... It does feel like this is synchronicity's responsibility and the fix should happen on that side - I'll try something.

I'm personally not convinced the exception wrapping/unwrapping does us much good, and it's something I'd like to get rid of in synchronicity 2.0 if we can somehow get the traces relatively simple there through other means.

Copy link
Contributor

Choose a reason for hiding this comment

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

After some more investigation, the problem might be worse than I thought, apparently the client has some explicit wrapping using UserCodeException too in _process_result, which seems like the more likely culprit here 🤯

Copy link
Contributor

Choose a reason for hiding this comment

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

We need to get a 😵‍💫 option for GitHub reactions 😁

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm inclined to believe this isn't actually a synchronicity issue but a Modal hack introducing explicit UserCodeException wrapping on the modal client side. This was introduced in 0.0.35 :D

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohhhh no, synchronicity.UserCodeException is apparently being returned by .map(return_exceptions=True) as well :(

Now I'm not sure what to do - will have to discuss in client team...


with app.run(client=client):
results = FunctionCall.gather(failure_modal.spawn(), failure_modal.spawn(), return_exceptions=True)
for result in results:
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be nice to assert that you can have a mixture of exceptions with expected successful results.

@@ -1760,13 +1760,15 @@ async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_
return fc

@staticmethod
async def gather(*function_calls: "_FunctionCall[T]") -> typing.Sequence[T]:
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 going to mean that the return type is "wrong" in the case of an exception right? Any idea how asyncio.gather handles this? Might need to @overload

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 - add an @overload which differentiates the return type based on a Literal[True] value for the return_exceptions argument, and otherwise only returns Sequence[T] - that should fix the existing type assertion

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