of this value is also used for determining how long to wait on
lock contention to free up.
*/
- state.asyncIdleWaitTime = isWebLocker ? 150 : 150;
+ state.asyncIdleWaitTime = isWebLocker ? 250 : 150;
/**
Whether the async counterpart should log exceptions to
0 = no exception logging.
1 = only log exceptions for "significant" ops like xOpen(),
- xRead(), and xWrite().
+ xRead(), and xWrite(). Exceptions related to, e.g., wait/retry
+ loops in acquiring SyncAccessHandles are not logged.
2 = log all exceptions.
*/
*/
state.opIds = Object.create(null);
{
- /*
- Maintenance reminder:
-
- Some of these fields are only for use by the "opfs-wl" VFS,
- but they must also be set up for the "ofps" VFS so that the
- sizes and offsets calculated here are consistent in the async
- proxy. Hypothetically they could differ and it would cope
- but... why invite disaster over eliding a few superfluous (for
- "opfs') properties?
- */
/* Indexes for use in our SharedArrayBuffer... */
let i = 0;
/* SAB slot used to communicate which operation is desired
between both workers. This worker writes to it and the other
- listens for changes. */
+ listens for changes and clears it. The values written to it
+ are state.opIds.x[A-Z][a-z]+, defined below.*/
state.opIds.whichOp = i++;
- /* Slot for storing return values. This worker listens to that
- slot and the other worker writes to it. */
+ /* Slot for storing return values. This side listens to that
+ slot and the async proxy writes to it. */
state.opIds.rc = i++;
- /* Each function gets an ID which this worker writes to
- the whichOp slot. The async-api worker uses Atomic.wait()
- on the whichOp slot to figure out which operation to run
- next. */
+ /* Each function gets an ID which this worker writes to the
+ state.opIds.whichOp slot. The async-api worker uses
+ Atomic.wait() on the whichOp slot to figure out which
+ operation to run next. */
state.opIds.xAccess = i++;
state.opIds.xClose = i++;
state.opIds.xDelete = i++;
state.opIds.xTruncate = i++;
state.opIds.xUnlock = i++;
state.opIds.xWrite = i++;
- state.opIds.mkdir = i++;
+ state.opIds.mkdir = i++ /*currently unused*/;
/** Internal signals which are used only during development and
testing via the dev console. */
state.opIds['opfs-async-metrics'] = i++;
state.opIds['opfs-async-shutdown'] = i++;
/* The retry slot is used by the async part for wait-and-retry
- semantics. Though we could hypothetically use the xSleep slot
- for that, doing so might lead to undesired side effects. */
+ semantics. It is never written to, only used as a convenient
+ place to wait-with-timeout for a value which will never be
+ written, i.e. sleep()ing, before retrying a failed attempt to
+ acquire a SharedAccessHandle. */
state.opIds.retry = i++;
state.sabOP = new SharedArrayBuffer(
- i * 4/* ==sizeof int32, noting that Atomics.wait() and friends
- can only function on Int32Array views of an SAB. */);
+ i * 4/* 4==sizeof int32, noting that Atomics.wait() and
+ friends can only function on Int32Array views of an
+ SAB. */);
}
/**
SQLITE_xxx constants to export to the async worker
counterpart...
*/
state.sq3Codes = Object.create(null);
- [
+ for(const k of [
'SQLITE_ACCESS_EXISTS',
'SQLITE_ACCESS_READWRITE',
'SQLITE_BUSY',
'SQLITE_LOCK_RESERVED',
'SQLITE_LOCK_PENDING',
'SQLITE_LOCK_EXCLUSIVE'
- ].forEach((k)=>{
- if(undefined === (state.sq3Codes[k] = capi[k])){
- toss("Maintenance required: not found:",k);
- }
- });
+ ]){
+ state.sq3Codes[k] =
+ capi[k] ?? toss("Maintenance required: not found:",k);
+ }
state.opfsFlags = Object.assign(Object.create(null),{
/**
Flag for use with xOpen(). URI flag "opfs-unlock-asap=1"
enables this. See defaultUnlockAsap, below.
- */
+ */
OPFS_UNLOCK_ASAP: 0x01,
/**
Flag for use with xOpen(). URI flag "delete-before-open=1"
downstream errors. An unlink can fail if, e.g., another tab
has the handle open.
- It goes without saying that deleting a file out from under another
- instance results in Undefined Behavior.
+ It goes without saying that deleting a file out from under
+ another instance results in Undefined Behavior.
*/
OPFS_UNLINK_BEFORE_OPEN: 0x02,
/**
- If true, any async routine which implicitly acquires a sync
- access handle (i.e. an OPFS lock) will release that lock at
- the end of the call which acquires it. If false, such
- "autolocks" are not released until the VFS is idle for some
- brief amount of time.
-
- The benefit of enabling this is much higher concurrency. The
- down-side is much-reduced performance (as much as a 4x decrease
- in speedtest1).
+ If true, any async routine which must implicitly acquire a
+ sync access handle (i.e. an OPFS lock), without an active
+ xLock(), will release that lock at the end of the call which
+ acquires it. If false, such implicit locks are not released
+ until the VFS is idle for some brief amount of time, as
+ defined by state.asyncIdleWaitTime.
+
+ The benefit of enabling this is higher concurrency. The
+ down-side is much-reduced performance (as much as a 4x
+ decrease in speedtest1).
*/
defaultUnlockAsap: false
});
- opfsVfs.metrics.reset();
+ opfsVfs.metrics.reset()/*must not be called until state.opIds is set up*/;
const metrics = opfsVfs.metrics.counters;
/**
Runs the given operation (by name) in the async worker
counterpart, waits for its response, and returns the result
- which the async worker writes to SAB[state.opIds.rc]. The
- 2nd and subsequent arguments must be the arguments for the
- async op.
+ which the async worker writes to SAB[state.opIds.rc]. The 2nd
+ and subsequent arguments must be the arguments for the async op
+ (see sqlite3-opfs-async-proxy.c-pp.js).
*/
const opRun = opfsVfs.opRun = (op,...args)=>{
const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
https://github.com/sqlite/sqlite-wasm/issues/12
Summary: in at least one browser flavor, under high loads,
- the wait()/notify() pairings can get out of sync. Calling
- wait() here until it returns 'not-equal' gets them back in
- sync.
+ the wait()/notify() pairings can get out of sync and/or
+ spuriously wake up. Calling wait() here until it returns
+ 'not-equal' gets them back in sync.
*/
}
/* When the above wait() call returns 'not-equal', the async
- half will have completed the operation and reported its results
- in the state.opIds.rc slot of the SAB. */
+ half will have completed the operation and reported its
+ results in the state.opIds.rc slot of the SAB. It may have
+ also serialized an exception for us. */
const rc = Atomics.load(state.sabOPView, state.opIds.rc);
metrics[op].wait += performance.now() - t;
if(rc && state.asyncS11nExceptions){
isFromUnlock/*only if called from this.xUnlock()*/){
const whichOp = isFromUnlock ? 'xUnlock' : 'xLock';
const fh = __openFiles[fid];
- const lockName = "sqlite3-vfs-opfs:" + fh.filenameAbs;
//error("xLock()",fid, lockType, isFromUnlock, fh);
const requestedMode = (lockType >= state.sq3Codes.SQLITE_LOCK_RESERVED)
? 'exclusive' : 'shared';
if( existing.mode === requestedMode
|| (existing.mode === 'exclusive'
&& requestedMode === 'shared') ) {
- storeAndNotify(whichOp, 0);
- existing.mode = requestedMode/* ??? */;
fh.xLock = lockType;
+ storeAndNotify(whichOp, 0);
+ /* Don't do this: existing.mode = requestedMode;
+
+ Paraphrased from advice given by a consultanting
+ developer:
+
+ If you hold an exclusive lock and SQLite requests shared,
+ you should keep exiting.mode as exclusive in because the
+ underlying Web Lock is still exclusive. Changing it to
+ shared would trick xLock into thinking it needs to
+ perform a release/re-acquire dance if an exclusive is
+ later requested.
+ */
return 0 /* Already held at required or higher level */;
}
/*
Upgrade path: we must release shared and acquire exclusive.
This transition is NOT atomic in Web Locks API.
+
+ Except that it _effectively_ is atomic if we don't call
+ closeSyncHandle(fh), as no other worker can lock that
+ until we let it go. But we can't do that without leading
+ to a deadly embrace, so...
*/
- if( 1 ){
- /* Except that it _effectively_ is atomic if we don't call
- closeSyncHandle(fh), as no other worker can lock that
- until we let it go. But we can't do that without leading
- to a deadly embrace, so... */
- await closeSyncHandle(fh);
- }
+ await closeSyncHandle(fh);
existing.resolveRelease();
delete __activeWebLocks[fid];
}
+ const lockName = "sqlite3-vfs-opfs:" + fh.filenameAbs;
const oldLockType = fh.xLock;
return new Promise((resolveWaitLoop) => {
//error("xLock() initial promise entered...");
navigator.locks.request(lockName, { mode: requestedMode }, async (lock) => {
//error("xLock() Web Lock entered.", fh);
- fh.xLock = lockType/*must be set before getSyncHandle() is called*/;
+ fh.xLock = lockType/*must be set before getSyncHandle() is called!*/;
__implicitLocks.delete(fid);
let rc = 0;
try{
- /* Make ONE attempt to get the handle, but with a
- higher-than-default retry-wait. */
- await getSyncHandle(fh, 'xLock', 317, 5);
+ /* Make only one attempt to get the handle. */
+ await getSyncHandle(fh, 'xLock');
}catch(e){
fh.xLock = oldLockType;
state.s11n.storeException(1, e);
: new Promise((resolveRelease) => {
__activeWebLocks[fid] = { mode: requestedMode, resolveRelease };
});
- storeAndNotify(whichOp, rc) /* Unblock the C side */;
- resolveWaitLoop(0) /* Unblock waitLoop() */;
- await releasePromise; // Hold the lock until xUnlock
+ storeAndNotify(whichOp, rc) /* unblock the C side */;
+ resolveWaitLoop(0) /* unblock waitLoop() */;
+ await releasePromise /* hold the lock until xUnlock */;
});
});
};
+ /** Internal helper for the opfs-wl xUnlock() */
const wlCloseHandle = async(fh)=>{
let rc = 0;
try{
+ /* For the record, we've never once seen closeSyncHandle()
+ throw, nor should it because destructors do not throw. */
await closeSyncHandle(fh);
}catch(e){
state.s11n.storeException(1,e);
storeAndNotify('xUnlock', rc);
return rc;
}
- error("xUnlock()",fid, lockType, fh);
+ //error("xUnlock()",fid, lockType, fh);
let rc = 0;
if( lockType === state.sq3Codes.SQLITE_LOCK_NONE ){
/* SQLite usually unlocks all the way to NONE */
}
}else{
-
/* Original/"legacy" xLock() and xUnlock() */
+
vfsAsyncImpls.xLock = async function(fid/*sqlite3_file pointer*/,
lockType/*SQLITE_LOCK_...*/){
const fh = __openFiles[fid];
mTimeEnd = opfsVfs.mTimeEnd,
opRun = opfsVfs.opRun,
debug = (...args)=>sqlite3.config.debug("opfs:",...args),
+ warn = (...args)=>sqlite3.config.warn("opfs:",...args),
__openFiles = opfsVfs.__openFiles;
//debug("options:",JSON.stringify(options));
return opfsVfs.bindVfs(util.nu({
xLock: function(pFile,lockType){
mTimeStart('xLock');
- debug("xLock()...");
+ //debug("xLock()...");
const f = __openFiles[pFile];
const rc = opRun('xLock', pFile, lockType);
- debug("xLock() rc ",rc);
- if( 0===rc ) f.lockType = lockType;
+ if( rc ){
+ warn("xLock() rc ",rc);
+ }else{
+ f.lockType = lockType;
+ }
mTimeEnd();
return rc;
},
stderr("Invalid VFS name:",vfs);
return;
}
- db = new ctor({
- filename: 'file:'+dbName+'?opfs-unlock-asap='+options.unlockAsap,
- flags: 'c'
- });
- sqlite3.capi.sqlite3_busy_timeout(db.pointer, 5000);
- db.transaction((db)=>{
- db.exec([
- "create table if not exists t1(w TEXT UNIQUE ON CONFLICT REPLACE,v);",
- "create table if not exists t2(w TEXT UNIQUE ON CONFLICT REPLACE,v);"
- ]);
- });
+ while(true){
+ try{
+ if( !db ){
+ db = new ctor({
+ filename: 'file:'+dbName+'?opfs-unlock-asap='+options.unlockAsap,
+ flags: 'c'
+ });
+ sqlite3.capi.sqlite3_busy_timeout(db.pointer, 15000);
+ }
+ db.transaction((db)=>{
+ db.exec([
+ "create table if not exists t1(w TEXT UNIQUE ON CONFLICT REPLACE,v);",
+ "create table if not exists t2(w TEXT UNIQUE ON CONFLICT REPLACE,v);"
+ ]);
+ });
+ break;
+ }catch(e){
+ if(e instanceof sqlite3.SQLite3Error
+ && sqlite3.capi.SQLITE_BUSY===e.resultCode){
+ stderr("Retrying for BUSY: ",e.message);
+ continue;
+ }
+ throw e;
+ }
+ }
const maxIterations =
urlArgs.has('iterations') ? (+urlArgs.get('iterations') || 10) : 10;
++interval.count;
const prefix = "v(#"+interval.count+")";
stdout("Setting",prefix,"=",tm);
- try{
- db.exec({
- sql:"INSERT OR REPLACE INTO t1(w,v) VALUES(?,?)",
- bind: [options.workerName, new Date().getTime()]
- });
- //stdout("Set",prefix);
- }catch(e){
- interval.error = e;
+ while(true){
+ try{
+ db.exec({
+ sql:"INSERT OR REPLACE INTO t1(w,v) VALUES(?,?)",
+ bind: [options.workerName, new Date().getTime()]
+ });
+ //stdout("Set",prefix);
+ break;
+ }catch(e){
+ if(e instanceof sqlite3.SQLite3Error
+ && sqlite3.capi.SQLITE_BUSY===e.resultCode){
+ stderr("Retrying for BUSY: ",e.message);
+ continue;
+ }
+ stderr("Error: ",e.message);
+ interval.error = e;
+ throw e;
+ }
}
//stdout("doWork()",prefix,"error ",interval.error);
};
-C This\sone\sreliably\sruns\s5\sworkers.\sChecking\sin\sbefore\ssubsequent\scleanups\sand\sdebug\soutput\sremoval\sbreak\sit.
-D 2026-03-06T17:10:28.455
+C Minor\scleanups\sand\sdocs.\sTeach\sthe\sOPFS\sconcurrency\stester\sto\sdeal\swith\sSQLITE_BUSY\sinstead\sof\sfailing.
+D 2026-03-06T19:33:25.647
F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
F ext/wasm/api/extern-post-js.c-pp.js d9f42ecbedc784c0d086bc37800e52946a14f7a21600b291daa3f963c314f930
F ext/wasm/api/extern-pre-js.js cc61c09c7a24a07dbecb4c352453c3985170cec12b4e7e7e7a4d11d43c5c8f41
F ext/wasm/api/opfs-common-inline.c-pp.js 5be8d6d91963849e218221b48206ae55612630bb2cd7f30b1b6fcf7a9e374b76
-F ext/wasm/api/opfs-common-shared.c-pp.js 7bfbf3a5ce1b558ec3d0b3e14f375e9f6003b6bb49c58480e2fbf2726cf59b2c
+F ext/wasm/api/opfs-common-shared.c-pp.js eccb37a2347b8b17a664401cd8ef0ee0a7e18cb81939ee4ef404905e8e9188bf
F ext/wasm/api/post-js-footer.js a50c1a2c4d008aede7b2aa1f18891a7ee71437c2f415b8aeb3db237ddce2935b
F ext/wasm/api/post-js-header.js f35d2dcf1ab7f22a93d565f8e0b622a2934fc4e743edf3b708e4dd8140eeff55
F ext/wasm/api/pre-js.c-pp.js 9234ea680a2f6a2a177e8dcd934bdc5811a9f8409165433a252b87f4c07bba6f
F ext/wasm/api/sqlite3-api-prologue.js 98fedc159c9239b226d19567d7172300dee5ffce176e5fa2f62dd1f17d088385
F ext/wasm/api/sqlite3-api-worker1.c-pp.js 1041dd645e8e821c082b628cd8d9acf70c667430f9d45167569633ffc7567938
F ext/wasm/api/sqlite3-license-version-header.js 98d90255a12d02214db634e041c8e7f2f133d9361a8ebf000ba9c9af4c6761cc
-F ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js b3235922c15ee9b92a5424e34580bf16cb971adf23559c9e7119d563b8da2fe9
+F ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js c19ca5986bceb60561973635bd68acbb93f5e1752b1d1b7f4cae20abaa8d5bd1
F ext/wasm/api/sqlite3-vfs-helper.c-pp.js 3f828cc66758acb40e9c5b4dcfd87fd478a14c8fb7f0630264e6c7fa0e57515d
F ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js a61dd2b4d919d2d5d83c5c7e49b89ecbff2525ff81419f6a6dbaecaf3819c490
F ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js 1575ea6bbcf2da1e6df6892c17521a0c1c1c199a672e9090176ea0b88de48bd9
F ext/wasm/api/sqlite3-vfs-opfs-wl.c-pp.js 8233c5f9021b0213134e2adbaf6036b8f1dffd4747083a4087c1c19ae107f962
-F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 3fde62ac67c963ee04030cf279357bb19b98a420973f55245c44396828b582d6
+F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js f3bef4dbb8364a37471e4bc33e9b1e52795596456090007aaeae25acc35d2e85
F ext/wasm/api/sqlite3-vtab-helper.c-pp.js 366596d8ff73d4cefb938bbe95bc839d503c3fab6c8335ce4bf52f0d8a7dee81
F ext/wasm/api/sqlite3-wasm.c 45bb20e19b245136711f9b78584371233975811b6560c29ed9b650e225417e29
F ext/wasm/api/sqlite3-worker1-promiser.c-pp.js aa9715f661fb700459a5a6cb1c32a4d6a770723b47aa9ac0e16c2cf87d622a66
F ext/wasm/tester1.c-pp.js a4e79fbf63bb3255d2b8ffc1cd538c115d2f6b599bc324904c80f6644379a284
F ext/wasm/tests/opfs/concurrency/index.html 657578a6e9ce1e9b8be951549ed93a6a471f4520a99e5b545928668f4285fb5e
F ext/wasm/tests/opfs/concurrency/test.js 74f4ef9a827d081e6bb0ffb1d124bb54015dab8f7ae47abd5b5f26d71633331a
-F ext/wasm/tests/opfs/concurrency/worker.js d0303b1403867e97455f7563285af3eb4471961b19bc22e45d021d896d48e27c
+F ext/wasm/tests/opfs/concurrency/worker.js ce1d5d7545b17f62bac2dcce2505a89c3690e1d9209512cc51512cee6e3024f5
F ext/wasm/tests/opfs/sahpool/digest-worker.js b0ab6218588f1f0a6d15a363b493ceaf29bfb87804d9e0165915a9996377cf79
F ext/wasm/tests/opfs/sahpool/digest.html 206d08a34dc8bd570b2581d3d9ab3ecad3201b516a598dd096dcf3cf8cd81df8
F ext/wasm/tests/opfs/sahpool/index.html be736567fd92d3ecb9754c145755037cbbd2bca01385e2732294b53f4c842328
F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98
F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c
-P 53aa080e357d7a2ffeab68a3584fda43d51ecef3dc8a1d46dd32392ae4f9740c
-R fcfb0bfb06d58bd7f0d57908763a15cf
+P ba81d95febc5fd0f9bbb2685fef5b1b10f9991751f2bdfafba80c15877af1cef
+R 5bd89f58e70b2c6dcff7610e01717205
U stephan
-Z 62318d6c194878a798365e388df66b6b
+Z 52bcc9f2059e4acc631f2cd7a59885ba
# Remove this line to create a well-formed Fossil manifest.
-ba81d95febc5fd0f9bbb2685fef5b1b10f9991751f2bdfafba80c15877af1cef
+247ffed141f66a6a5a396a3e002995a9f00c70333271199200530066e77956c4