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

Compatibility with comlink for workers #3669

Closed
lgarron opened this issue Jul 18, 2023 · 12 comments
Closed

Compatibility with comlink for workers #3669

lgarron opened this issue Jul 18, 2023 · 12 comments
Labels
bug Something isn't working node.js Compatibility with Node.js APIs

Comments

@lgarron
Copy link
Contributor

lgarron commented Jul 18, 2023

What is the problem this feature would solve?

I'm excited to see worker support land in #3645 — the tradeoffs look pretty great!

However, I've so far been unable to use the bun v0.6.15 canary with comlink. Adapting the comlink README:

// index.js
import { wrap } from "comlink";

async function init() {
  const worker = new Worker(await import.meta.resolve("./worker.js"), {"type": "module"});
  // WebWorkers use `postMessage` and therefore work with Comlink.
  const obj = wrap(worker);
  console.log(`Counter: ${await obj.counter}`);
  await obj.inc();
  console.log(`Counter: ${await obj.counter}`);
}
await init(); // Note: `await` was missing here in the original report; see issue conversation
// worker.js
import { expose } from "comlink";

const obj = {
  counter: 0,
  inc() {
    this.counter++;
  },
};

expose(obj);
console.log("Worker initialized.")

This prints "Worker initialized." but not the counters. In fact, there is no error output, so I'm a bit stuck trying to debug.

What is the feature you are proposing to solve the problem?

Due to the lack of an error, I'm not 100% sure what would be needed to be compatible with comlink.

  • The fact that comlink has hardcoded assumptions about browsers and node may be causing unexpected behaviour.
  • This may be due to MessagePort, but I'm not sure (since some postMessage(…) functionality is already supported).

Given that comlink is the de facto library for exposing an async API over a worker boundary, I think it would be pretty valuable to be compatible out of the box.

What alternatives have you considered?

I'm not aware of any viable alternatives to comlink (which has ≈10K stars on GitHub). If bun were not compatible, I'd probably have to figure out how to implement custom workarounds instead of consolidating around ecosystem standards and conventions. I have a lot of experience with this, but it doesn't scale to other projects.

It's also possible that small fixes to comlink would make it work with bun — I don't have enough information to draft a potential PR, though.

@lgarron lgarron added the enhancement New feature or request label Jul 18, 2023
@lgarron lgarron changed the title Compatibility with comlink Compatibility with comlink for workers Jul 18, 2023
@Jarred-Sumner
Copy link
Collaborator

Workers have a lifetime similar to Node rather than Web

Try calling .ref() on the Worker. Or pass { bun: { ref: true } } to the Worker constructor if you want to stick with browser compatible code

@Jarred-Sumner
Copy link
Collaborator

Also see https://bun.sh/docs/api/workers

@lgarron
Copy link
Contributor Author

lgarron commented Jul 18, 2023

Workers have a lifetime similar to Node rather than Web

Try calling .ref() on the Worker. Or pass { bun: { ref: true } } to the Worker constructor if you want to stick with browser compatible code

Hmm, interesting. Normally, to get node's worker_threads working like web workers I have to call .unref(). However, if the main thread is awaiting on a Promise that requires a message from the worker to resolve, the program shouldn't be exiting early either way, right?

If I ref the worker, I see the output "Counter: 0" but the program exits (with exit code 0) before await obj.inc() resolves. But thanks, this gives me a place to start debugging!

EDIT: actually, it seems to be a race condition whether "Counter: 0" prints or whether the program exits before that line as well.

@lgarron
Copy link
Contributor Author

lgarron commented Jul 18, 2023

If I ref the worker, I see the output "Counter: 0" but the program exits (with exit code 0) before await obj.inc() resolves. But thanks, this gives me a place to start debugging!

EDIT: actually, it seems to be a race condition whether "Counter: 0" prints or whether the program exits before that line as well.

More specifically, the following program exits without waiting 10 seconds and without printing an error. Somehow the execution of the main thread is being halted due to the presence of await obj.inc(); without throwing an error. 🤔

