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

Introduce ClientAppException #3191

Merged
merged 14 commits into from
Apr 2, 2024
28 changes: 20 additions & 8 deletions src/py/flwr/client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
TRANSPORT_TYPE_GRPC_RERE,
TRANSPORT_TYPE_REST,
TRANSPORT_TYPES,
ErrorCode,
)
from flwr.common.exit_handlers import register_exit_handlers
from flwr.common.logger import log, warn_deprecated_feature
Expand Down Expand Up @@ -491,14 +492,11 @@ def _load_client_app() -> ClientApp:
# Load ClientApp instance
client_app: ClientApp = load_client_app_fn()

# Execute ClientApp
reply_message = client_app(message=message, context=context)
# Update node state
node_state.update_context(
run_id=message.metadata.run_id,
context=context,
)

except Exception as ex: # pylint: disable=broad-exception-caught
log(ERROR, "ClientApp raised an exception", exc_info=ex)
log(ERROR, "SuperNode raised an exception", exc_info=ex)

# Legacy grpc-bidi
if transport in ["grpc-bidi", None]:
Expand All @@ -507,11 +505,25 @@ def _load_client_app() -> ClientApp:

# Don't update/change NodeState

# Create error message
e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
# Reason example: "<class 'ZeroDivisionError'>:<'division by zero'>"
reason = str(type(ex)) + ":<'" + str(ex) + "'>"
if isinstance(ex, LoadClientAppError):
reason = (
"An exception was raised when attempting to load "
"the ClientApp module"
)
e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION

# Create error message
reply_message = message.create_error_reply(
error=Error(code=0, reason=reason)
error=Error(code=e_code, reason=reason)
)
else:
# No exception, update node state
node_state.update_context(
run_id=message.metadata.run_id,
context=context,
)

# Send
Expand Down
9 changes: 9 additions & 0 deletions src/py/flwr/client/client_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
from .typing import ClientAppCallable


class ClientAppException(Exception):
"""Exepction raised when an exception is raised while executing a ClientApp."""

def __init__(self, message: str):
ex_name = self.__class__.__name__
self.message = f"\nA {ex_name} occurred." + message
super().__init__(self.message)


class ClientApp:
"""Flower ClientApp.

Expand Down
3 changes: 2 additions & 1 deletion src/py/flwr/common/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class ErrorCode:
"""Error codes for Message's Error."""

UNKNOWN = 0
CLIENT_APP_RAISED_EXCEPTION = 1
LOAD_CLIENT_APP_EXCEPTION = 1
CLIENT_APP_RAISED_EXCEPTION = 2

def __new__(cls) -> ErrorCode:
"""Prevent instantiation."""
Expand Down
17 changes: 13 additions & 4 deletions src/py/flwr/server/superlink/fleet/vce/vce_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
from logging import DEBUG, ERROR, INFO, WARN
from typing import Callable, Dict, List, Optional

from flwr.client.client_app import ClientApp, LoadClientAppError
from flwr.client.client_app import ClientApp, ClientAppException, LoadClientAppError
from flwr.client.node_state import NodeState
from flwr.common.constant import PING_MAX_INTERVAL
from flwr.common.constant import PING_MAX_INTERVAL, ErrorCode
from flwr.common.logger import log
from flwr.common.message import Error
from flwr.common.object_ref import load_app
Expand Down Expand Up @@ -94,9 +94,18 @@ async def worker(
except Exception as ex: # pylint: disable=broad-exception-caught
log(ERROR, ex)
log(ERROR, traceback.format_exc())

if isinstance(ex, ClientAppException):
e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
elif isinstance(ex, LoadClientAppError):
e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
else:
e_code = ErrorCode.UNKNOWN

reason = str(type(ex)) + ":<'" + str(ex) + "'>"
error = Error(code=0, reason=reason)
out_mssg = message.create_error_reply(error=error)
out_mssg = message.create_error_reply(
error=Error(code=e_code, reason=reason)
)

finally:
if out_mssg:
Expand Down
24 changes: 2 additions & 22 deletions src/py/flwr/simulation/ray_transport/ray_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import asyncio
import threading
import traceback
from abc import ABC
from logging import DEBUG, ERROR, WARNING
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union
Expand All @@ -25,22 +24,13 @@
from ray import ObjectRef
from ray.util.actor_pool import ActorPool

from flwr.client.client_app import ClientApp, LoadClientAppError
from flwr.client.client_app import ClientApp, ClientAppException, LoadClientAppError
from flwr.common import Context, Message
from flwr.common.logger import log

ClientAppFn = Callable[[], ClientApp]


class ClientException(Exception):
"""Raised when client side logic crashes with an exception."""

def __init__(self, message: str):
div = ">" * 7
self.message = "\n" + div + "A ClientException occurred." + message
super().__init__(self.message)


class VirtualClientEngineActor(ABC):
"""Abstract base class for VirtualClientEngine Actors."""

Expand Down Expand Up @@ -71,17 +61,7 @@ def run(
raise load_ex

except Exception as ex:
client_trace = traceback.format_exc()
mssg = (
"\n\tSomething went wrong when running your client run."
"\n\tClient "
+ cid
+ " crashed when the "
+ self.__class__.__name__
+ " was running its run."
"\n\tException triggered on the client side: " + client_trace,
)
raise ClientException(str(mssg)) from ex
raise ClientAppException(str(ex)) from ex

return cid, out_message, context

Expand Down
Loading