Skip to content

Commit faa5eb5

Browse files
committed
Fix concurrency issues
1 parent 6455933 commit faa5eb5

File tree

7 files changed

+66
-22
lines changed

7 files changed

+66
-22
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ jobs:
99
matrix:
1010
node-version: [16.x, 18.x, 20.x, '*']
1111

12-
env:
13-
WASI_VERSION: 14
14-
WASI_SDK_PATH: /tmp/wasi-sdk
15-
1612
steps:
1713
- uses: actions/checkout@v3
1814
with:
@@ -25,9 +21,7 @@ jobs:
2521
2622
- name: Set up WASI-SDK
2723
run: |
28-
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_VERSION}/wasi-sdk-${WASI_VERSION}.0-linux.tar.gz
29-
mkdir -p $WASI_SDK_PATH
30-
tar xvf wasi-sdk-*-linux.tar.gz -C $WASI_SDK_PATH --strip-components=1
24+
npm run setup-wasi-sdk
3125
3226
- uses: actions/setup-node@v3
3327
with:
@@ -36,7 +30,7 @@ jobs:
3630
- run: npm install
3731

3832
- name: Build & test
39-
run: wasisdkroot=$WASI_SDK_PATH make && npm run test
33+
run: make && npm run test
4034
env:
4135
# Legacy provider required for old webpack version in new Node releases:
4236
NODE_OPTIONS: ${{ matrix.node-version != '16.x' && '--openssl-legacy-provider' || '' }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ sample/lib/
55
sample/data/random*
66
sample/data/sample.wasm.xz
77
sample/data/sample.wasm-brotli.br
8+
wasi-sdk/

Makefile

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ xzdir := module/xz-embedded
22
xzlibdir := $(xzdir)/linux/lib/xz
33

44
ifeq ($(wasisdkroot),)
5-
$(error wasisdkroot is not set)
5+
wasisdkroot := wasi-sdk
66
endif
77

8-
ifeq ($(shell grep -o WSL2 /proc/version ),WSL2)
9-
# Runs a lot faster for me
10-
webpackcommand := cmd.exe /c npm run webpack
11-
else
12-
webpackcommand := npm run webpack
13-
endif
8+
webpackcommand := node_modules/.bin/webpack
149

1510
.PHONY: all clean sample run-sample package
1611

@@ -64,6 +59,6 @@ run-sample:
6459
clean:
6560
rm -rf dist
6661
rm -rf sample/lib
67-
rm sample/data/random*
68-
rm sample/data/sample.wasm.xz
69-
rm sample/data/sample.wasm-brotli.br
62+
rm -f sample/data/random*
63+
rm -f sample/data/sample.wasm.xz
64+
rm -f sample/data/sample.wasm-brotli.br

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ XZ-Decompress doesn't have built-in support for `.tar`. However, you can use it
5454
* Clone/update submodules
5555
* `git submodule update --init --recursive`
5656
* Ensure you have a working Clang toolchain that can build wasm
57-
* For example, install https://github.com/WebAssembly/wasi-sdk
58-
* `export wasisdkroot=/path/to/wask-sdk`
57+
* If running on Linux: `npm run setup-wasi-sdk`
58+
* Otherwise you can install https://github.com/WebAssembly/wasi-sdk and `export wasisdkroot=/path/to/wasi-sdk`
5959
* (For testing only) Ensure you have `xz` and `brotli` available as commands on $PATH
6060
* Run `make`
6161

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"scripts": {
1111
"test": "mocha -r ts-node/register 'test/**/*.spec.ts'",
12+
"setup-wasi-sdk": "wasisdkroot=wasi-sdk && wasi_version=21 && rm -rf \"${wasisdkroot}\" && mkdir -p \"${wasisdkroot}\" && curl -fL -# \"https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${wasi_version}/wasi-sdk-${wasi_version}.0-linux.tar.gz\" | tar -xzf - -C \"${wasisdkroot}\" --strip-components 1",
1213
"webpack": "webpack"
1314
},
1415
"repository": {

src/xz-decompress.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,40 @@ class XzContext {
6363
}
6464
}
6565

66+
// Simple mutex to serialize context creation and prevent resource exhaustion
67+
class ContextMutex {
68+
constructor() {
69+
this.locked = false;
70+
this.waitQueue = [];
71+
}
72+
73+
async acquire() {
74+
if (!this.locked) {
75+
this.locked = true;
76+
return;
77+
}
78+
79+
// Wait in queue
80+
return new Promise((resolve) => {
81+
this.waitQueue.push(resolve);
82+
});
83+
}
84+
85+
release() {
86+
if (this.waitQueue.length > 0) {
87+
const next = this.waitQueue.shift();
88+
next();
89+
} else {
90+
this.locked = false;
91+
}
92+
}
93+
}
94+
6695
export class XzReadableStream extends ReadableStream {
6796
static _moduleInstancePromise;
6897
static _moduleInstance;
98+
static _contextMutex = new ContextMutex();
99+
69100
static async _getModuleInstance() {
70101
const base64Wasm = xzwasmBytes.replace('data:application/wasm;base64,', '');
71102
const wasmBytes = Uint8Array.from(atob(base64Wasm), c => c.charCodeAt(0)).buffer;
@@ -81,6 +112,8 @@ export class XzReadableStream extends ReadableStream {
81112

82113
super({
83114
async start(controller) {
115+
await XzReadableStream._contextMutex.acquire();
116+
84117
if (!XzReadableStream._moduleInstance) {
85118
await (XzReadableStream._moduleInstancePromise || (XzReadableStream._moduleInstancePromise = XzReadableStream._getModuleInstance()));
86119
}
@@ -105,12 +138,14 @@ export class XzReadableStream extends ReadableStream {
105138
xzContext.resetOutputBuffer();
106139

107140
if (nextOutputResult.finished) {
108-
xzContext.dispose(); // Not sure if this always happens
141+
xzContext.dispose();
142+
XzReadableStream._contextMutex.release();
109143
controller.close();
110144
}
111145
},
112146
cancel() {
113-
xzContext.dispose(); // Not sure if this always happens
147+
xzContext.dispose();
148+
XzReadableStream._contextMutex.release();
114149
return compressedReader.cancel();
115150
}
116151
});

test/test.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,22 @@ describe("Streaming XS decompression", () => {
4747
expect(result1).to.equal('hello world\n');
4848
expect(result2).to.equal('hello world\n');
4949
});
50+
51+
it("can decompress many demo test streams in parallel", async () => {
52+
const streams = [];
53+
const promises = [];
54+
55+
for (let i = 0; i < 10_000; i++) {
56+
const dataStream = buildStaticDataStream(Buffer.from(HELLO_WORLD_XZ, 'base64'));
57+
const stream = new XzReadableStream(dataStream);
58+
streams.push(stream);
59+
promises.push(collectOutputString(stream));
60+
}
61+
62+
const results = await Promise.all(promises);
63+
64+
for (const result of results) {
65+
expect(result).to.equal('hello world\n');
66+
}
67+
});
5068
});

0 commit comments

Comments
 (0)