juicedata/juicefs · error

jfsObjects.NewNSLock: the length of the objects parameter mu

Error message

jfsObjects.NewNSLock: the length of the objects parameter must be 1, current %s

What it means

NewNSLock implements minio's RWLocker interface and requires the namespace lock to be scoped to exactly one object path. The JuiceFS gateway cannot express a multi-object or zero-object lock in its single-lockfile flock implementation, so it panics when the caller violates that contract.

Source

Thrown at pkg/gateway/gateway.go:1523

	return j.getFlockWithTimeOut(ctx, meta.F_RDLCK, timeout)
}

func (j *jfsFLock) RUnlock() {
	if j.inode == 0 || j.readonly {
		return
	}
	if errno := j.meta.Flock(mctx, j.inode, j.owner, meta.F_UNLCK, true); errno != 0 {
		logger.Errorf("failed to release lock for inode %d by owner %d, error : %s", j.inode, j.owner, errno)
	}
	j.localLock.RUnlock()
}

func (n *jfsObjects) NewNSLock(bucket string, objects ...string) minio.RWLocker {
	if n.gConf.ReadOnly {
		return &jfsFLock{readonly: true}
	}
	if len(objects) != 1 {
		panic(fmt.Errorf("jfsObjects.NewNSLock: the length of the objects parameter must be 1, current %s", objects))
	}

	lockfile := path.Join(minio.MinioMetaBucket, minio.MinioMetaLockFile)
	var file *fs.File
	var errno syscall.Errno
	file, errno = n.fs.Open(mctx, lockfile, vfs.MODE_MASK_W)
	if errno != 0 && !errors.Is(errno, syscall.ENOENT) {
		logger.Errorf("failed to open the file to be locked: %s error %s", lockfile, errno)
		return &jfsFLock{}
	}
	if errors.Is(errno, syscall.ENOENT) {
		if file, errno = n.fs.Create(mctx, lockfile, 0666, n.gConf.Umask); errno != 0 {
			if errors.Is(errno, syscall.EEXIST) {
				if file, errno = n.fs.Open(mctx, lockfile, vfs.MODE_MASK_W); errno != 0 {
					logger.Errorf("failed to open the file to be locked: %s error %s", lockfile, errno)
					return &jfsFLock{}
				}
			} else {

View on GitHub (pinned to c9a67b23e8)

Solutions

  1. Pass exactly one object path when acquiring a namespace lock
  2. If multiple objects need locking, call NewNSLock once per object and acquire each lock separately
  3. Verify the vendored minio version matches what the JuiceFS gateway expects (go.mod) so internal lock calls pass 1 object

Example fix

// before
locker := jfs.NewNSLock(bucket, obj1, obj2)
// after
l1 := jfs.NewNSLock(bucket, obj1)
l2 := jfs.NewNSLock(bucket, obj2)
Defensive patterns

Strategy: validation

Validate before calling

if len(objects) != 1 {
    return fmt.Errorf("NewNSLock requires exactly 1 object, got %d", len(objects))
}
locker := jfs.NewNSLock(bucket, objects[0])

Type guard

func validNSLockArgs(objects []string) bool { return len(objects) == 1 }

Try / catch

// panic-based: wrap call site
defer func() {
    if r := recover(); r != nil {
        log.Printf("NewNSLock misuse: %v", r)
    }
}()

Prevention

When it happens

Trigger: Calling jfsObjects.NewNSLock with zero objects (NewNSLock(bucket)) or more than one object (NewNSLock(bucket, obj1, obj2)); internally this happens only if a minio Multipart/erasure-lock caller passes multiple lock targets.

Common situations: Embedding jfsObjects into a custom S3 gateway layer that calls NewNSLock directly with multiple object names; version drift between minio/pkg and JuiceFS changing how many lock targets are passed; readonly mode masking the bug until a writable mount is used.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of juicedata/juicefs@c9a67b23e8 (2026-09-06). Data as JSON: /api/errors/be1a38759dc7aa1e. Report an issue: GitHub.