Support for bulk change sequence interface

This commit is contained in:
Ben Dischinger
2026-04-29 01:16:01 -05:00
parent 70f8b6bdfa
commit cfbfa9920f
6 changed files with 584 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
FROM golang:1.24-bookworm
ARG SCOUTFS_BRANCH=main
# Clone scoutfs and checkout the specified branch
RUN git clone https://github.com/versity/scoutfs.git /tmp/scoutfs && \
cd /tmp/scoutfs && \
git checkout ${SCOUTFS_BRANCH}
# Install the scoutfs headers where c_defs_linux.go expects them
RUN mkdir -p /usr/include/scoutfs && \
cp /tmp/scoutfs/kmod/src/ioctl.h /usr/include/scoutfs/ioctl.h && \
cp /tmp/scoutfs/kmod/src/format.h /usr/include/scoutfs/format.h
WORKDIR /work
COPY go.mod ./
COPY c_defs_linux.go ./
# Generate scoutfsdefs.go
CMD ["sh", "-c", "go tool cgo -godefs c_defs_linux.go > /output/scoutfsdefs.go"]
+24
View File
@@ -34,7 +34,14 @@ package scoutfs
// typedef int32_t __s32;
// typedef int64_t __s64;
// #define __packed
// #ifndef DIV_ROUND_UP
// #define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
// #endif
// #ifndef BITS_PER_LONG
// #define BITS_PER_LONG (sizeof(long) * 8)
// #endif
// #include "/usr/include/scoutfs/ioctl.h"
// #include "/usr/include/scoutfs/format.h"
// typedef struct scoutfs_ioctl_walk_inodes_entry scoutfs_ioctl_walk_inodes_entry_t;
// typedef struct scoutfs_ioctl_walk_inodes scoutfs_ioctl_walk_inodes_t;
// typedef struct scoutfs_ioctl_ino_path scoutfs_ioctl_ino_path_t;
@@ -73,6 +80,12 @@ package scoutfs
// typedef struct scoutfs_ioctl_read_xattr_index scoutfs_ioctl_read_xattr_index_t;
// typedef struct scoutfs_ioctl_inode_attr_x scoutfs_ioctl_inode_attr_x_t;
// typedef struct scoutfs_ioctl_punch_offline scoutfs_ioctl_punch_offline_t;
// typedef struct scoutfs_ioctl_meta_seq scoutfs_ioctl_meta_seq_t;
// typedef struct scoutfs_ioctl_raw_read_meta_seq scoutfs_ioctl_raw_read_meta_seq_t;
// typedef struct scoutfs_ioctl_raw_read_inode_info scoutfs_ioctl_raw_read_inode_info_t;
// typedef struct scoutfs_ioctl_raw_read_result scoutfs_ioctl_raw_read_result_t;
// typedef struct scoutfs_inode scoutfs_inode_t;
// typedef struct scoutfs_timespec scoutfs_timespec_t;
import "C"
const IOCQUERYINODES = C.SCOUTFS_IOC_WALK_INODES
@@ -124,6 +137,11 @@ const IOCIAXBITS = C.SCOUTFS_IOC_IAX__BITS
const IOCGETATTRX = C.SCOUTFS_IOC_GET_ATTR_X
const IOCSETATTRX = C.SCOUTFS_IOC_SET_ATTR_X
const IOCPUNCHOFFLINE = C.SCOUTFS_IOC_PUNCH_OFFLINE
const IOCRAWREADMETASEQ = C.SCOUTFS_IOC_RAW_READ_META_SEQ
const IOCRAWREADINODEINFO = C.SCOUTFS_IOC_RAW_READ_INODE_INFO
const RAWREADRESULTINODE = C.SCOUTFS_IOC_RAW_READ_RESULT_INODE
const RAWREADRESULTXATTR = C.SCOUTFS_IOC_RAW_READ_RESULT_XATTR
type InodesEntry C.scoutfs_ioctl_walk_inodes_entry_t
type queryInodes C.scoutfs_ioctl_walk_inodes_t
@@ -151,6 +169,12 @@ type indexEntry C.scoutfs_ioctl_xattr_index_entry_t
type readXattrIndex C.scoutfs_ioctl_read_xattr_index_t
type inodeAttrX C.scoutfs_ioctl_inode_attr_x_t
type punchOffline C.scoutfs_ioctl_punch_offline_t
type MetaSeqEntry C.scoutfs_ioctl_meta_seq_t
type rawReadMetaSeq C.scoutfs_ioctl_raw_read_meta_seq_t
type rawReadInodeInfo C.scoutfs_ioctl_raw_read_inode_info_t
type RawReadResult C.scoutfs_ioctl_raw_read_result_t
type ScoutfsInode C.scoutfs_inode_t
type ScoutfsTimespec C.scoutfs_timespec_t
const sizeofstatfsMore = C.sizeof_scoutfs_ioctl_statfs_more_t
const sizeofxattrTotal = C.sizeof_scoutfs_ioctl_xattr_total_t
+181
View File
@@ -0,0 +1,181 @@
package main
import (
"flag"
"fmt"
"os"
"sort"
"time"
"unsafe"
scoutfs "github.com/versity/scoutfs-go"
)
type inoState struct {
metaSeq uint64
}
func main() {
path := flag.String("path", "", "path to scoutfs mount point")
debug := flag.Bool("debug", false, "enable verbose debugging output")
var xattrNames stringSlice
flag.Var(&xattrNames, "xattr", "xattr name to read with inodes (can be repeated)")
flag.Parse()
if *path == "" {
fmt.Fprintf(os.Stderr, "error: -path is required\n")
flag.Usage()
os.Exit(1)
}
f, err := os.Open(*path)
if err != nil {
fmt.Fprintf(os.Stderr, "error opening %s: %v\n", *path, err)
os.Exit(1)
}
defer f.Close()
// Track known inodes and their meta_seq
known := make(map[uint64]*inoState)
lines := 0
// Allocate a reusable results buffer for RawReadInodeInfo
resultsBuf := make([]byte, 1024*1024)
for {
reader := scoutfs.NewRawMetaSeqReader(f)
var allMS []scoutfs.MetaSeqEntry
for {
items, err := reader.Next()
if err != nil {
fmt.Fprintf(os.Stderr, "error reading meta_seq: %v\n", err)
os.Exit(1)
}
if items == nil {
break
}
allMS = append(allMS, items...)
}
// Find inodes that differ from our known state
var diffInos []uint64
seen := make(map[uint64]uint64) // ino -> meta_seq from current scan
for _, ms := range allMS {
seen[ms.Ino] = ms.Seq
}
// Check for new or changed inodes
for ino, metaSeq := range seen {
st, ok := known[ino]
if !ok || st.metaSeq != metaSeq {
diffInos = append(diffInos, ino)
}
}
// Check for removed inodes
for ino := range known {
if _, ok := seen[ino]; !ok {
diffInos = append(diffInos, ino)
}
}
if len(diffInos) > 0 {
// Sort and deduplicate (inos must be sorted for the ioctl)
sort.Slice(diffInos, func(i, j int) bool {
return diffInos[i] < diffInos[j]
})
results, err := scoutfs.RawReadInodeInfo(f, diffInos, xattrNames, resultsBuf)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading inode info: %v\n", err)
os.Exit(1)
}
// Build set of returned inos
returnedInos := make(map[uint64]bool)
for _, r := range results {
returnedInos[r.Ino] = true
}
// Remove inodes that weren't returned (nlink==0 or deleted)
for _, ino := range diffInos {
if !returnedInos[ino] {
if *debug {
fmt.Printf(" remove ino %d\n", ino)
}
delete(known, ino)
}
}
// Print header periodically
if lines%16 == 0 {
fmt.Printf("%8s %12s %12s %12s %6s %6s %6s %6s",
"ino", "meta_seq", "data_seq", "data_ver",
"nlink", "uid", "gid", "mode")
if len(xattrNames) > 0 {
fmt.Printf(" xattrs")
}
fmt.Println()
}
for _, r := range results {
action := "update"
if _, ok := known[r.Ino]; !ok {
action = "add"
}
known[r.Ino] = &inoState{metaSeq: r.Inode.Meta_seq}
fmt.Printf("%8d %12d %12d %12d %6d %6d %6d %6o",
r.Ino,
r.Inode.Meta_seq,
r.Inode.Data_seq,
r.Inode.Data_version,
r.Inode.Nlink,
r.Inode.Uid,
r.Inode.Gid,
r.Inode.Mode)
if len(r.Xattrs) > 0 {
for _, x := range r.Xattrs {
fmt.Printf(" %s=%q", x.Name, x.Value)
}
}
if *debug {
fmt.Printf(" [%s]", action)
}
fmt.Println()
lines++
}
}
// Update known state from current scan
for ino, metaSeq := range seen {
if st, ok := known[ino]; ok {
st.metaSeq = metaSeq
}
}
if *debug {
fmt.Printf("tracking %d inodes, inode result size %d\n",
len(known), unsafe.Sizeof(scoutfs.ScoutfsInode{}))
}
time.Sleep(1 * time.Second)
}
}
// stringSlice implements flag.Value for repeated string flags
type stringSlice []string
func (s *stringSlice) String() string { return fmt.Sprintf("%v", *s) }
func (s *stringSlice) Set(val string) error {
*s = append(*s, val)
return nil
}
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
#
# generate-defs.sh - Regenerate scoutfsdefs.go from scoutfs kernel headers
#
# This script uses Docker to build a Linux container with the scoutfs
# kernel headers installed, then runs "go tool cgo -godefs" to generate
# the Go type definitions from c_defs_linux.go.
#
# Usage:
# ./generate-defs.sh [BRANCH]
#
# Arguments:
# BRANCH The scoutfs git branch to use for headers (default: main)
#
# Examples:
# ./generate-defs.sh # use main branch
# ./generate-defs.sh zab/get_changed_inos # use a feature branch
#
set -euo pipefail
BRANCH="${1:-main}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
IMAGE_NAME="scoutfs-godefs"
echo "==> Building Docker image with scoutfs branch: ${BRANCH}"
docker build \
--build-arg "SCOUTFS_BRANCH=${BRANCH}" \
--no-cache \
-f "${SCRIPT_DIR}/Dockerfile.godefs" \
-t "${IMAGE_NAME}" \
"${SCRIPT_DIR}"
echo "==> Generating scoutfsdefs.go"
docker run --rm \
-v "${SCRIPT_DIR}:/output" \
"${IMAGE_NAME}"
echo "==> Verifying build"
cd "${SCRIPT_DIR}"
GOOS=linux go vet ./...
GOOS=linux go build ./...
echo "==> Done. scoutfsdefs.go has been regenerated from branch: ${BRANCH}"
+257
View File
@@ -1830,3 +1830,260 @@ func PunchHole(f *os.File, length, offset, version uint64) error {
_, err := scoutfsctl(f, IOCPUNCHOFFLINE, unsafe.Pointer(&po))
return err
}
// RawMetaSeqReader iterates over meta_seq items without cluster locking.
// Use NewRawMetaSeqReader to create a new reader.
type RawMetaSeqReader struct {
fsfd *os.File
start MetaSeqEntry
end MetaSeqEntry
last MetaSeqEntry
batch uint32
buf []byte
done bool
}
// RMSOption sets various options for NewRawMetaSeqReader
type RMSOption func(*RawMetaSeqReader)
// WithRMSBatchSize sets the max number of meta_seq items to be returned at a time
func WithRMSBatchSize(size uint32) RMSOption {
return func(r *RawMetaSeqReader) {
r.batch = size
}
}
// WithRMSRange sets the start and end range for the meta_seq reader
func WithRMSRange(start, end MetaSeqEntry) RMSOption {
return func(r *RawMetaSeqReader) {
r.start = start
r.end = end
}
}
// NewRawMetaSeqReader creates a new reader for raw meta_seq items.
// An open file within scoutfs is supplied for ioctls
// (usually just the base mount point directory).
func NewRawMetaSeqReader(f *os.File, opts ...RMSOption) *RawMetaSeqReader {
r := &RawMetaSeqReader{
fsfd: f,
batch: 1024,
end: MetaSeqEntry{Seq: max64, Ino: max64},
}
for _, opt := range opts {
opt(r)
}
r.buf = make([]byte, int(unsafe.Sizeof(MetaSeqEntry{}))*int(r.batch))
return r
}
// Next gets the next batch of meta_seq items.
// Returns nil, nil when iteration is complete.
func (r *RawMetaSeqReader) Next() ([]MetaSeqEntry, error) {
if r.done {
return nil, nil
}
rms := rawReadMetaSeq{
Start: r.start,
End: r.end,
Ptr: uint64(uintptr(unsafe.Pointer(&r.buf[0]))),
Size: r.batch,
}
n, err := scoutfsctl(r.fsfd, IOCRAWREADMETASEQ, unsafe.Pointer(&rms))
if err != nil {
return nil, err
}
r.last = rms.Last
if n == 0 {
r.done = true
return nil, nil
}
rbuf := bytes.NewReader(r.buf)
items := make([]MetaSeqEntry, n)
var e MetaSeqEntry
for i := 0; i < n; i++ {
err := binary.Read(rbuf, binary.LittleEndian, &e)
if err != nil {
return nil, err
}
items[i] = e
}
// Advance start past last for the next call
r.start = r.last
r.start.Ino++
if r.start.Ino == 0 {
r.start.Seq++
}
// If last reached the end of the range, we're done
if r.last.Seq == r.end.Seq && r.last.Ino == r.end.Ino {
r.done = true
}
return items, nil
}
// Last returns the last meta_seq position from the most recent Next() call.
// This is the last item that could have been returned by the kernel.
func (r *RawMetaSeqReader) Last() MetaSeqEntry {
return r.last
}
// Reset resets the reader to start from the beginning of the range
func (r *RawMetaSeqReader) Reset() {
r.start = MetaSeqEntry{}
r.last = MetaSeqEntry{}
r.done = false
}
// RawInodeResult contains the inode metadata and optional xattr values
// returned by RawReadInodeInfo for a single inode
type RawInodeResult struct {
Ino uint64
Inode ScoutfsInode
Xattrs []RawXattrResult
}
// RawXattrResult contains a single xattr name and value
type RawXattrResult struct {
Name string
Value []byte
}
// RawReadInodeInfo reads inode metadata (and optionally xattr values) for
// the given inode numbers without cluster locking.
//
// inos must be a sorted slice of unique non-zero inode numbers.
// xattrNames optionally specifies xattr names to return with each inode.
// resultsBuf is a caller-provided buffer for the ioctl results, allowing
// reuse across calls to avoid repeated allocations.
//
// An open file within scoutfs is supplied for ioctls
// (usually just the base mount point directory).
func RawReadInodeInfo(f *os.File, inos []uint64, xattrNames []string, resultsBuf []byte) ([]RawInodeResult, error) {
if len(inos) == 0 {
return nil, nil
}
// Build the null-terminated xattr names buffer
var namesBuf []byte
for _, name := range xattrNames {
namesBuf = append(namesBuf, []byte(name)...)
namesBuf = append(namesBuf, 0)
}
rii := rawReadInodeInfo{
Inos_ptr: uint64(uintptr(unsafe.Pointer(&inos[0]))),
Inos_count: uint32(len(inos)),
Results_ptr: uint64(uintptr(unsafe.Pointer(&resultsBuf[0]))),
Results_size: uint32(len(resultsBuf)),
}
if len(namesBuf) > 0 {
rii.Names_ptr = uint64(uintptr(unsafe.Pointer(&namesBuf[0])))
rii.Names_count = uint32(len(xattrNames))
}
n, err := scoutfsctl(f, IOCRAWREADINODEINFO, unsafe.Pointer(&rii))
if err != nil {
return nil, err
}
if n == 0 {
return nil, nil
}
return parseRawReadResults(resultsBuf[:n])
}
func parseRawReadResults(buf []byte) ([]RawInodeResult, error) {
var results []RawInodeResult
resultHdrSize := int(unsafe.Sizeof(RawReadResult{}))
off := 0
for off < len(buf) {
if off+resultHdrSize > len(buf) {
break
}
// Read result header - copy to aligned struct
var hdr RawReadResult
copy((*[12]byte)(unsafe.Pointer(&hdr))[:], buf[off:off+resultHdrSize])
off += resultHdrSize
if int(hdr.Size) > len(buf)-off {
return results, fmt.Errorf("result payload size %d exceeds remaining buffer %d", hdr.Size, len(buf)-off)
}
payload := buf[off : off+int(hdr.Size)]
off += int(hdr.Size)
switch hdr.Type {
case RAWREADRESULTINODE:
if len(payload) < 8 {
return results, fmt.Errorf("inode result too small: %d bytes", len(payload))
}
ino := binary.LittleEndian.Uint64(payload[:8])
inodeBytes := payload[8:]
var inode ScoutfsInode
inodeSize := int(unsafe.Sizeof(inode))
// Handle potentially smaller inode structs from older format versions
readSize := len(inodeBytes)
if readSize > inodeSize {
readSize = inodeSize
}
err := binary.Read(
bytes.NewReader(inodeBytes[:readSize]),
binary.LittleEndian,
(*[168]byte)(unsafe.Pointer(&inode))[:readSize],
)
if err != nil {
return results, fmt.Errorf("parse inode %d: %v", ino, err)
}
results = append(results, RawInodeResult{
Ino: ino,
Inode: inode,
})
case RAWREADRESULTXATTR:
if len(results) == 0 {
return results, fmt.Errorf("xattr result without preceding inode result")
}
// Find null terminator separating name from value
nullIdx := bytes.IndexByte(payload, 0)
if nullIdx < 0 {
return results, fmt.Errorf("xattr result missing null terminator")
}
name := string(payload[:nullIdx])
value := make([]byte, len(payload)-nullIdx-1)
copy(value, payload[nullIdx+1:])
last := &results[len(results)-1]
last.Xattrs = append(last.Xattrs, RawXattrResult{
Name: name,
Value: value,
})
default:
// Skip unknown result types for forward compatibility
}
}
return results, nil
}
+58
View File
@@ -52,6 +52,11 @@ const IOCIAXBITS = 0x100
const IOCGETATTRX = 0x4068e812
const IOCSETATTRX = 0x4068e813
const IOCPUNCHOFFLINE = 0x4020e818
const IOCRAWREADMETASEQ = 0x8040e819
const IOCRAWREADINODEINFO = 0x8028e819
const RAWREADRESULTINODE = 0x1
const RAWREADRESULTXATTR = 0x2
type InodesEntry struct {
Major uint64
@@ -248,6 +253,59 @@ type punchOffline struct {
Version uint64
Flags uint64
}
type MetaSeqEntry struct {
Seq uint64
Ino uint64
}
type rawReadMetaSeq struct {
Start MetaSeqEntry
End MetaSeqEntry
Last MetaSeqEntry
Ptr uint64
Size uint32
X_pad uint32
}
type rawReadInodeInfo struct {
Inos_ptr uint64
Inos_count uint32
Names_count uint32
Names_ptr uint64
Results_ptr uint64
Results_size uint32
X_pad [4]uint8
}
type RawReadResult struct {
Size uint32
X_pad [7]uint8
Type uint8
}
type ScoutfsInode struct {
Size uint64
Meta_seq uint64
Data_seq uint64
Data_version uint64
Online_blocks uint64
Offline_blocks uint64
Next_readdir_pos uint64
Next_xattr_id uint64
Version uint64
Nlink uint32
Uid uint32
Gid uint32
Mode uint32
Rdev uint32
Flags uint32
Atime ScoutfsTimespec
Ctime ScoutfsTimespec
Mtime ScoutfsTimespec
Crtime ScoutfsTimespec
Proj uint64
}
type ScoutfsTimespec struct {
Sec uint64
Nsec uint32
X__pad [4]uint8
}
const sizeofstatfsMore = 0x30
const sizeofxattrTotal = 0x28