dsfx/internal/sim/disk.go

311 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sim
import (
"errors"
"io"
"io/fs"
"math/rand"
"sync"
"time"
"git.numenor-labs.us/dsfx/internal/lib/disk"
)
// Tolerance defines simulation tolerance parameters.
type Tolerance struct {
// Latency to wait before executing an operation.
Latency time.Duration
// FailureChance is the probability (0.01.0) that an operation fails.
FailureChance float64
// CorruptionChance is the probability (0.01.0) that a write is corrupted.
CorruptionChance float64
}
// simEntry represents an entry in our inmemory file system.
type simEntry struct {
name string
isDir bool
perm fs.FileMode
modTime time.Time
// For files, data holds the file contents.
data []byte
mu sync.Mutex
}
// simFile is a simulated file object. It implements fs.File and io.Writer.
// The same underlying simEntry is shared among all handles to a given file.
type simFile struct {
entry *simEntry
// offset to simulate reading sequentially.
offset int
// readOnly indicates whether writes are allowed.
readOnly bool
// closed is set once Close() has been called.
closed bool
// You could add a mutex here if you want to protect concurrent access per handle.
mu sync.Mutex
// tol copied from the SimDisk for perfile simulation.
tol Tolerance
}
// Ensure simFile implements the interfaces.
var _ disk.File = (*simFile)(nil)
// Read reads from the simulated file starting at the current offset.
func (sf *simFile) Read(p []byte) (int, error) {
sf.mu.Lock()
defer sf.mu.Unlock()
if sf.closed {
return 0, errors.New("read from closed file")
}
// Lock the underlying data.
sf.entry.mu.Lock()
defer sf.entry.mu.Unlock()
if sf.offset >= len(sf.entry.data) {
return 0, io.EOF
}
n := copy(p, sf.entry.data[sf.offset:])
sf.offset += n
return n, nil
}
// Write writes the given bytes at the end of the file (for a writable file).
// It may simulate corruption.
func (sf *simFile) Write(p []byte) (int, error) {
if sf.readOnly {
return 0, errors.New("cannot write to read-only file")
}
sf.mu.Lock()
defer sf.mu.Unlock()
if sf.closed {
return 0, errors.New("write to closed file")
}
// simulate latency
time.Sleep(sf.tol.Latency)
// simulate failure
if rand.Float64() < sf.tol.FailureChance {
return 0, errors.New("simulated write failure")
}
// prepare data to write
dataToWrite := make([]byte, len(p))
copy(dataToWrite, p)
// simulate corruption by flipping bits in the data (if triggered)
if rand.Float64() < sf.tol.CorruptionChance {
for i := range dataToWrite {
dataToWrite[i] = ^dataToWrite[i] // simple corruption: bitwise complement
}
}
// lock the underlying entry and append data
sf.entry.mu.Lock()
defer sf.entry.mu.Unlock()
sf.entry.data = append(sf.entry.data, dataToWrite...)
// update modification time
sf.entry.modTime = time.Now()
return len(p), nil
}
// Close marks the file as closed.
func (sf *simFile) Close() error {
sf.mu.Lock()
defer sf.mu.Unlock()
if sf.closed {
return errors.New("file already closed")
}
sf.closed = true
return nil
}
// Stat returns file information for the simulated file.
func (sf *simFile) Stat() (fs.FileInfo, error) {
sf.mu.Lock()
defer sf.mu.Unlock()
if sf.closed {
return nil, errors.New("stat on closed file")
}
return &simFileInfo{entry: sf.entry}, nil
}
// simFileInfo implements fs.FileInfo for a simEntry.
type simFileInfo struct {
entry *simEntry
}
var _ fs.FileInfo = (*simFileInfo)(nil)
func (fi *simFileInfo) Name() string { return fi.entry.name }
func (fi *simFileInfo) Size() int64 {
fi.entry.mu.Lock()
defer fi.entry.mu.Unlock()
return int64(len(fi.entry.data))
}
func (fi *simFileInfo) Mode() fs.FileMode { return fi.entry.perm }
func (fi *simFileInfo) ModTime() time.Time { return fi.entry.modTime }
func (fi *simFileInfo) IsDir() bool { return fi.entry.isDir }
func (fi *simFileInfo) Sys() interface{} { return nil }
// SimDisk is our inmemory simulation that implements the disk.Disk interface.
type SimDisk struct {
// tol holds the tolerance parameters to simulate latency, failures, corruption.
tol Tolerance
// entries is a map from file or directory name to its corresponding simEntry.
entries map[string]*simEntry
mu sync.Mutex
}
// NewSimDisk returns a new simulated disk with the given tolerance parameters.
func NewSimDisk(tol Tolerance) disk.Disk {
return &SimDisk{
tol: tol,
entries: make(map[string]*simEntry),
}
}
// simulateOp sleeps for the configured latency and then returns an error if a failure is simulated.
func (sd *SimDisk) simulateOp() error {
time.Sleep(sd.tol.Latency)
if rand.Float64() < sd.tol.FailureChance {
return errors.New("simulated disk operation failure")
}
return nil
}
// Mkdir creates a directory in the simulated file system.
func (sd *SimDisk) Mkdir(name string, perm fs.FileMode) error {
if err := sd.simulateOp(); err != nil {
return err
}
sd.mu.Lock()
defer sd.mu.Unlock()
if _, exists := sd.entries[name]; exists {
return errors.New("directory already exists")
}
sd.entries[name] = &simEntry{
name: name,
isDir: true,
perm: perm,
modTime: time.Now(),
}
return nil
}
// MkdirAll creates a directory and any necessary parent directories in the simulated file system.
// It behaves like os.MkdirAll.
// If the directory already exists, it does nothing.
func (sd *SimDisk) MkdirAll(name string, perm fs.FileMode) error {
if err := sd.simulateOp(); err != nil {
return err
}
sd.mu.Lock()
defer sd.mu.Unlock()
if _, exists := sd.entries[name]; exists {
return errors.New("directory already exists")
}
sd.entries[name] = &simEntry{
name: name,
isDir: true,
perm: perm,
modTime: time.Now(),
}
return nil
}
// Create creates (or truncates) a file in the simulated file system and returns a writable file.
func (sd *SimDisk) Create(name string) (disk.File, error) {
if err := sd.simulateOp(); err != nil {
return nil, err
}
sd.mu.Lock()
// For simplicity, Create always truncates (or creates new)
se := &simEntry{
name: name,
isDir: false,
perm: 0644,
modTime: time.Now(),
data: []byte{},
}
sd.entries[name] = se
sd.mu.Unlock()
// Return a writable file handle
return &simFile{
entry: se,
offset: 0,
readOnly: false,
tol: sd.tol,
}, nil
}
// Open opens an existing file in the simulated file system.
// The returned file is "read-only" (write attempts will error),
func (sd *SimDisk) Open(name string) (disk.File, error) {
if err := sd.simulateOp(); err != nil {
return nil, err
}
sd.mu.Lock()
se, exists := sd.entries[name]
sd.mu.Unlock()
if !exists {
return nil, errors.New("file does not exist")
}
if se.isDir {
return nil, errors.New("cannot open directory")
}
// Return a new file handle starting at offset 0; marked as readOnly.
return &simFile{
entry: se,
offset: 0,
readOnly: true,
tol: sd.tol,
}, nil
}
// Stat returns information about the file or directory.
func (sd *SimDisk) Stat(name string) (fs.FileInfo, error) {
if err := sd.simulateOp(); err != nil {
return nil, err
}
sd.mu.Lock()
se, exists := sd.entries[name]
sd.mu.Unlock()
if !exists {
return nil, errors.New("no such file or directory")
}
return &simFileInfo{entry: se}, nil
}
// Remove deletes a file or directory from the simulated file system.
func (sd *SimDisk) Remove(name string) error {
if err := sd.simulateOp(); err != nil {
return err
}
sd.mu.Lock()
defer sd.mu.Unlock()
if _, exists := sd.entries[name]; !exists {
return errors.New("file or directory does not exist")
}
delete(sd.entries, name)
return nil
}