fix(puterfs): await queue items for fsentry action (#2616)

* style(puterfs): update adherence to linter rules

* fix(puterfs): await queue items for fsentry actions

This will ensure some operations always function as expected in a
sequence, although it may incur some performance costs.
This commit is contained in:
Eric Dubé
2026-03-06 20:04:18 -05:00
committed by GitHub
parent a0fc1bca30
commit 2a027ed410
3 changed files with 127 additions and 55 deletions
+13 -9
View File
@@ -1,13 +1,16 @@
import { TeePromise } from 'teepromise';
export default class BaseOperation {
static STATUS_PENDING = {};
static STATUS_RUNNING = {};
static STATUS_DONE = {};
/** @type {PromiseLike<void> & { resolve: () => void }} */
#donePromise;
constructor () {
this.status_ = this.constructor.STATUS_PENDING;
this.donePromise = new Promise((resolve, reject) => {
this.doneResolve = resolve;
this.doneReject = reject;
});
this.#donePromise = new TeePromise();
}
get status () {
return this.status_;
@@ -15,13 +18,14 @@ export default class BaseOperation {
set status (status) {
this.status_ = status;
if ( status === this.constructor.STATUS_DONE ) {
this.doneResolve();
this.#donePromise.resolve();
}
}
awaitDone () {
return this.donePromise;
async awaitDone () {
await this.#donePromise;
}
onComplete (fn) {
this.donePromise.then(fn);
async onComplete (fn) {
await this.#donePromise;
fn();
}
}
@@ -1,3 +1,4 @@
import { TeePromise } from 'teepromise';
import BaseOperation from './BaseOperation.js';
import Delete from './Delete.js';
import Insert from './Insert.js';
@@ -128,8 +129,10 @@ export default class FSEntryController {
async fast_get_direct_descendants (uuid) {
return (uuid === PuterPath.NULL_UUID
? await db.read('SELECT uuid FROM fsentries WHERE parent_uid IS NULL')
: await db.read('SELECT uuid FROM fsentries WHERE parent_uid = ?',
[uuid])).map(x => x.uuid);
: await db.read(
'SELECT uuid FROM fsentries WHERE parent_uid = ?',
[uuid],
)).map(x => x.uuid);
}
waitForEntry (node, callback) {
@@ -183,8 +186,10 @@ export default class FSEntryController {
op.apply(answer);
}
if ( answer.is_diff ) {
const base_entry = await this.find(new NodeUIDSelector(uuid),
fetch_entry_options);
const base_entry = await this.find(
new NodeUIDSelector(uuid),
fetch_entry_options,
);
answer.entry = { ...base_entry, ...answer.entry };
}
return answer.entry;
@@ -198,10 +203,14 @@ export default class FSEntryController {
*/
async get_descendants (uuid) {
return uuid === PuterPath.NULL_UUID
? await db.read('SELECT uuid FROM fsentries WHERE parent_uid IS NULL',
[uuid])
: await db.read('SELECT uuid FROM fsentries WHERE parent_uid = ?',
[uuid])
? await db.read(
'SELECT uuid FROM fsentries WHERE parent_uid IS NULL',
[uuid],
)
: await db.read(
'SELECT uuid FROM fsentries WHERE parent_uid = ?',
[uuid],
)
;
}
@@ -217,10 +226,14 @@ export default class FSEntryController {
this.defaultProperties.join(', ')
}${thumbnail ? ', thumbnail' : ''}`;
return uuid === PuterPath.NULL_UUID
? await db.read(`SELECT ${columns} FROM fsentries WHERE parent_uid IS NULL`,
[uuid])
: await db.read(`SELECT ${columns} FROM fsentries WHERE parent_uid = ?`,
[uuid])
? await db.read(
`SELECT ${columns} FROM fsentries WHERE parent_uid IS NULL`,
[uuid],
)
: await db.read(
`SELECT ${columns} FROM fsentries WHERE parent_uid = ?`,
[uuid],
)
;
}
@@ -276,9 +289,11 @@ export default class FSEntryController {
if ( id === undefined ) return null;
if ( typeof id !== 'number' ) {
throw new Error('unexpected type for id value',
typeof id,
id);
throw new Error(
'unexpected type for id value',
typeof id,
id,
);
}
return this.find(new NodeInternalIDSelector('mysql', id));
}
@@ -293,11 +308,13 @@ export default class FSEntryController {
async findByUID (uuid, fetch_entry_options = {}) {
const { thumbnail } = fetch_entry_options;
let fsentry = await db.tryHardRead(`SELECT ${
this.defaultProperties.join(', ')
}${thumbnail ? ', thumbnail' : ''
} FROM fsentries WHERE uuid = ? LIMIT 1`,
[uuid]);
let fsentry = await db.tryHardRead(
`SELECT ${
this.defaultProperties.join(', ')
}${thumbnail ? ', thumbnail' : ''
} FROM fsentries WHERE uuid = ? LIMIT 1`,
[uuid],
);
return fsentry[0];
}
@@ -311,11 +328,13 @@ export default class FSEntryController {
async findByID (id, fetch_entry_options = {}) {
const { thumbnail } = fetch_entry_options;
let fsentry = await db.tryHardRead(`SELECT ${
this.defaultProperties.join(', ')
}${thumbnail ? ', thumbnail' : ''
} FROM fsentries WHERE id = ? LIMIT 1`,
[id]);
let fsentry = await db.tryHardRead(
`SELECT ${
this.defaultProperties.join(', ')
}${thumbnail ? ', thumbnail' : ''
} FROM fsentries WHERE id = ? LIMIT 1`,
[id],
);
return fsentry[0];
}
@@ -346,9 +365,11 @@ export default class FSEntryController {
const resultColsSql = this.defaultProperties.join(', ') +
(thumbnail ? ', thumbnail' : '');
result = await db.read(`SELECT ${ resultColsSql
} FROM fsentries WHERE path=? LIMIT 1`,
[path]);
result = await db.read(
`SELECT ${ resultColsSql
} FROM fsentries WHERE path=? LIMIT 1`,
[path],
);
// using knex instead
@@ -360,13 +381,17 @@ export default class FSEntryController {
const isLast = i == parts.length - 1;
const colsSql = isLast ? resultColsSql : 'uuid';
if ( parent_uid === null ) {
result = await db.read(`SELECT ${ colsSql
} FROM fsentries WHERE parent_uid IS NULL AND name=? LIMIT 1`,
[part]);
result = await db.read(
`SELECT ${ colsSql
} FROM fsentries WHERE parent_uid IS NULL AND name=? LIMIT 1`,
[part],
);
} else {
result = await db.read(`SELECT ${ colsSql
} FROM fsentries WHERE parent_uid=? AND name=? LIMIT 1`,
[parent_uid, part]);
result = await db.read(
`SELECT ${ colsSql
} FROM fsentries WHERE parent_uid=? AND name=? LIMIT 1`,
[parent_uid, part],
);
}
if ( ! result[0] ) return false;
@@ -377,12 +402,14 @@ export default class FSEntryController {
if ( fetch_entry_options.tracer ) {
const tracer = fetch_entry_options.tracer;
const options = fetch_entry_options.trace_options;
await tracer.startActiveSpan('fs:sql:findByPath',
...(options ? [options] : []),
async span => {
await loop();
span.end();
});
await tracer.startActiveSpan(
'fs:sql:findByPath',
...(options ? [options] : []),
async span => {
await loop();
span.end();
},
);
} else {
await loop();
}
@@ -396,8 +423,10 @@ export default class FSEntryController {
* @returns {Promise<number|undefined>} The ID of the child entry or undefined if not found
*/
async findNameInRoot (name) {
let child_id = await db.read('SELECT `id` FROM `fsentries` WHERE `parent_uid` IS NULL AND name = ? LIMIT 1',
[name]);
let child_id = await db.read(
'SELECT `id` FROM `fsentries` WHERE `parent_uid` IS NULL AND name = ? LIMIT 1',
[name],
);
return child_id[0]?.id;
}
@@ -408,8 +437,10 @@ export default class FSEntryController {
* @returns {Promise<number|undefined>} The ID of the child entry or undefined if not found
*/
async findNameInParent (parent_uid, name) {
let child_id = await db.read('SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1',
[parent_uid, name]);
let child_id = await db.read(
'SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1',
[parent_uid, name],
);
return child_id[0]?.id;
}
@@ -420,8 +451,10 @@ export default class FSEntryController {
* @returns {Promise<boolean>} True if the name exists under the parent, false otherwise
*/
async nameExistsUnderParent (parent_uid, name) {
let check_dupe = await db.read('SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1',
[parent_uid, name]);
let check_dupe = await db.read(
'SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1',
[parent_uid, name],
);
return !!check_dupe[0];
}
@@ -442,6 +475,7 @@ export default class FSEntryController {
// #region queue logic
async enqueue_ (op) {
const tp = new TeePromise();
while (
this.currentState.queue.length > this.max_queue ||
this.deferredState.queue.length > this.max_queue
@@ -474,6 +508,8 @@ export default class FSEntryController {
}
this.checkShouldExec_();
await op.awaitDone();
}
checkShouldExec_ () {
+32
View File
@@ -0,0 +1,32 @@
{
"name": "puterfs",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"teepromise": "^0.1.1",
"uuid": "^13.0.0"
}
},
"node_modules/teepromise": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/teepromise/-/teepromise-0.1.1.tgz",
"integrity": "sha512-BS++sfQYjtOdPvBCb3sd0mNYfPcZKFjSx1yA85Yz/BAAQ3jyZAINd5iB7p70Z8D0Q4XElRwKaa4/lPEP4EHyiw==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
}
}
}