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

Refactor websocket #4879

Merged
merged 18 commits into from
Nov 11, 2024
Merged

Refactor websocket #4879

merged 18 commits into from
Nov 11, 2024

Conversation

tofarr
Copy link
Collaborator

@tofarr tofarr commented Nov 10, 2024

Refactor client side web socket control for better maintainability.

  • [] Include this change in the Release Notes. If checked, you must provide an end-user friendly description for your change below

So, when implementing the reconnect, I kept hitting walls. Eventually I figured some of the problem was the way we use sockets on the client side - it feels that we are trying to use it like JQuery. This refactor:

  • Turns the websocket from a hook in into a component, so that it is added / removed like any other (And removing closes the socket automatically)
  • Formalizes the state a socket expose in it's hook (A status enum, an array of events, and a send method)
  • Formalizes the inputs to the socket (token, ghToken, settings, and a new enabled flag)
  • Separates the socket state from the events the client executes (Initial query, etc.)

This will make reconnect logic far easier to implement, as it can mostly be isolated to WsClientProvider.


To run this PR locally, use the following command:

docker run -it --rm   -p 3000:3000   -v /var/run/docker.sock:/var/run/docker.sock   --add-host host.docker.internal:host-gateway   -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:c642be1-nikolaik   --name openhands-app-c642be1   docker.all-hands.dev/all-hands-ai/openhands:c642be1

@tofarr tofarr marked this pull request as ready for review November 10, 2024 17:36
@amanape amanape self-requested a review November 11, 2024 15:32
frontend/src/context/ws-client-provider.tsx Outdated Show resolved Hide resolved
frontend/src/context/ws-client-provider.tsx Show resolved Hide resolved
Comment on lines +141 to +142
// Strict mode mounts and unmounts each component twice, so we have to wait in the destructor
// before actually closing the socket and cancel the operation if the component gets remounted.
Copy link
Member

Choose a reason for hiding this comment

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

We actually have a useEffectOnce!

export const useEffectOnce = (callback: () => void) => {
const isUsedRef = React.useRef(false);
React.useEffect(() => {
if (isUsedRef.current) {
return;
}
isUsedRef.current = true;
callback();
}, [isUsedRef.current]);
};

Copy link
Member

Choose a reason for hiding this comment

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

With that we may also be able to avoid persisting timeout (or having a timeout altogether) across re-renders

Copy link
Collaborator Author

@tofarr tofarr Nov 11, 2024

Choose a reason for hiding this comment

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

UseEffectOnce will execute the effect once the first time the component mounts, but then not again after this, which means if the tokens or enabled flag changes then the effect will not re-execute.

I thought about adding a custom effect useEffectIfActuallyChanged, but decided against it because this is what useEffect is actually meant to do, and it only does not to test whether components actually mount and unmount properly. Our websocket is a rare corner case where we don't want the mount / unmount behavior.

Copy link
Member

Choose a reason for hiding this comment

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

I'm referring to the one below the comment which is already executed only once when the component mounts, as implied by the empty dependency array, not the one above 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe I misunderstood? I was talking about this one:

 React.useEffect(() => {
    const timeout = closeRef.current;
    if (timeout != null) {
      clearTimeout(timeout);
    }

    return () => {
      closeRef.current = setTimeout(() => {
        wsRef.current?.close();
      }, 100);
    };
  }, []);

In this case, if I useEffectOnce...

  • useEffectOnce does not return a destructor - the destructor is what I am primarily interested in here - it is where I close the websocket if it is still open
  • Even if it did return a destructor, when should it be invoked? It would need timeout logic like I have - otherwise the destructor would always be invoked the first time the component is unmounted, closing the websocket.

Comment on lines +306 to +308
<p className="text-xs text-danger">
Changing settings during an active session will end the session
</p>
Copy link
Member

Choose a reason for hiding this comment

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

Note that the warning will always be rendered

image

Copy link
Member

Choose a reason for hiding this comment

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

This actually may be annoying. With these changes we're either always going to show this error or never since the parent does not have access to the context. 🤔

@@ -20,6 +20,7 @@ export const VERIFIED_ANTHROPIC_MODELS = [
"claude-2",
"claude-2.1",
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
Copy link
Member

Choose a reason for hiding this comment

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

If this is to prevent the failing test, I've already fixed it in #4887

Comment on lines +144 to +150
export function handleAssistantMessage(message: Record<string, unknown>) {
if (message.action) {
handleActionMessage(message as unknown as ActionMessage);
} else if (message.observation) {
handleObservationMessage(message as unknown as ObservationMessage);
} else if (message.status_update) {
handleStatusMessage(message as unknown as StatusMessage);
Copy link
Member

Choose a reason for hiding this comment

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

BTW we have some improved (but still unused) types in frontend/src/types/core. No need to change them yet, just making you aware of it if you aren't already

frontend/src/components/event-handler.tsx Outdated Show resolved Hide resolved
frontend/src/components/event-handler.tsx Show resolved Hide resolved
frontend/src/components/event-handler.tsx Outdated Show resolved Hide resolved
frontend/src/components/event-handler.tsx Show resolved Hide resolved
@tofarr tofarr requested a review from amanape November 11, 2024 20:41
@tofarr tofarr enabled auto-merge (squash) November 11, 2024 22:23
@tofarr tofarr merged commit a1a9d2f into main Nov 11, 2024
13 checks passed
@tofarr tofarr deleted the refactor-websocket-2 branch November 11, 2024 22:36
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.

2 participants