From: stephan Date: Fri, 6 Mar 2026 19:33:25 +0000 (+0000) Subject: Minor cleanups and docs. Teach the OPFS concurrency tester to deal with SQLITE_BUSY... X-Git-Tag: major-release~100^2~10 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a85d09f1a47854b5279868bc8cfa79ec90241315;p=thirdparty%2Fsqlite.git Minor cleanups and docs. Teach the OPFS concurrency tester to deal with SQLITE_BUSY instead of failing. FossilOrigin-Name: 247ffed141f66a6a5a396a3e002995a9f00c70333271199200530066e77956c4 --- diff --git a/ext/wasm/api/opfs-common-shared.c-pp.js b/ext/wasm/api/opfs-common-shared.c-pp.js index 2020184173..734b211c09 100644 --- a/ext/wasm/api/opfs-common-shared.c-pp.js +++ b/ext/wasm/api/opfs-common-shared.c-pp.js @@ -588,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 ? 150 : 150; + state.asyncIdleWaitTime = isWebLocker ? 250 : 150; /** Whether the async counterpart should log exceptions to @@ -599,7 +599,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 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. */ @@ -636,29 +637,20 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ */ 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++; @@ -672,25 +664,28 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 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', @@ -724,17 +719,16 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ '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" @@ -747,33 +741,34 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 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); @@ -791,14 +786,15 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 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){ diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js b/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js index e2b48bff57..e83cdf416d 100644 --- a/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js +++ b/ext/wasm/api/sqlite3-opfs-async-proxy.c-pp.js @@ -618,7 +618,6 @@ const installAsyncProxy = function(){ 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'; @@ -627,38 +626,48 @@ const installAsyncProxy = function(){ 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); @@ -669,16 +678,19 @@ const installAsyncProxy = function(){ : 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); @@ -696,7 +708,7 @@ const installAsyncProxy = function(){ 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 */ @@ -723,8 +735,8 @@ const installAsyncProxy = function(){ } }else{ - /* Original/"legacy" xLock() and xUnlock() */ + vfsAsyncImpls.xLock = async function(fid/*sqlite3_file pointer*/, lockType/*SQLITE_LOCK_...*/){ const fh = __openFiles[fid]; diff --git a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js index 6d0988d6d7..ebe74e38b6 100644 --- a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js +++ b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js @@ -90,6 +90,7 @@ const installOpfsVfs = async function callee(options){ 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)); @@ -99,11 +100,14 @@ const installOpfsVfs = async function callee(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; }, diff --git a/ext/wasm/tests/opfs/concurrency/worker.js b/ext/wasm/tests/opfs/concurrency/worker.js index 365900c2d5..0f9c9b68cb 100644 --- a/ext/wasm/tests/opfs/concurrency/worker.js +++ b/ext/wasm/tests/opfs/concurrency/worker.js @@ -61,17 +61,31 @@ globalThis.sqlite3InitModule().then(async function(sqlite3){ 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; @@ -81,14 +95,24 @@ globalThis.sqlite3InitModule().then(async function(sqlite3){ ++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); }; diff --git a/manifest b/manifest index d85a70c591..0974a04f21 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -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 @@ -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 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 @@ -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 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 @@ -647,7 +647,7 @@ F ext/wasm/tester1.c-pp.html 52d88fe2c6f21a046030a36410b4839b632f4424028197a45a3 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 @@ -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 53aa080e357d7a2ffeab68a3584fda43d51ecef3dc8a1d46dd32392ae4f9740c -R fcfb0bfb06d58bd7f0d57908763a15cf +P ba81d95febc5fd0f9bbb2685fef5b1b10f9991751f2bdfafba80c15877af1cef +R 5bd89f58e70b2c6dcff7610e01717205 U stephan -Z 62318d6c194878a798365e388df66b6b +Z 52bcc9f2059e4acc631f2cd7a59885ba # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index fc702daf45..e3c556d7b3 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -ba81d95febc5fd0f9bbb2685fef5b1b10f9991751f2bdfafba80c15877af1cef +247ffed141f66a6a5a396a3e002995a9f00c70333271199200530066e77956c4