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

Chrome unregisters MSW after 5 mins of inactivity / idle #2115

Open
4 tasks done
jkinger opened this issue Mar 28, 2024 · 13 comments
Open
4 tasks done

Chrome unregisters MSW after 5 mins of inactivity / idle #2115

jkinger opened this issue Mar 28, 2024 · 13 comments
Labels
bug Something isn't working help wanted Extra attention is needed needs:triage Issues that have not been investigated yet. scope:browser Related to MSW running in a browser

Comments

@jkinger
Copy link

jkinger commented Mar 28, 2024

Prerequisites

Environment check

  • I'm using the latest msw version
  • I'm using Node.js version 18 or higher

Browsers

Chromium (Chrome, Brave, etc.)

Reproduction repository

https://github.com/jkinger/msw-react

Reproduction steps

You can either clone the provided repo or use the Github Pages link here: https://jkinger.github.io/msw-react/

If Cloning the Repo

  1. npm install
  2. npm run dev
  3. Load localhost in browser.

App Url

  1. Be sure your Chrome DevTools are NOT open.
  2. Click on 'Fetch Product' and verify that a product name loads under Results below. This ensures MSW is successfully intercepting requests.
  3. Now either open a new Tab or leave the current site without any interaction for at least 5 mins.
  4. Come back after 5mins and try to 'Fetch Product'
  5. Notice a Unexpected token JSON error in place of where another product name should be, at this point, the MSW has stopped intercepting requests.

Note: I had a similar issue open in v1 and closed in hopes this would be addressed in v2.

Current behavior

If the Chrome tab is left open (no DevTools) with no user activity for at least 5 minutes and you come back after that time you'll notice MSW stops intercepting requests.

Expected behavior

If the Chrome tab is left open with no user activity for at least 5 minutes and you come back after that time MSW should still be intercepting requests as before.

@jkinger jkinger added bug Something isn't working needs:triage Issues that have not been investigated yet. scope:browser Related to MSW running in a browser labels Mar 28, 2024
@kettanaito
Copy link
Member

Hi, @jkinger. Thanks for reporting this.

Looks like Chrome has changed the way it deactivates idle service workers.

We have a ping/pong messaging in place to make sure the client constantly messages the worker so it never goes idle:

context.keepAliveInterval = window.setInterval(
() => context.workerChannel.send('KEEPALIVE_REQUEST'),
5000,
)

case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}

Previously, the worker was considered idle if it hasn't handled any messages in the N period of time. Now, it looks like the criteria for that changed.

@kettanaito
Copy link
Member

kettanaito commented Apr 1, 2024

Extension Service Worker termination policy change

I can see that Google has changed the Service Worker termination rules for Service Workers in extensions:

The service worker has not received an event for over thirty seconds and there are no outstanding long running tasks in progress. If a service worker received an event during that time, the idle timer was removed.

https://developer.chrome.com/blog/longer-esw-lifetimes/

This should have no effect on MSW as it's not an extension but a Service Worker registered by your app.

@kettanaito
Copy link
Member

kettanaito commented Apr 1, 2024

Chrome FAQ on Service Worker termination

I've found another mention of this behavior that doesn't seem to be related to extension Service Workers but generally to Service Workers in Chrome:

The browser can terminate a running SW thread at almost any time. Chrome terminates a SW if the SW has been idle for 30 seconds. Chrome also detects long-running workers and terminates them. It does this if an event takes more than 5 minutes to settle, or if the worker is busy running synchronous JavaScript and does not respond to a ping within 30 seconds. When a SW is not running, Developer Tools and chrome://serviceworker-internals show its status as STOPPED.

https://chromium.googlesource.com/chromium/src/+/master/docs/security/service-worker-security-faq.md#do-service-workers-live-forever

If this is true, then the idle period has shortened from 5 minutes to 30 seconds.

It's worth mentioning this is a behavior specific only to the Manifest V3 API. Perhaps that's why it's not surfacing for everyone?

@kettanaito
Copy link
Member

I wonder why Chrome doesn't do this to MSW's Service Worker:

Service workers don't live indefinitely. While exact timings differ between browsers, service workers will be terminated if they've been idle for a few seconds, or if they've been busy for too long. If a service worker has been terminated and an event occurs that would start it up, it will restart.

https://web.dev/learn/pwa/service-workers#service_worker_lifespan

From what I understand, the worker goes idle, then the browser terminates it, and then your requests fail. Is that correct, @jkinger?

@kettanaito kettanaito added the help wanted Extra attention is needed label Apr 1, 2024
@kettanaito kettanaito changed the title Chrome unregisters MSW after 5 mins of inactivity / idle (MSW v2) Chrome unregisters MSW after 5 mins of inactivity / idle Apr 1, 2024
@kettanaito
Copy link
Member

A good thread on why Service Workers are killed after becoming idle: w3c/ServiceWorker#980

@kettanaito
Copy link
Member

