-
Notifications
You must be signed in to change notification settings - Fork 38
Fix response cross-contamination via request-response correlation UIDs #105
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,15 +24,15 @@ async function importModuleRespectingNodePath(modulePath) { | |
| for(const nodePath of NODE_PATHS) { | ||
| // Try to resolve the module in the current path | ||
| const modulePathToTry = path.join(nodePath, modulePath) | ||
| if (fileExists(modulePathToTry)) { | ||
| if (await fileExists(modulePathToTry)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you 🙇 |
||
| // imports are cached. To bust that cache, add unique query string to module name | ||
| // eg NodeJS.call({"esm-module.mjs?q=#{System.unique_integer()}", :fn}) | ||
| // it will leak memory, so I'm not doing it by default! | ||
| // it will leak memory, so I'm not doing it by default! | ||
| // see more: https://ar.al/2021/02/22/cache-busting-in-node.js-dynamic-esm-imports/#cache-invalidation-in-esm-with-dynamic-imports | ||
| return await import(modulePathToTry) | ||
| } | ||
| } | ||
|
|
||
| throw new Error(`Could not find module '${modulePath}'. Hint: File extensions are required in ESM. Tried ${NODE_PATHS.join(", ")}`) | ||
| } | ||
|
|
||
|
|
@@ -45,22 +45,28 @@ function getAncestor(parent, [key, ...keys]) { | |
| } | ||
|
|
||
| async function getResponse(string) { | ||
| let uid = "" | ||
| try { | ||
| const [[modulePath, ...keys], args, useImport] = JSON.parse(string) | ||
| const parsed = JSON.parse(string) | ||
| uid = parsed[0] | ||
| const [[modulePath, ...keys], args, useImport] = parsed.slice(1) | ||
| const importFn = useImport ? importModuleRespectingNodePath : requireModule | ||
| const mod = await importFn(modulePath) | ||
| const mod = await importFn(modulePath) | ||
| const fn = await getAncestor(mod, keys) | ||
| if (!fn) throw new Error(`Could not find function '${keys.join(".")}' in module '${modulePath}'`) | ||
| const returnValue = fn(...args) | ||
| const result = returnValue instanceof Promise ? await returnValue : returnValue | ||
| return JSON.stringify([true, result]) | ||
| } catch ({ message, stack }) { | ||
| return JSON.stringify([false, `${message}\n${stack}`]) | ||
| return { uid, data: JSON.stringify([true, result]) } | ||
| } catch (err) { | ||
| const message = err?.message ?? String.valueOf(err) | ||
| const stack = err?.stack ?? "" | ||
| return { uid, data: JSON.stringify([false, `${message}\n${stack}`]) } | ||
| } | ||
| } | ||
|
|
||
| async function onLine(string) { | ||
| const buffer = Buffer.from(`${await getResponse(string)}\n`) | ||
| const { uid, data } = await getResponse(string) | ||
| const buffer = Buffer.from(`${uid}:${data}\n`) | ||
|
|
||
| // The function we called might have written something to stdout without starting a new line. | ||
| // So we add one here and write the response after the prefix | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -174,6 +174,39 @@ defmodule NodeJS.Test do | |
| end | ||
| end | ||
|
|
||
| describe "request-response correlation" do | ||
| test "stale responses from timed-out calls never leak into subsequent calls" do | ||
| # Use a dedicated single-worker pool so all calls go to the same worker. | ||
| # This guarantees the stale response lands in the same GenServer mailbox | ||
| # that the follow-up call is waiting on. | ||
| path = __ENV__.file |> Path.dirname() |> Path.join("js") | ||
|
|
||
| start_supervised!( | ||
| Supervisor.child_spec( | ||
| {NodeJS.Supervisor, path: path, name: NodeJS.RaceTest, pool_size: 1}, | ||
| id: NodeJS.RaceTest | ||
| ) | ||
| ) | ||
|
|
||
| # Send a call that takes 300ms but times out after 50ms. | ||
| # After timeout, the worker is free but the Node.js response is still pending. | ||
| assert {:error, "Call timed out."} = | ||
| NodeJS.call("slow-async-echo", [9999, 300], | ||
| timeout: 50, | ||
| name: NodeJS.RaceTest | ||
| ) | ||
|
Comment on lines
+191
to
+197
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💯 |
||
|
|
||
| # Immediately send a follow-up call that takes 500ms. | ||
| # Its `receive` window overlaps with the stale response arriving at ~300ms. | ||
| # Without UID correlation, `receive` would pick up the stale `9999` response. | ||
| assert {:ok, 42} = | ||
| NodeJS.call("slow-async-echo", [42, 500], | ||
| timeout: 5_000, | ||
| name: NodeJS.RaceTest | ||
| ) | ||
|
Comment on lines
+191
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great test! Fails 100% of the time on the old version of the code. 👍 |
||
| end | ||
| end | ||
|
|
||
| describe "overriding call timeout" do | ||
| test "works, and you can tell because the slow function will time out" do | ||
| assert {:error, "Call timed out."} = NodeJS.call("slow-async-echo", [1111], timeout: 0) | ||
|
|
@@ -243,12 +276,12 @@ defmodule NodeJS.Test do | |
|
|
||
| test "fails if extension is not specified" do | ||
| assert {:error, msg} = NodeJS.call({"esm-module", :hello}, ["me"], esm: true) | ||
| assert js_error_message(msg) =~ "Cannot find module" | ||
| assert msg =~ "find module" | ||
| end | ||
|
|
||
| test "fails if file not found" do | ||
| assert {:error, msg} = NodeJS.call({"nonexisting.js", :hello}, [], esm: true) | ||
| assert js_error_message(msg) =~ "Cannot find module" | ||
| assert msg =~ "find module" | ||
| end | ||
|
|
||
| test "fails if file has errors" do | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.