-
Notifications
You must be signed in to change notification settings - Fork 8
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
Conversation
src/Space.test.ts
Outdated
@@ -61,12 +62,14 @@ describe('Space', () => { | |||
|
|||
describe('enter', () => { | |||
it<SpaceTestContext>('enter a space successfully', async ({ space, presence }) => { | |||
mockSubscribeForSpaceEnter(presence); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(see ce44eb9)
There was a problem hiding this comment.
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; | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you
spyOn
onsubscribe
that it gets called twice butunsubscribe
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()
.
There was a problem hiding this comment.
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()
});
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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.
2d523b2
to
3335598
Compare
3335598
to
a07cc7a
Compare
@dpiatek I've also updated the test for |
a07cc7a
to
399d203
Compare
399d203
to
c8d9e25
Compare
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.
c8d9e25
to
803416d
Compare
This fixes a bug where
space.enter()
would sometimes hang. See commit messages for more details.