if( !urlParams.has('vfs') ){
throw new Error("Expecting vfs=opfs|opfs-wl URL argument for this worker");
}
-const isWebLocker = 'opfs-wl'===urlParams.get('vfs');
+const workerId = urlParams.get('opfs-async-proxy-id')
+ ?? 'unnamed';
+const isWebLocker = true; //'opfs-wl'===urlParams.get('vfs');
const wPost = (type,...args)=>postMessage({type, payload:args});
const installAsyncProxy = function(){
const toss = function(...args){throw new Error(args.join(' '))};
this API.
*/
const state = Object.create(null);
+
+ /* initS11n() is preprocessor-injected so that we have identical
+ copies in the synchronous and async halves. This side does not
+ load the SQLite library, so does not have access to that copy. */
//#define opfs-async-proxy
//#include api/opfs-common-inline.c-pp.js
//#undef opfs-async-proxy
2 = warnings and errors
3 = debug, warnings, and errors
*/
- state.verbose = 1;
+ state.verbose = 2;
const loggers = {
0:console.error.bind(console),
2:console.log.bind(console)
};
const logImpl = (level,...args)=>{
- if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
+ if(state.verbose>level) loggers[level]('opfs-async-proxy',workerId+":",...args);
};
const log = (...args)=>logImpl(2, ...args);
const warn = (...args)=>logImpl(1, ...args);
there's another race condition there). That's easy to say but
creating a viable test for that condition has proven challenging
so far.
+
+ 2026-03-06:
+
+ - baseWaitTime is the number of milliseconds to wait for the
+ first retry, doubling for each retry. It defaults to
+ (state.asyncIdleWaitTime*2).
+
+ - maxTries is the number of attempt to make, each one spaced out
+ by one additional factor of the baseWaitTime (e.g. 300, then 600,
+ then 900, the 1200...). This MUST be an integer >0 and defaults
+ to 6.
+
+ Only the Web Locks impl should use the 3rd and 4th parameters.
*/
- const getSyncHandle = async (fh,opName)=>{
+ const getSyncHandle = async (fh,opName, baseWaitTime, maxTries)=>{
if(!fh.syncHandle){
const t = performance.now();
log("Acquiring sync handle for",fh.filenameAbs);
- const maxTries = 6,
- msBase = state.asyncIdleWaitTime * 2;
+ const msBase = baseWaitTime ?? (state.asyncIdleWaitTime * 2);
+ maxTries ??= 6;
let i = 1, ms = msBase;
for(; true; ms = msBase * ++i){
try {
/**
Stores the given value at state.sabOPView[state.opIds.rc] and then
Atomics.notify()'s it.
+
+ The opName is only used for logging and debugging - all result
+ codes are expected on the same state.sabOPView slot.
*/
const storeAndNotify = (opName, value)=>{
log(opName+"() => notify(",value,")");
await releaseImplicitLock(fh);
storeAndNotify('xFileSize', rc);
},
- xLock: async function(fid/*sqlite3_file pointer*/,
- lockType/*SQLITE_LOCK_...*/){
- const fh = __openFiles[fid];
- let rc = 0;
- const oldLockType = fh.xLock;
- fh.xLock = lockType;
- if( !fh.syncHandle ){
- try {
- await getSyncHandle(fh,'xLock');
- __implicitLocks.delete(fid);
- }catch(e){
- state.s11n.storeException(1,e);
- rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
- fh.xLock = oldLockType;
- }
- }
- storeAndNotify('xLock',rc);
- },
+ /**
+ The first argument is semantically invalid here - it's an
+ address in the synchronous side's heap. We can do nothing with
+ it here except use it as a unique-per-file identifier.
+ i.e. a lookup key.
+ */
xOpen: async function(fid/*sqlite3_file pointer*/, filename,
flags/*SQLITE_OPEN_...*/,
opfsFlags/*OPFS_...*/){
await releaseImplicitLock(fh);
storeAndNotify('xTruncate',rc);
},
- xUnlock: async function(fid/*sqlite3_file pointer*/,
- lockType/*SQLITE_LOCK_...*/){
- let rc = 0;
- const fh = __openFiles[fid];
- if( fh.syncHandle
- && state.sq3Codes.SQLITE_LOCK_NONE===lockType
- /* Note that we do not differentiate between lock types in
- this VFS. We're either locked or unlocked. */ ){
- try { await closeSyncHandle(fh) }
- catch(e){
- state.s11n.storeException(1,e);
- rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
- }
- }
- storeAndNotify('xUnlock',rc);
- },
xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
let rc;
const fh = __openFiles[fid];
}
}/*vfsAsyncImpls*/;
+ if( isWebLocker ){
+ /* We require separate xLock() and xUnlock() implementations for the
+ original and Web Lock implementations. The ones in this block
+ are for the WebLock impl. */
+
+ /** Registry of active Web Locks: fid -> { mode, resolveRelease } */
+ const __activeWebLocks = Object.create(null);
+
+ vfsAsyncImpls.xLock = async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/,
+ 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';
+ const existing = __activeWebLocks[fid];
+ if( existing ){
+ if( existing.mode === requestedMode
+ || (existing.mode === 'exclusive'
+ && requestedMode === 'shared') ) {
+ storeAndNotify(whichOp, 0);
+ existing.mode = requestedMode/* ??? */;
+ fh.lockType = lockType;
+ 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.
+ */
+ if( 0 ){
+ /* 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. */
+ await closeSyncHandle(fh);
+ }
+ existing.resolveRelease();
+ delete __activeWebLocks[fid];
+ }
+
+ const oldLockType = fh.xLock;
+ let didNotify = false;
+ 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;
+ __implicitLocks.delete(fid);
+ if( 1 ){
+ /* Make ONE attempt to get the handle, but with a
+ higher-than-default retry-wait time. */
+ await getSyncHandle(fh, 'xLock', 1000, 5);
+ }else{
+ /* Try to get a lock until either we get one or trying to
+ results in a "not found" error (see getSyncHandle() docs). */
+ while( !fh.syncHandle ){
+ try{
+ await getSyncHandle(fh, 'xLock', 1000, 3);
+ }catch(e){
+ const rc = GetSyncHandleError.convertRc(e, 0);
+ if( rc === state.sq3Codes.SQLITE_CANTOPEN ){
+ /* File was deleted - see getSyncHandle() */
+ throw e;
+ }
+ error("xLock() still waiting to unlock SyncAccessHandle",fh);
+ }
+ }
+ }
+ error("xLock() SAH acquired.", fh);
+ const releasePromise = new Promise((resolveRelease) => {
+ __activeWebLocks[fid] = { mode: requestedMode, resolveRelease };
+ });
+ didNotify = true;
+ storeAndNotify(whichOp, 0) /* Unblock the C side */;
+ resolveWaitLoop(0) /* Unblock waitLoop() */;
+ await releasePromise; // Hold the lock until xUnlock
+ }).catch(e=>{
+ /**
+ We have(?) a potential deadlock situation: if the above
+ throws, we can't just blindly storeAndNotify() here to
+ unlock the C side, as it might interfere with an
+ unrelated operation. The `didNotify` check here assumes
+ that any exception which can be thrown will happen before
+ the above `didNotify=true`. e.g. getSyncHandle() can
+ throw. Apropos: we probably need to be able to configure
+ the async side with busy timeout values, and try until
+ that limit is reached, or tell it to wait indefinitely.
+
+ Because waitLoop() is `await`ing on this Promise, we can
+ be sure that the following storeAndNotify() is not
+ crossing wires with a different operation.
+ */
+ fh.xLock = oldLockType;
+ error("Exception acquiring Web Lock", e);
+ if( !didNotify ){
+ state.s11n.storeException(1, e);
+ const rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_LOCK);
+ storeAndNotify(whichOp, rc);
+ }
+ throw e /* what else can we do? */;
+ })
+ });
+ };
+
+ vfsAsyncImpls.xUnlock = async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/){
+ const fh = __openFiles[fid];
+ const existing = __activeWebLocks[fid];
+ if( !existing ){
+ await closeSyncHandle(fh);
+ storeAndNotify('xUnlock', 0);
+ return 0;
+ }
+ error("xUnlock()",fid, lockType, fh);
+ let rc = 0;
+ if( lockType === state.sq3Codes.SQLITE_LOCK_NONE ){
+ /* SQLite usually unlocks all the way to NONE */
+ existing.resolveRelease();
+ delete __activeWebLocks[fid];
+ try {await closeSyncHandle(fh)}
+ catch(e){
+ state.s11n.storeException(1,e);
+ rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
+ }
+ }else if( lockType === state.sq3Codes.SQLITE_LOCK_SHARED
+ && existing.mode === 'exclusive' ){
+ /* downgrade Exclusive -> Shared */
+ existing.resolveRelease();
+ delete __activeWebLocks[fid];
+ return vfsAsyncImpls.xLock(fid, lockType, true);
+ }else{
+ /* ??? */
+ error("xUnlock() unhandled condition", fh);
+ }
+ storeAndNotify('xUnlock', rc);
+ if( 0===rc ) fh.lockType = lockType;
+ return 0;
+ }
+
+ }else{
+
+ /* Original/"legacy" xLock() and xUnlock() */
+ vfsAsyncImpls.xLock = async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/){
+ const fh = __openFiles[fid];
+ let rc = 0;
+ const oldLockType = fh.xLock;
+ fh.xLock = lockType;
+ if( !fh.syncHandle ){
+ try {
+ await getSyncHandle(fh,'xLock');
+ __implicitLocks.delete(fid);
+ }catch(e){
+ state.s11n.storeException(1,e);
+ rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
+ fh.xLock = oldLockType;
+ }
+ }
+ storeAndNotify('xLock',rc);
+ };
+
+ vfsAsyncImpls.xUnlock = async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/){
+ let rc = 0;
+ const fh = __openFiles[fid];
+ if( fh.syncHandle
+ && state.sq3Codes.SQLITE_LOCK_NONE===lockType
+ /* Note that we do not differentiate between lock types in
+ this VFS. We're either locked or unlocked. */ ){
+ try { await closeSyncHandle(fh) }
+ catch(e){
+ state.s11n.storeException(1,e);
+ rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
+ }
+ }
+ storeAndNotify('xUnlock',rc);
+ }
+
+ }/*xLock() and xUnlock() impls*/
+
const waitLoop = async function f(){
if( !f.inited ){
f.inited = true;
an exception string written by the upcoming
operation */
) || [];
- //warn("waitLoop() whichOp =",opId, hnd, args);
+ //error("waitLoop() whichOp =",opId, f.opHandlers[opId].key, args);
await hnd(...args);
}catch(e){
error('in waitLoop():', e);
navigator.storage.getDirectory().then(function(d){
state.rootDir = d;
globalThis.onmessage = function({data}){
- warn(globalThis.location.href,"onmessage()",data);
+ //log(globalThis.location.href,"onmessage()",data);
switch(data.type){
case 'opfs-async-init':{
/* Receive shared state from synchronous partner */
const capi = sqlite3.capi,
state = opfsUtil.createVfsState('opfs', options),
opfsVfs = state.vfs,
- metrics = opfsVfs.metrics.counters,
mTimeStart = opfsVfs.mTimeStart,
mTimeEnd = opfsVfs.mTimeEnd,
opRun = opfsVfs.opRun,
+ debug = (...args)=>sqlite3.config.debug("opfs:",...args),
__openFiles = opfsVfs.__openFiles;
- /* At this point, createVfsState() has populated state and
- opfsVfs with any code common to both the "opfs" and "opfs-wl"
+ //debug("options:",JSON.stringify(options));
+ /* At this point, createVfsState() has populated `state` and
+ `opfsVfs` with any code common to both the "opfs" and "opfs-wl"
VFSes. Now comes the VFS-dependent work... */
return opfsVfs.bindVfs(util.nu({
xLock: function(pFile,lockType){
mTimeStart('xLock');
- ++metrics.xLock.count;
+ debug("xLock()...");
const f = __openFiles[pFile];
- let rc = 0;
- /* All OPFS locks are exclusive locks. If xLock() has
- previously succeeded, do nothing except record the lock
- type. If no lock is active, have the async counterpart
- lock the file. */
- if( f.lockType ) {
- f.lockType = lockType;
- }else{
- rc = opRun('xLock', pFile, lockType);
- if( 0===rc ) f.lockType = lockType;
- }
+ const rc = opRun('xLock', pFile, lockType);
+ debug("xLock() rc ",rc);
+ if( 0===rc ) f.lockType = lockType;
mTimeEnd();
return rc;
},
xUnlock: function(pFile,lockType){
mTimeStart('xUnlock');
- ++metrics.xUnlock.count;
const f = __openFiles[pFile];
- let rc = 0;
- if( capi.SQLITE_LOCK_NONE === lockType
- && f.lockType ){
- rc = opRun('xUnlock', pFile, lockType);
- }
+ const rc = opRun('xUnlock', pFile, lockType);
if( 0===rc ) f.lockType = lockType;
mTimeEnd();
return rc;
OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
sqlite3.oo1.OpfsDb = OpfsDb;
OpfsDb.importDb = opfsUtil.importDb;
- sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenCallback(
- opfsVfs.pointer,
- function(oo1Db, sqlite3){
- /* Set a relatively high default busy-timeout handler to
- help OPFS dbs deal with multi-tab/multi-worker
- contention. */
- sqlite3.capi.sqlite3_busy_timeout(oo1Db, 10000);
- }
- );
+ if( false ){
+ sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenCallback(
+ opfsVfs.pointer,
+ function(oo1Db, sqlite3){
+ /* Set a relatively high default busy-timeout handler to
+ help OPFS dbs deal with multi-tab/multi-worker
+ contention. */
+ sqlite3.capi.sqlite3_busy_timeout(oo1Db, 10000);
+ }
+ );
+ }
}/*extend sqlite3.oo1*/
})/*bindVfs()*/;
}/*installOpfsVfs()*/;
-C Remove\san\sextraneous\sOPFS\smetrics\sincrement.
-D 2026-03-06T11:49:36.430
+C This\sWeb\sLock\simpl\scan\sreliably\srun\sa\ssingle\sOPFS\sconnection\sbut\srather\sunreliably\s'loses'\sworkers\swith\shigher\scounts,\spresumably\sdue\sto\sdeadlock\sor\sdeadly\sembrace\s(how\s_all_\sof\sthem\scan\sdeadlock\sat\sonce\sis\sunclear,\sbut\sclearly\sa\sbug).
+D 2026-03-06T16:04:21.050
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 ecc3c69dc8a3b676f6999ef1b2ac4be825b7952ce5acbe86941f4f2f38607cb7
+F ext/wasm/api/opfs-common-shared.c-pp.js 7bfbf3a5ce1b558ec3d0b3e14f375e9f6003b6bb49c58480e2fbf2726cf59b2c
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 5f95c210183d203c7bc5be4a8139cc7fa2e50810c306bb2651648c32ab419fcf
+F ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js f471178310dd8c27f46aaa3bb2bdfb8e0b695b37a54b9a9c66b820e413b55f68
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 f3b7296480984bcc6050fe9724a8b215c405977dd69daea7145ece25751e4b33
+F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 3fde62ac67c963ee04030cf279357bb19b98a420973f55245c44396828b582d6
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/test-opfs-vfs.js 1618670e466f424aa289859fe0ec8ded223e42e9e69b5c851f809baaaca1a00c
F ext/wasm/tester1-worker.c-pp.html d0032241d0b24d996cf1c4dd0dde364189693af9b5c986e48af7d3d720fcd244
F ext/wasm/tester1.c-pp.html 52d88fe2c6f21a046030a36410b4839b632f4424028197a45a3d5669ea724ddb
-F ext/wasm/tester1.c-pp.js 6b946cd6d4da130dbae4a401057716d27117ca02cad2ea8c29ae9c46c675d618
+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 3425e6dad755a1c69a6efc63a47a3ade4e7f0a9a138994ba37f996571fb46288
+F ext/wasm/tests/opfs/concurrency/worker.js d0303b1403867e97455f7563285af3eb4471961b19bc22e45d021d896d48e27c
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 521bb140b7ed237c118ac9094732d06907229a6ff385502e850c679bd623fd58
-R 334abfd624b27a29d02bb7116f94ced4
+P bf3548a37712e848c7a9cadfdc1669a2be572ea0a0c28d84c157ab30f8c30c44
+R aeb14d0bd734de11301263e4e120c1e3
U stephan
-Z 944b558939ff5f7ba7e037ff2a332b5b
+Z c3b8772f8a9f1db66a44dbdb2829ac29
# Remove this line to create a well-formed Fossil manifest.