]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
This Web Lock impl can reliably run a single OPFS connection but rather unreliably...
authorstephan <stephan@noemail.net>
Fri, 6 Mar 2026 16:04:21 +0000 (16:04 +0000)
committerstephan <stephan@noemail.net>
Fri, 6 Mar 2026 16:04:21 +0000 (16:04 +0000)
FossilOrigin-Name: d4e8583e2e80665adfe4e814adb6c219936af1dcac4105795045cb1a7b1e4864

ext/wasm/api/opfs-common-shared.c-pp.js
ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js
ext/wasm/api/sqlite3-vfs-opfs.c-pp.js
ext/wasm/tester1.c-pp.js
ext/wasm/tests/opfs/concurrency/worker.js
manifest
manifest.uuid

index 6540939b44113a3fcf585320c2dae7d6a05c1c3a..20201841736eaefea5c8f860c14c4affca4d2e20 100644 (file)
@@ -446,7 +446,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
 
      - Call opfvs.bindVfs()
   */
-  opfsUtil.initOptions = function(options, callee){
+  opfsUtil.initOptions = function callee(options, callee){
     const urlParams = new URL(globalThis.location.href).searchParams;
     if(urlParams.has('opfs-disable')){
       //sqlite3.config.warn('Explicitly not installing "opfs" VFS due to opfs-disable flag.');
@@ -458,19 +458,22 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
       return;
     }
     options = util.nu(options);
-    if(undefined===options.verbose){
-      options.verbose = urlParams.has('opfs-verbose')
-        ? (+urlParams.get('opfs-verbose') || 2) : 1;
-    }
-    if(undefined===options.sanityChecks){
-      options.sanityChecks = urlParams.has('opfs-sanity-check');
-    }
-    if(undefined===options.proxyUri){
-      options.proxyUri = callee.defaultProxyUri;
-    }
+    options.verbose ??= urlParams.has('opfs-verbose')
+      ? (+urlParams.get('opfs-verbose') || 2) : 1;
+    options.sanityChecks ??= urlParams.has('opfs-sanity-check');
+    options.proxyUri ??= callee.defaultProxyUri;
     if('function' === typeof options.proxyUri){
       options.proxyUri = options.proxyUri();
     }
+    if( false ){
+      /* This ends up with the same values for all Worker instances. */
+      callee.counter ??= 0;
+      ++callee.counter;
+      options.workerId ??= urlParams.get('opfs-async-proxy-id') ?? callee.counter;
+    }else{
+      options.workerId ??= (Math.random() * 10000000) | 0;
+    }
+    //sqlite3.config.warn("opfsUtil options =",JSON.stringify(options), 'urlParams =', urlParams);
     return options;
   };
 
@@ -585,7 +588,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
        of this value is also used for determining how long to wait on
        lock contention to free up.
     */
-    state.asyncIdleWaitTime = isWebLocker ? 100 : 150;
+    state.asyncIdleWaitTime = isWebLocker ? 150 : 150;
 
     /**
        Whether the async counterpart should log exceptions to
@@ -1089,7 +1092,12 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
           promiseWasRejected = false;
           return promiseResolve_(sqlite3);
         };
-        options.proxyUri += '?vfs='+vfsName;
+        let proxyUri = options.proxyUri +(
+          (options.proxyUri.indexOf('?')<0) ? '?' : '&'
+        )+'vfs='+vfsName;
+        if( options.workerId ){
+          proxyUri += '&opfs-async-proxy-id='+encodeURIComponent(options.workerId);
+        }
         const W = opfsVfs.worker =
 //#if target:es6-bundler-friendly
               (()=>{
@@ -1102,9 +1110,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
                 }
               })();
 //#elif target:es6-module
-        new Worker(new URL(options.proxyUri, import.meta.url));
+        new Worker(new URL(proxyUri, import.meta.url));
 //#else
-        new Worker(options.proxyUri);
+        new Worker(proxyUri);
 //#endif
         let zombieTimer = setTimeout(()=>{
           /* At attempt to work around a browser-specific quirk in which
index 0fc23540e1c3e12cbfe3c99f437c74617ecf1b3d..5c6a11dfd2e1e3270d5a9f4f361cfd0d66d39e91 100644 (file)
@@ -54,7 +54,9 @@ const urlParams = new URL(globalThis.location.href).searchParams;
 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(' '))};
@@ -70,6 +72,10 @@ const installAsyncProxy = function(){
      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
@@ -82,7 +88,7 @@ const installAsyncProxy = function(){
      2 = warnings and errors
      3 = debug, warnings, and errors
   */
-  state.verbose = 1;
+  state.verbose = 2;
 
   const loggers = {
     0:console.error.bind(console),
@@ -90,7 +96,7 @@ const installAsyncProxy = function(){
     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);
@@ -297,13 +303,26 @@ const installAsyncProxy = function(){
      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 {
@@ -337,6 +356,9 @@ const installAsyncProxy = function(){
   /**
      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,")");
@@ -466,24 +488,12 @@ const installAsyncProxy = function(){
       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_...*/){
@@ -575,22 +585,6 @@ const installAsyncProxy = function(){
       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];
@@ -611,6 +605,187 @@ const installAsyncProxy = function(){
     }
   }/*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;
@@ -645,7 +820,7 @@ const installAsyncProxy = function(){
                   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);
@@ -656,7 +831,7 @@ const installAsyncProxy = function(){
   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 */
index 89b52c836d54a4d2e7cf2d199d9fc6c9129527c2..6d0988d6d71a81f89a9fb975b472ac8319c32900 100644 (file)
@@ -86,43 +86,31 @@ const installOpfsVfs = async function callee(options){
   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;
@@ -137,15 +125,17 @@ const installOpfsVfs = async function callee(options){
       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()*/;
index 083b5eca44ded8cd0ba142c94457da6766b475a1..8c0ba19165e614c9bf7e4ae1d2007143da5dc547 100644 (file)
@@ -129,17 +129,6 @@ globalThis.sqlite3InitModule = sqlite3InitModule;
       };
     }
   }
-  const reportFinalTestStatus = function(pass){
-    if(isUIThread()){
-      let e = document.querySelector('#color-target');
-      e.classList.add(pass ? 'tests-pass' : 'tests-fail');
-      e = document.querySelector('title');
-      e.innerText = (pass ? 'PASS' : 'FAIL') + ': ' + e.innerText;
-    }else{
-      postMessage({type:'test-result', payload:{pass}});
-    }
-    TestUtil.checkHeapSize(true);
-  };
   const log = (...args)=>{
     //console.log(...args);
     logClass('',...args);
@@ -157,6 +146,19 @@ globalThis.sqlite3InitModule = sqlite3InitModule;
     console.debug('tester1',...args);
   };
 
+  const reportFinalTestStatus = function(pass){
+    debug("Final test status:",pass);
+    if(isUIThread()){
+      let e = document.querySelector('#color-target');
+      e.classList.add(pass ? 'tests-pass' : 'tests-fail');
+      e = document.querySelector('title');
+      e.innerText = (pass ? 'PASS' : 'FAIL') + ': ' + e.innerText;
+    }else{
+      postMessage({type:'test-result', payload:{pass}});
+    }
+    TestUtil.checkHeapSize(true);
+  };
+
   const toss = (...args)=>{
     error(...args);
     throw new Error(args.join(' '));
index 77e320e9afa9e7460fc60b065ea42b30aadd79d2..365900c2d5d8c2a0f7a0775f97aa32974b630d1d 100644 (file)
@@ -1,15 +1,17 @@
-importScripts(
-  (new URL(globalThis.location.href).searchParams).get('sqlite3.dir') + '/sqlite3.js'
-);
-//const sqlite3InitModule = (await import("../../../jswasm/sqlite3.mjs", )).default;
+'use strict';
+const urlArgs = new URL(globalThis.location.href).searchParams;
+const options = {
+  workerName: urlArgs.get('workerId') || Math.round(Math.random()*10000),
+  unlockAsap: urlArgs.get('opfs-unlock-asap') || 0,
+  vfs: urlArgs.get('vfs')
+};
+const jsSqlite = urlArgs.get('sqlite3.dir')
+  +'/sqlite3.js?opfs-async-proxy-id='
+      +options.workerName;
+importScripts(jsSqlite)/*Sigh - URL args are not propagated this way*/;
+//const sqlite3InitModule = (await import(jsSqlite)).default;
 globalThis.sqlite3InitModule.__isUnderTest = true;
 globalThis.sqlite3InitModule().then(async function(sqlite3){
-  const urlArgs = new URL(globalThis.location.href).searchParams;
-  const options = {
-    workerName: urlArgs.get('workerId') || Math.round(Math.random()*10000),
-    unlockAsap: urlArgs.get('opfs-unlock-asap') || 0 /*EXPERIMENTAL*/,
-    vfs: urlArgs.get('vfs')
-  };
   const wPost = (type,...payload)=>{
     postMessage({type, worker: options.workerName, payload});
   };
@@ -41,7 +43,9 @@ globalThis.sqlite3InitModule().then(async function(sqlite3){
       db.close();
     }
     if(interval.error){
-      wPost('failed',"Ending work after interval #"+interval.count,
+      stderr("Ending work at interval #"+interval.count,
+             "due to error:", interval.error);
+      wPost('failed', "at interval #"+interval.count,
             "due to error:",interval.error);
     }else{
       wPost('finished',"Ending work after",interval.count,"intervals.");
index 94e2e47dacaf33cdc22f97da5bff351795a2941e..78515cb419720b62cee2edd93c180451ccf96f70 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-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
@@ -585,7 +585,7 @@ F ext/wasm/api/README.md a905d5c6bfc3e2df875bd391d6d6b7b48d41b43bdee02ad115b4724
 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
@@ -594,12 +594,12 @@ F ext/wasm/api/sqlite3-api-oo1.c-pp.js 45454631265d9ce82685f1a64e1650ee19c8e121c
 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
@@ -644,10 +644,10 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555
 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
@@ -2191,8 +2191,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee
 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.
index 48801140c5e4cca6f219a3841d5c5ed287d44203..17f5b7a2ef5956ba398abcc0d5e25720ff26dedf 100644 (file)
@@ -1 +1 @@
-bf3548a37712e848c7a9cadfdc1669a2be572ea0a0c28d84c157ab30f8c30c44
+d4e8583e2e80665adfe4e814adb6c219936af1dcac4105795045cb1a7b1e4864