diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 45b0eea..c4779e4 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -14,7 +14,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Expired records are now properly removed from bbolt databases ([#848](https://github.com/TecharoHQ/anubis/pull/848)). - - Fix hanging on service restart ([#853](https://github.com/TecharoHQ/anubis/issues/853)) ### Added @@ -38,6 +37,10 @@ This has been fixed in the following ways: Hopefully this should limit the event loop thrashing and let ia32 browsers (as well as any environment with a smaller stack size than amd64 and aarch64 seem to have) function normally when processing Anubis proof of work challenges. +#### Fix potential memory leak when discovering a solution + +In some cases, the parallel solution finder in Anubis could cause all of the worker promises to leak due to the fact the promises were being improperly terminated. This was fixed by having Anubis debounce worker termination instead of allowing it to potentially recurse infinitely. + ## v1.21.0: Minfilia Warde > Please, be at ease. You are among friends here. diff --git a/web/js/proof-of-work-slow.mjs b/web/js/proof-of-work-slow.mjs index 69ef5a8..33780f8 100644 --- a/web/js/proof-of-work-slow.mjs +++ b/web/js/proof-of-work-slow.mjs @@ -14,32 +14,42 @@ export default function process( ); let worker = new Worker(webWorkerURL); - const terminate = () => { + let settled = false; + + const cleanup = () => { + if (settled) return; + settled = true; worker.terminate(); if (signal != null) { - // clean up listener to avoid memory leak - signal.removeEventListener("abort", terminate); - if (signal.aborted) { - console.log("PoW aborted"); - reject(false); - } + signal.removeEventListener("abort", onAbort); } + URL.revokeObjectURL(webWorkerURL); }; + + const onAbort = () => { + console.log("PoW aborted"); + cleanup(); + reject(new DOMException("Aborted", "AbortError")); + }; + if (signal != null) { - signal.addEventListener("abort", terminate, { once: true }); + if (signal.aborted) { + return onAbort(); + } + signal.addEventListener("abort", onAbort, { once: true }); } worker.onmessage = (event) => { if (typeof event.data === "number") { progressCallback?.(event.data); } else { - terminate(); + cleanup(); resolve(event.data); } }; worker.onerror = (event) => { - terminate(); + cleanup(); reject(event); }; @@ -47,8 +57,6 @@ export default function process( data, difficulty, }); - - URL.revokeObjectURL(webWorkerURL); }); } diff --git a/web/js/proof-of-work.mjs b/web/js/proof-of-work.mjs index 8759d9f..d70b9ee 100644 --- a/web/js/proof-of-work.mjs +++ b/web/js/proof-of-work.mjs @@ -12,19 +12,31 @@ export default function process( ); const workers = []; - const terminate = () => { + let settled = false; + + const cleanup = () => { + if (settled) { + return; + } + settled = true; workers.forEach((w) => w.terminate()); if (signal != null) { - // clean up listener to avoid memory leak - signal.removeEventListener("abort", terminate); - if (signal.aborted) { - console.log("PoW aborted"); - reject(false); - } + signal.removeEventListener("abort", onAbort); } + URL.revokeObjectURL(webWorkerURL); }; + + const onAbort = () => { + console.log("PoW aborted"); + cleanup(); + reject(new DOMException("Aborted", "AbortError")); + }; + if (signal != null) { - signal.addEventListener("abort", terminate, { once: true }); + if (signal.aborted) { + return onAbort(); + } + signal.addEventListener("abort", onAbort, { once: true }); } for (let i = 0; i < threads; i++) { @@ -34,13 +46,13 @@ export default function process( if (typeof event.data === "number") { progressCallback?.(event.data); } else { - terminate(); + cleanup(); resolve(event.data); } }; worker.onerror = (event) => { - terminate(); + cleanup(); reject(event); }; @@ -53,8 +65,6 @@ export default function process( workers.push(worker); } - - URL.revokeObjectURL(webWorkerURL); }); }