I wonder if instead of trying to keep the worker alive and playing a guess game with the browser and its internal policies, we could detect that the browser has unregistered the worker (I do believe the worker's state change event should be emitted in that case) and if that happens, try to register and activate the worker again?

@jkinger
Copy link
Author

jkinger commented Apr 1, 2024

@kettanaito

Yes, all that you explained above sounds accurate as this just started happening within 6 months or so and only happens on Chromium-based browsers. Firefox doesn't have this issue.

Your last post about "..try to register and activate the worker again" is what I've been thinking about too. Could I re-instantiate the MSW service again somehow (like below) when the service worker is de-registered or starts throwing errors?

  const { worker } = await import('./mocks/browser')
  return worker.start()

Or would there be more to it? Or maybe something like a worker.restart()?

@kettanaito
Copy link
Member

I think it's not something you should be doing manually. I wonder if that unregistration that Chrome does trigger the state change event on the worker.

@jkinger, can you please try this out in your scenario?

navigator.serviceWorker.controller.addEventListener('statechange', (event) => {
  console.log(event.target.state)
})

What does this print when Chrome terminates the idle worker? Does it print anything at all?

I hope Chrome doesn't go around the worker's lifecycle and the termination that it initiates still causes the worker to go into something like "redundant". If we can detect that via a listener, we can call something akin to worker.start() when that state transition happens.

@jkinger
Copy link
Author

jkinger commented Apr 4, 2024

@kettanaito

I've taken your recommendation and code to see if I can get notified when Chrome stops the service worker but unfortunately not getting any events for "'statechange'" when Chrome stops it. I know what you're getting at so trying to explore other listeners as well.

FWIW another observation I've noticed is Chrome doesn't seem to unregister the Service Worker but only stops it. When I return to the Browser / Tab and start interacting with the page again Chrome starts the Server Worker again and it's still under the same Registration ID it was first registered with.

Thanks for your help thus far.

@kettanaito
Copy link
Member

FWIW another observation I've noticed is Chrome doesn't seem to unregister the Service Worker but only stops it.

Yeah, this makes sense. Aligns with what Chrome is saying they do.

When I return to the Browser / Tab and start interacting with the page again Chrome starts the Server Worker again and it's still under the same Registration ID it was first registered with.

Does this "solve" the issue then? Can you describe to me the scenario when Chrome kills the worker and that causes issues?

@jkinger
Copy link
Author

jkinger commented Apr 4, 2024

Does this "solve" the issue then?

Yeah, you would think, right? But it doesn't, I've been monitoring the Service Worker via chrome://serviceworker-internals/?devtools and I see when Chrome stops it after the set time of inactivity and then I see it being started again when I start interacting with the page but nothing is being intercepted as before.

There is a disconnect happening somewhere during the Stop / Restart of this Worker that's keeping MSW inoperable and that is what I'm trying to pin down. If Chrome re-starts it again then why doesn't MSW start responding to requests again? That's the part that doesn't make sense. It's like the Starting, Stopping then Starting again throws it off somehow.

The only way I'm able to get MSW to start responding again is by doing a Refresh.

@kettanaito
Copy link
Member

kettanaito commented Apr 4, 2024

There is a disconnect happening somewhere during the Stop / Restart of this Worker that's keeping MSW inoperable and that is what I'm trying to pin down.

I can imagine if terminating the worker also removes any listeners attached to it, MSW will lose any connection with the worker. We rely on a MessageChannel to talk to the worker. If that gets shutdown and doesn't revive, MSW won't work.

The only way I'm able to get MSW to start responding again is by doing a Refresh.

This is a strong indication that's precisely what's happening.

@jkinger
Copy link
Author

jkinger commented Apr 8, 2024

I finally identified the root cause: when Chrome stops and then restarts the service worker, it creates a new instance of mockServiceWorker.js. This action removes any previously registered Client IDs under activeClientIds. To complicate matters, when Chrome restarts the service worker, it provides no indication (that I'm aware of) that a new instance was created. Consequently, no MOCK_ACTIVATE message is posted to re-register a new active client ID.

Fortunately, all the listeners remain connected and active, so fetch will still fire. However, since there are no activeClientIds, the activeClientIds.size === 0 check will prevent it from proceeding.

I'm not yet sufficiently familiar with the MSW code to suggest a comprehensive solution, but I managed to devise a quick, albeit makeshift, fix. If implemented in the App before a fetch, it should temporarily resolve the issue. Here's what the fix looks like:

  if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.postMessage('MOCK_ACTIVATE');

    // Small wait for changes to propagate
    await new Promise((resolve) => {
      setTimeout(resolve, 10);
    });
  }

 await fetch('endpoint', {...});

By calling MOCK_ACTIVATE manually it registers a new client ID in activeClientIds and allows the rest of the Service Worker's "fetch" to continue.

I've updated my Github repo (as well as the live site) with this fix, so anyone interested can see it in action.

It’s not the most elegant solution, but it should suffice until a more definitive fix is developed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed needs:triage Issues that have not been investigated yet. scope:browser Related to MSW running in a browser
Projects
None yet
Development

No branches or pull requests

2 participants