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

[COL-335] Fix bug where space.enter() sometimes hangs. #227

Merged
merged 7 commits into from
Oct 19, 2023

Conversation

lawrence-forooghian
Copy link
Collaborator

This fixes a bug where space.enter() would sometimes hang. See commit messages for more details.

@github-actions github-actions bot temporarily deployed to staging/pull/227/typedoc October 18, 2023 17:38 Inactive
@lawrence-forooghian lawrence-forooghian marked this pull request as ready for review October 18, 2023 17:42
@@ -61,12 +62,14 @@ describe('Space', () => {

describe('enter', () => {
it<SpaceTestContext>('enter a space successfully', async ({ space, presence }) => {
mockSubscribeForSpaceEnter(presence);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd propose we do this a bit differently. I think having mockSubscribeForSpaceEnter in different tests will just make it harder to decide if that method should be used or the alternative presenceMap approach, which sets the presence set, without needing to call enter().

So my suggestion would be to for this spec (that tests enter) use a beforeEach that sets up the mock for presence, but all the other specs are refactored to not call enter() but just use the presenceMap approach. WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It sounds reasonable. I'll have to take a bit of time to understand how the presenceMap is currently used, which I'll do now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Turns out that none of these space.enter() calls in the other tests were needed, so I've removed them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(see ce44eb9)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've moved the mocking of presence.subscribe into the beforeEach of enter’s tests, as you suggested.

)
) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add test coverage for this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree it would be good to test it, but I don't have any good ideas on how. We want to test that space.enter() doesn’t return as a result of receiving a presence message whose client ID or connection ID doesn't match ours, right? I'm not sure of a good way of testing "doesn’t return" without introducing a timeout, something like this:

    it<SpaceTestContext>('doesn’t complete as a result of a presence message from a client ID that is not ours', async ({
      space,
      presence,
    }) => {
      let hasEmittedPresenceMessageWithCorrectClientId = false;
      vi.spyOn(presence, 'subscribe').mockImplementation(
        async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => {
          listener!(
            createPresenceMessage('enter', {
              clientId: 'OTHER_MOCK_CLIENT_ID',
              connectionId: '1',
            }),
          );
          setTimeout(() => {
            hasEmittedPresenceMessageWithCorrectClientId = true;
            listener!(
              createPresenceMessage('enter', {
                clientId: 'MOCK_CLIENT_ID',
                connectionId: '1',
              }),
            );
          }, 500);
        },
      );

      await space.enter();
      expect(hasEmittedPresenceMessageWithCorrectClientId).to.be.true;
    });

How do you feel about that?

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you spyOn on subscribe that it gets called twice but unsubscribe just once & expect that the promise only resolves after the second message, not the first? Do you need a timeout here, or could maybe just call them one after the other?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could you spyOn on subscribe that it gets called twice but unsubscribe just once

That's not the expected behaviour — space.enter() only ever calls subscribe once, but it doesn't call unsubscribe until it receives a matching presence message.

Do you need a timeout here, or could maybe just call them one after the other?

I'm not sure exactly what you're suggesting, do you mean something like this?

    it<SpaceTestContext>('doesn’t complete as a result of a presence message from a client ID that is not ours', async ({
      space,
      presence,
    }) => {
      let hasEmittedPresenceMessageWithCorrectClientId = false;
      vi.spyOn(presence, 'subscribe').mockImplementation(
        async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => {
          listener!(
            createPresenceMessage('enter', {
              clientId: 'OTHER_MOCK_CLIENT_ID',
              connectionId: '1',
            }),
          );
          hasEmittedPresenceMessageWithCorrectClientId = true;
          listener!(
            createPresenceMessage('enter', {
              clientId: 'MOCK_CLIENT_ID',
              connectionId: '1',
            }),
          );
        },
      );

      await space.enter();
      expect(hasEmittedPresenceMessageWithCorrectClientId).to.be.true;
    });

If so, that test wouldn't really be testing anything — it would continue to pass even if you removed the client ID check from the implementation of space.enter().

Copy link
Contributor

Choose a reason for hiding this comment

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

ok, had a stab at it - bit different then I described, but how about:

it<SpaceTestContext>('doesn’t complete as a result of a presence message from a client ID that is not ours', async ({
  space, 
  presence,
}) => {     
  const unsubscribeSpy = vi.spyOn(presence, 'unsubscribe');

  vi.spyOn(presence, 'subscribe').mockImplementation(
    async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => {
      listener!(
        createPresenceMessage('enter', {
          clientId: 'OTHER_MOCK_CLIENT_ID', // a clientId that does not match our setup mock
          connectionId: '1',
        }),
      );
    },
  );

  Promise.resolve(space.enter())
  expect(unsubscribeSpy).not.toHaveBeenCalled()
}, 1000);


it<SpaceTestContext>('completes as a result of a presence message from a client ID that is ours', async ({
  space,
  presence,
}) => {
  const unsubscribeSpy = vi.spyOn(presence, 'unsubscribe');

  vi.spyOn(presence, 'subscribe').mockImplementation(
    async (_, listener?: (presenceMessage: Types.PresenceMessage) => void) => {
      listener!(
        createPresenceMessage('enter', {
          clientId: 'MOCK_CLIENT_ID', // a clientId that matches our setup mock
          connectionId: '1',
        }),
      );
    },
  );

  Promise.resolve(space.enter())
  expect(unsubscribeSpy).toHaveBeenCalled()
});

Copy link
Contributor

@dpiatek dpiatek Oct 19, 2023

Choose a reason for hiding this comment

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

Sorry, the timeout should be removed, was experimenting with how we can test enter not resolving.

Promise.resolve is a bit of trick - It doesn't do anything except pass the promise returned from enter, but it allows us to call the function without an await, which would cause the test to wait. This could also be accomplished with an IIFE, albeit it would be more verbose.

to check whether presence.unsubscribe has been called instead of checking whether space.enter() has returned?

yes, that's the key idea - still not ideal, but avoids any complexity arising from timers, for example (and is fairly concise).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

but it allows us to call the function without an await

Can't we just do that anyway? An async function is just really a function that returns a Promise, I thought. Why would we need to await its return value in order to be able to call it?

yes, that's the key idea

Cool, I'll write something based around this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we just do that anyway? An async function is just really a function that returns a Promise, I thought. Why would we need to await its return value in order to be able to call it?

you can, but then you need to add an exception for typescript (or eslint? can't remember). I don't really mind what we do here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you can, but then you need to add an exception for typescript (or eslint? can't remember)

Ah, OK — well, I've removed the Promise.resolve and nothing seems to be complaining, so I'll keep it like that 😄

Copy link
Collaborator Author

@lawrence-forooghian lawrence-forooghian Oct 19, 2023

Choose a reason for hiding this comment

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

OK, I've updated the tests in a4e0351, please could you take a look?

space.enter() doesn’t await the result of the presence enter operation
(whether it should or not is another matter), and the other tests for
space.enter() don’t mock its completion, so let’s be consistent with
them.
This removes the possibility of one test case affecting another due to
shared mock objects.
These calls presumably exist because the method that they are testing
requires that the client has entered the space. But calling
`space.enter()` in the context of these tests does nothing useful — the
underlying Realtime object is a mock, and the `presenceMap` objects in
the tests’ contexts have already been configured such that a member is
present for the space’s connection ID.
Instead of using the private `subscriptions` property, implement a
`once`-like behaviour by passing `subscribe` a listener that removes
itself once triggered.
@github-actions github-actions bot temporarily deployed to staging/pull/227/typedoc October 19, 2023 13:15 Inactive
@lawrence-forooghian
Copy link
Collaborator Author

@dpiatek I've also updated the test for space.enter() to check that it subscribes to both enter and present events.

@github-actions github-actions bot temporarily deployed to staging/pull/227/typedoc October 19, 2023 13:42 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/227/typedoc October 19, 2023 16:00 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/227/typedoc October 19, 2023 16:02 Inactive
It shouldn’t return as a result of somebody else entering the space.

I’m not sure whether this is the best way of predicating whether or not
to return — perhaps checking for some unique ID on the received ENTER
presence message would be better. But it seems like an OK low-thought
solution at least for the time being.
Calling presence.enter() does not necessarily result in the presence
object emitting an ENTER event. This could happen, for example, if the
channel does not become attached quickly enough, or if a transport
upgrade is happening at roughly the same time as the presence enter call
(note that the latter possibility means that we wouldn’t gain much by
trying to work around the former by making sure the channel becomes
attached before performing the presence enter). In both these cases, the
only visible side effect of the presence enter call will be a PRESENT
event emitted as a result of a presence SYNC.

So, we update space.enter such that it also will return if it receives a
PRESENT event for the current member.

Resolves COL-335.
@lawrence-forooghian lawrence-forooghian merged commit 6e77941 into main Oct 19, 2023
6 checks passed
@lawrence-forooghian lawrence-forooghian deleted the COL-335-space.enter-hanging branch October 19, 2023 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants