Skip to content

Commit 30ce6f5

Browse files
committed
feat: 🎸 update locking architecture
1 parent 23d8abd commit 30ce6f5

File tree

3 files changed

+103
-41
lines changed

3 files changed

+103
-41
lines changed

src/nfs/v4/attributes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ export const STAT_ATTRS = new Set<Nfsv4Attr>([
200200
Nfsv4Attr.FATTR4_TIME_MODIFY,
201201
]);
202202

203+
/**
204+
* Attributes that require filesystem stats (e.g. disk space).
205+
* If none of these are requested, we can skip the filesystem stats call.
206+
*/
203207
export const FS_ATTRS = new Set<Nfsv4Attr>([
204208
Nfsv4Attr.FATTR4_FILES_AVAIL,
205209
Nfsv4Attr.FATTR4_FILES_FREE,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as struct from '../../structs';
2+
3+
/**
4+
* Lock stateid record for NFSv4 lock operations.
5+
* Per RFC 7530 §9.1.4.1, all locks held on a particular file by a particular
6+
* owner share a single stateid, with the seqid incremented on each LOCK/LOCKU.
7+
* The stateid remains valid even after all locks are freed, as long as the
8+
* associated open file remains open.
9+
*/
10+
export class LockStateid {
11+
constructor(
12+
/**
13+
* The "other" field of the stateid (96 bits).
14+
* Uniquely identifies this lock-owner+file combination.
15+
* Remains constant across all LOCK/LOCKU operations.
16+
*/
17+
public readonly other: Uint8Array,
18+
19+
/**
20+
* Current seqid value for this lock stateid.
21+
* Incremented on each LOCK or LOCKU operation that affects locks.
22+
* Starts at 1 when first created.
23+
*/
24+
public seqid: number,
25+
26+
/**
27+
* Key identifying the lock-owner that owns this stateid.
28+
* Format: `${clientid}:${hex(owner)}`.
29+
*/
30+
public readonly lockOwnerKey: string,
31+
32+
/**
33+
* Absolute file system path of the file this stateid applies to.
34+
* A lock-owner can have different stateids for different files.
35+
*/
36+
public readonly path: string,
37+
) {}
38+
39+
/**
40+
* Get the full stateid with current seqid.
41+
*/
42+
toStateid(): struct.Nfsv4Stateid {
43+
return new struct.Nfsv4Stateid(this.seqid, this.other);
44+
}
45+
46+
/**
47+
* Increment seqid and return new stateid.
48+
* Per RFC 7530, seqid wraps from 0xFFFFFFFF to 1 (not 0).
49+
*/
50+
incrementAndGetStateid(): struct.Nfsv4Stateid {
51+
this.seqid = this.seqid === 0xffffffff ? 1 : this.seqid + 1;
52+
return this.toStateid();
53+
}
54+
}

src/nfs/v4/server/operations/node/Nfsv4OperationsNode.ts

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {OpenFileState} from '../OpenFileState';
2222
import {OpenOwnerState} from '../OpenOwnerState';
2323
import {LockOwnerState} from '../LockOwnerState';
2424
import {ByteRangeLock} from '../ByteRangeLock';
25+
import {LockStateid} from '../LockStateid';
2526
import {FilesystemStats} from '../FilesystemStats';
2627
import {FileHandleMapper, ROOT_FH} from './fh';
2728
import {isErrCode, normalizeNodeFsError} from './util';
@@ -103,6 +104,8 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
103104
protected locks: Map<string, ByteRangeLock> = new Map();
104105
/** Map from lock-owner key to lock-owner state. */
105106
protected lockOwners: Map<string, LockOwnerState> = new Map();
107+
/** Map from lock stateid 'other' field to lock stateid state. Per RFC 7530, one stateid per lock-owner per file. */
108+
protected lockStateids: Map<string, LockStateid> = new Map();
106109

107110
/**
108111
* Server-wide monotonic change counter for directory change_info.
@@ -246,6 +249,28 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
246249
return `${this.makeStateidKey(stateid)}:${offset}:${length}`;
247250
}
248251

252+
protected makeLockStateidKey(lockOwnerKey: string, path: string): string {
253+
return `${lockOwnerKey}:${path}`;
254+
}
255+
256+
protected getOrCreateLockStateid(lockOwnerKey: string, path: string): LockStateid {
257+
const key = this.makeLockStateidKey(lockOwnerKey, path);
258+
let lockStateid = this.lockStateids.get(key);
259+
if (!lockStateid) {
260+
const other = randomBytes(12);
261+
lockStateid = new LockStateid(other, 1, lockOwnerKey, path);
262+
this.lockStateids.set(key, lockStateid);
263+
const otherKey = Buffer.from(other).toString('hex');
264+
this.lockStateids.set(otherKey, lockStateid);
265+
}
266+
return lockStateid;
267+
}
268+
269+
protected findLockStateidByOther(other: Uint8Array): LockStateid | undefined {
270+
const otherKey = Buffer.from(other).toString('hex');
271+
return this.lockStateids.get(otherKey);
272+
}
273+
249274
protected hasConflictingLock(
250275
path: string,
251276
locktype: number,
@@ -950,7 +975,8 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
950975
const denied = new msg.Nfsv4LockResDenied(offset, length, locktype, conflictOwner);
951976
return new msg.Nfsv4LockResponse(Nfsv4Stat.NFS4ERR_LOCKED, undefined, denied);
952977
}
953-
const stateid = this.createStateid();
978+
const lockStateid = this.getOrCreateLockStateid(existingLockOwnerKey, currentPath);
979+
const stateid = lockStateid.incrementAndGetStateid();
954980
const lock = new ByteRangeLock(stateid, currentPath, locktype, offset, length, existingLockOwnerKey);
955981
const lockKey = this.makeLockKey(stateid, offset, length);
956982
this.locks.set(lockKey, lock);
@@ -1027,7 +1053,8 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
10271053
}
10281054
}
10291055
lockOwnerState.seqid = openToLock.lockSeqid;
1030-
const stateid = this.createStateid();
1056+
const lockStateid = this.getOrCreateLockStateid(lockOwnerKey, currentPath);
1057+
const stateid = lockStateid.incrementAndGetStateid();
10311058
const lock = new ByteRangeLock(stateid, currentPath, locktype, offset, length, lockOwnerKey);
10321059
const lockKey = this.makeLockKey(stateid, offset, length);
10331060
this.locks.set(lockKey, lock);
@@ -1053,43 +1080,16 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
10531080

10541081
public async LOCKU(request: msg.Nfsv4LockuRequest, ctx: Nfsv4OperationCtx): Promise<msg.Nfsv4LockuResponse> {
10551082
const {lockStateid, offset, length, seqid} = request;
1056-
const lockKey = this.makeLockKey(lockStateid, offset, length);
1057-
const stateidKey = this.makeStateidKey(lockStateid);
1058-
const lock = this.locks.get(lockKey);
1059-
let ownerKey: string | undefined;
1060-
let lockOwnerState: LockOwnerState | undefined;
1061-
if (lock) {
1062-
ownerKey = lock.lockOwnerKey;
1063-
lockOwnerState = this.lockOwners.get(ownerKey);
1064-
if (!lockOwnerState) throw Nfsv4Stat.NFS4ERR_BAD_STATEID;
1065-
this.renewClientLease(lockOwnerState.clientid);
1066-
} else {
1067-
const suffix = `:${stateidKey}:${offset.toString()}:${length.toString()}:${seqid}`;
1068-
for (const [candidateKey, candidateState] of this.lockOwners.entries()) {
1069-
const lastKey = candidateState.lastRequestKey;
1070-
if (lastKey && lastKey.startsWith('LOCKU:') && lastKey.endsWith(suffix)) {
1071-
lockOwnerState = candidateState;
1072-
ownerKey = candidateKey;
1073-
break;
1074-
}
1075-
}
1076-
if (!lockOwnerState) throw Nfsv4Stat.NFS4ERR_BAD_STATEID;
1077-
this.renewClientLease(lockOwnerState.clientid);
1078-
}
1079-
const requestKey = this.makeLockuRequestKey(ownerKey!, lockStateid, offset, length, seqid);
1083+
const lockStateidState = this.findLockStateidByOther(lockStateid.other);
1084+
if (!lockStateidState) throw Nfsv4Stat.NFS4ERR_BAD_STATEID;
1085+
const ownerKey = lockStateidState.lockOwnerKey;
1086+
const lockOwnerState = this.lockOwners.get(ownerKey);
1087+
if (!lockOwnerState) throw Nfsv4Stat.NFS4ERR_BAD_STATEID;
1088+
this.renewClientLease(lockOwnerState.clientid);
1089+
const currentPath = this.fh.currentPath(ctx);
1090+
if (lockStateidState.path !== currentPath) throw Nfsv4Stat.NFS4ERR_BAD_STATEID;
1091+
const requestKey = this.makeLockuRequestKey(ownerKey, lockStateid, offset, length, seqid);
10801092
const seqidValidation = this.validateSeqid(seqid, lockOwnerState.seqid);
1081-
if (!lock) {
1082-
if (seqidValidation === 'replay') {
1083-
if (lockOwnerState.lastRequestKey === requestKey && lockOwnerState.lastResponse) {
1084-
return lockOwnerState.lastResponse;
1085-
}
1086-
throw Nfsv4Stat.NFS4ERR_BAD_SEQID;
1087-
}
1088-
if (seqidValidation === 'invalid') {
1089-
throw Nfsv4Stat.NFS4ERR_BAD_SEQID;
1090-
}
1091-
throw Nfsv4Stat.NFS4ERR_BAD_STATEID;
1092-
}
10931093
if (seqidValidation === 'invalid') {
10941094
throw Nfsv4Stat.NFS4ERR_BAD_SEQID;
10951095
}
@@ -1100,9 +1100,13 @@ export class Nfsv4OperationsNode implements Nfsv4Operations {
11001100
throw Nfsv4Stat.NFS4ERR_BAD_SEQID;
11011101
}
11021102
lockOwnerState.seqid = seqid;
1103-
this.locks.delete(lockKey);
1104-
lockOwnerState.locks.delete(lockKey);
1105-
const stateid = this.createStateid();
1103+
const lockKey = this.makeLockKey(lockStateid, offset, length);
1104+
const lock = this.locks.get(lockKey);
1105+
if (lock) {
1106+
this.locks.delete(lockKey);
1107+
lockOwnerState.locks.delete(lockKey);
1108+
}
1109+
const stateid = lockStateidState.incrementAndGetStateid();
11061110
const resok = new msg.Nfsv4LockuResOk(stateid);
11071111
const response = new msg.Nfsv4LockuResponse(Nfsv4Stat.NFS4_OK, resok);
11081112
lockOwnerState.lastResponse = response;

0 commit comments

Comments
 (0)