import { wrap } from "comlink";

async function init() {
  try {
    const worker = new Worker(await import.meta.resolve("./worker.js"), {"type": "module", bun: { ref: true }});
    worker.ref()
    // WebWorkers use `postMessage` and therefore work with Comlink.
    const obj = wrap(worker);
    console.log(`Counter: ${await obj.counter}`);
    await obj.inc();
  } catch (e) {
    console.log("Error!", e);
  } finally {
    await new Promise((resolve) => setTimeout(resolve, 10000)); // sleep 10 seconds
  }
}
init();

@Jarred-Sumner
Copy link
Collaborator

However, if the main thread is awaiting on a Promise that requires a message from the worker to resolve, the program shouldn't be exiting early either way, right?

normally yes, but init isn’t awaited.

@lgarron
Copy link
Contributor Author

lgarron commented Jul 18, 2023

normally yes, but init isn’t awaited.

Ah, I'm sorry. Good catch — I was trying to stay close to the sample code but I'm not normally used to testing code using a named async wrapper like this. 🤦

@lgarron
Copy link
Contributor Author

lgarron commented Jul 18, 2023

Okay, I dug into this for an hour and unfortunately all I could determine is that the "message" listener on the worker side executes the first time it receives a postMessage(…) from requestResponseMessage(…) to the worker object, but it does not execute on subsequent times (in the current bun canary).

The worker remains alive and has not removed the "message" listener, so I don't know why it would stop receiving messages. I still have no idea if this might be due to bun's implementation or due to a bad assumption in comlink.

@Jarred-Sumner
Copy link
Collaborator

there's probably a bug in bun here, due to lack of test coverage. sorry for taking up your time on this

@Jarred-Sumner
Copy link
Collaborator

After 777ee4e, back and forth messages work as expected

image

@Jarred-Sumner
Copy link
Collaborator

There is a GC bug, I suggest waiting a couple more days or until v0.7 is released

@robobun robobun added bug Something isn't working node.js Compatibility with Node.js APIs and removed enhancement New feature or request labels Jul 18, 2023
@lgarron
Copy link
Contributor Author

lgarron commented Jul 19, 2023

Thanks for the fix, I'm glad to see it! 😻

I can confirm that the latest canary prints out all the expected counters.

However, it now seems to have the opposite problem, where the sample program in the first comment doesn't exit, and I'm unable to fix that by trying to explicity unref the worker. (From other projects, I know that comlink doesn't prevent the program from exiting, as long as the worker is unrefed.)

This isn't necessarily a big issue, but I thought I'd note it.

lgarron added a commit to cubing/cubing.js that referenced this issue Jul 24, 2023
For example: `bun run src/bin/scramble.ts -- 333`

Thanks to oven-sh/bun#3645 and
oven-sh/bun#3669, we can use `bun` directly on
our source code (except the WASM parts). This requires a few changes:

- Move around the source code to account for the fact that `esbuild`
  does not have understand relative `new URL(…, import.meta.url)` or
  `import.meta.resolve(…)` references yet:
  evanw/esbuild#312 (comment)
  - This has the unfortunate side effect that some files have to move to
    the source code root. This isn't *bad* per se, but it breaks some
    assumptions while still relying on some other assumptions. I hope we
    can move the code back some time soon.
- Avoid using the trampoline workaround when we seem to be in a browser environment.
- Avoid assuming that the output of `await import.meta.resolve(…)` can
  be passed to `new URL(…)` (`bun` returns a bath without the `file:`
  protocol).
@lgarron lgarron mentioned this issue Jul 24, 2023
4 tasks
@paperdave
Copy link
Member

I did some work on Workers and their lifetime+event loop to bring them closer to how they work in Node.js, and those unref issues shouldnt exist anymore; I think we can close this issue now. If there are any new inconsistencies, we can re-open this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working node.js Compatibility with Node.js APIs
Projects
None yet
Development

No branches or pull requests

4 participants