Skip to content

fix(worker-shell): bundle sqlite worker runtime - #144

Open
agent-think[bot] wants to merge 2 commits into
mainfrom
fix/issue-106-sqlite-worker
Open

fix(worker-shell): bundle sqlite worker runtime#144
agent-think[bot] wants to merge 2 commits into
mainfrom
fix/issue-106-sqlite-worker

Conversation

@agent-think

@agent-think agent-think Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #106.

Summary

  • bundle just-bash's SQLite query worker implementation into the optional SQLite module graph
  • provide sql-wasm.wasm as a Dynamic Worker Loader WebAssembly module and adapt sql.js to instantiate it
  • use an in-isolate implementation of just-bash's worker message protocol because workerd does not implement node:worker_threads.Worker
  • add a workerd regression test that creates, persists, and queries a SQLite database through WorkerShellBackend
  • verify the SQLite worker/Wasm content remains exclusive to @cloudflare/computer/shell/sqlite

Verification

  • npm run test:worker-backend --workspace @cloudflare/computer
  • npm exec vitest run --workspace packages/computer -- src/backends/worker-shell/shell-modules.test.ts src/backends/worker-shell/script/partition.test.ts
  • npm run typecheck --workspace @cloudflare/computer
  • npm run build --workspace @cloudflare/computer
  • npm pack --ignore-scripts --workspace @cloudflare/computer (published artifact contains the SQLite query worker and Wasm module)

The generated core group is byte-for-byte unchanged with and without this fix: 1,882,415 generated-file bytes, 1,793,181 module-source bytes, 179 modules, and a 623,950-byte shell.js. Projects that do not import the SQLite subpath therefore do not take a bundle-size increase.


Devin Review

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cf4baac

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +84 to +88
execute(args, context) {
return command.execute(args, {
...context,
fs: filesystemWithStableIdentity(context.fs),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Concurrent SQLite writes lose updates

adaptSqliteCommand preserves each Bash instance's fsIdentity, so concurrent executions acquire separate lock maps. They can read the same snapshot and overwrite one another's changes.

Learn more

just-bash stores SQLite locks in a WeakMap keyed first by context.fsIdentity, then by database identity. ShellWorker.exec creates a fresh Bash and filesystem adapter for every execution. Spreading context preserves that fresh identity even though this wrapper replaces context.fs. Two concurrent executions therefore never see each other's lock for the same database. Both can read the old bytes, execute independently, and write complete replacement database images.

Example: Executions A and B open /workspace/data.db together. Both read a database containing one row. A inserts alice, B inserts bob, and both export their image. If B writes last, alice disappears.

Recommended fix: Give every SQLite command in the Dynamic Worker a shared fsIdentity object while retaining canonical database paths as the second-level keys. Add a concurrent integration test that forces two executions to overlap and verifies both committed changes remain.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +41 to +43
async terminate() {
this.#terminated = true;
return 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 SQLite timeouts cannot stop queries

A CPU-bound query prevents terminate() from running until executeQuery finishes. SQLite limits and shell cancellation cannot stop it, blocking every command in the Dynamic Worker.

Learn more

The original SQLite implementation runs executeQuery in a Node worker thread. Its controller enforces maxSqliteTimeoutMs by terminating that thread. The inline adapter runs the same synchronous sql.js WebAssembly work on the Dynamic Worker's event loop. A timer, abort RPC, or terminate() call cannot execute while that work occupies the isolate. The configured query timeout therefore only takes effect after the query has already returned.

Example: A recursive query that runs for minutes starts with a five-second SQLite timeout. The five-second timer cannot run while WebAssembly executes. The shell isolate remains unavailable until the query naturally finishes instead of returning after five seconds.

Recommended fix: Execute SQLite in a separately terminable Worker-compatible isolate, or add an interruption mechanism inside SQLite that the runtime can trigger independently of the blocked event loop. Do not report successful termination unless the computation has actually stopped.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if (stat.identity !== undefined || (stat.dev !== undefined && stat.ino !== undefined)) {
return stat;
}
return { ...stat, identity: await target.realpath(path) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Symlinked databases bypass locks

filesystemWithStableIdentity uses an unresolved symlink path because realpath only normalizes paths. Concurrent writes through the symlink and target use different locks and can overwrite changes.

Learn more

SQLite adds the filesystem's stable identity to its database lock keys. This wrapper synthesizes that identity from realpath, assuming aliases converge to one canonical path. WorkspaceFsAdapter.realpath validates the path with stat but returns only normalizePath(path). A symlink and its target therefore remain distinct identities even when they reference the same file. Concurrent commands can then read and replace the same database without sharing a lock.

Example: /workspace/current.db is a symlink to /workspace/data.db. One command inserts through current.db while another inserts through data.db. Each obtains a different lock, and the later full-database write can erase the earlier insert.

Recommended fix: Resolve symlinks to their final canonical target before synthesizing identity, including relative and chained links. Add a concurrency test that opens one database through both its target path and a symlink.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@144

commit: cf4baac

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

worker-shell: sqlite3 is unusable in the published package — sqlite3-worker.js is referenced but its module content is not in the tarball

0 participants