-
Notifications
You must be signed in to change notification settings - Fork 104
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
Fix unsubscribe from optimistic inflight subscription #275
Conversation
Tried to reproduce this with test case: test.each(transportCases)("%s: unsubscribe inflight", async (transport, endpoint) => {
const c = new Centrifuge([{
transport: transport as TransportName,
endpoint: endpoint,
}], {
websocket: WebSocket,
fetch: fetch,
eventsource: EventSource,
readableStream: ReadableStream,
emulationEndpoint: 'http://localhost:8000/emulation'
});
const sub = c.newSubscription('test');
let unsubcribeCalled: any;
const unsubscribedPromise = new Promise<UnsubscribedContext>((resolve, _) => {
unsubcribeCalled = resolve;
})
sub.on('unsubscribed', (ctx) => {
unsubcribeCalled(ctx);
})
sub.subscribe();
c.connect();
sub.unsubscribe();
expect(sub.state).toBe(SubscriptionState.Unsubscribed);
await unsubscribedPromise;
let disconnectCalled: any;
const disconnectedPromise = new Promise<DisconnectedContext>((resolve, _) => {
disconnectCalled = resolve;
})
c.on('disconnected', (ctx) => {
disconnectCalled(ctx);
})
c.disconnect();
await disconnectedPromise;
expect(c.state).toBe(State.Disconnected);
}); And it worked fine for me. In this case I am not sure how the error you've shown may be possible. Let's break down:
So for now I do not see how to reproduce the error |
I see! Looks like I missed that the client must be originally in Connected state to reproduce, that's why you called test.each(transportCases)("%s: unsubscribe inflight", async (transport, endpoint) => {
const c = new Centrifuge([{
transport: transport as TransportName,
endpoint: endpoint,
}], {
websocket: WebSocket,
fetch: fetch,
eventsource: EventSource,
readableStream: ReadableStream,
emulationEndpoint: 'http://localhost:8000/emulation'
});
c.connect();
await c.ready(5000);
const sub = c.newSubscription('test');
let unsubcribeCalled: any;
const unsubscribedPromise = new Promise<UnsubscribedContext>((resolve, _) => {
unsubcribeCalled = resolve;
})
let subcribeCalled: any;
const subscribedPromise = new Promise<SubscribedContext>((resolve, _) => {
subcribeCalled = resolve;
})
sub.on('subscribed', (ctx) => {
subcribeCalled(ctx);
})
sub.on('unsubscribed', (ctx) => {
unsubcribeCalled(ctx);
})
sub.subscribe();
c.disconnect();
c.connect();
sub.unsubscribe();
expect(sub.state).toBe(SubscriptionState.Unsubscribed);
await unsubscribedPromise;
sub.subscribe();
await subscribedPromise;
let disconnectCalled: any;
const disconnectedPromise = new Promise<DisconnectedContext>((resolve, _) => {
disconnectCalled = resolve;
})
c.on('disconnected', (ctx) => {
disconnectCalled(ctx);
})
c.disconnect();
await disconnectedPromise;
expect(c.state).toBe(State.Disconnected);
}); Is this correct assumption? |
Yep, you are right connection must be connected before calls in my example, sorry that I did not mention that at first, here's short example: const MIN_RETRY = 4000;
const MAX_RETRY = 4.611686018427388e21;
const client = new Centrifuge(centrifugeUrl, {
minReconnectDelay: MIN_RETRY,
maxReconnectDelay: MAX_RETRY,
token: centrifugoToken
});
client.once('connected', () => {
let test = client.newSubscription('test-channel33');
test.subscribe();
client.disconnect();
client.connect();
test.unsubscribe();
})
client.connect() |
Yep, I see now that this alternative solution is worse than what you originally suggested. I'll come with another PR tomorrow - using your approach as base but slightly modified – to manage |
I have been thinking about per subscription basis, but will there be use for it? All subs, that will be created after connection + optimistic subscribe frames has been sent, will be sent after connection open, if we unsub from them before connection is opened they will be just removed from queue. Also it may make In my approach all logic about optiomistic frames are bound inside P.S. Just sharing my point of view to the problem, after hours spent in researches) |
Thanks, it's definitely more complexity. But having per-subscription flags seems natural to me because not all subscriptions are optimistic in Does it make sense? UPD. Opened PR with what I mean - #276 |
Thank you! Yeah, that's exact what I have been thinking about, when have been trying per sub approach) But it looks like it's already controlled by sub itself, because it will never call What’s also a little scary (from debugging side of view) is that the unsubscribe logic will be more spread between the client and the subscription entity, which will complicate future debugging. |
I mean that calling optimistic subscribe will already change state of subscription and only such subscriptions will be calling All other subscriptions will just not call sub/unsub before connection is established and after connection is established, there's no cases when subs need to know if they are optimistic or not. So it becomes logic inside subscription that is used only inside client. |
Subscription goes to At any point after that Also I think more logic may be added in the future which may result into unnecessary unsubscribe sent. I am not happy from tight coupling of Client and Subscription – but it's sth we already have and it's hard to change without loosing efficiency. What do you think? Maybe I am still missing sth which ruins my concerns? |
I suppose it's worth writing a test case to check this for sure. |
Just got another thought, maybe it's better to introduce some kind of flag, that will indicate that transport is open and rely only on it when sending sub/unsub frames? Looks like it will close and issue and also allow not only subs with getData to send their frame during connection process, which can speed up subscription process lit bit for some cases. But I did not deep dive server code yet and not sure if it's ready for it, but from first glance, based on screenshot in my prev comment, looks like yes. upd: I can prepare the PR if this idea makes sense |
May be a bug - they should not be included to optimistic batch. At least in my understanding. Let me check why this happens and come back.
I like the idea, may even simplify some things in current implementation. One note - it's not possible to use in emulation ( |
Well, it's not included in optimistic batch, it will be sent after it as a separate frame and looks like it works, based on responses from the server |
Sorry, I've meant why it's sent before connect reply received. Also wondering, is it the same now for subscription with token case (when no Subscription |
Looks like it's also a buggy current behaviour – it fails with unsubscribe in the same manner as optimistic subs. Right? |
You mean token promise may be resolved and _sendSubscribe will be called (here) without connection check? Seems true. Need to check connection status also. |
Yep, it will lead to same behaviour Looks like transport state could resolve all this problems. Already made draft and it works perfectly with transport that has separated logic for opening and sending first batch of commands. But if it's emulated transport, looks like it may require some kind of queue for unsub commands, that is called during opening process, if we sent optimistic commands. And separation of subscribe and unsubscribe commands, which will lead to situation where we will need to make one more call of sending missing commands. I have seen you made a change in your PR which will set optimistic false for all emulation transport, looks like in such case we will never have to call _unsubscribe before such transport is really opened and it will not be a problem. If its not really required to support optimistic subs for emulation, it will be easier to prepare proposal. P.S. Just deployed fork to production, already see significant drop of unsubscribe errors from 100-200 to 20-40 per minute, but clients are still updating |
Yep, sounds great
I think we can skip optimistic subs for emulation transports for now, it's a bit unfortunate, but I think it will be right to implement it as a separate step later if needed. |
Made new PR - #278 , left some notes inside of it, can you check it out plz? |
Closing in favour of #278 |
Alternative solution for #274