All checks were successful
		
		
	
	Test / Create distribution (push) Successful in 26s
				
			Test / Sandbox (push) Successful in 42s
				
			Test / Sandbox (race detector) (push) Successful in 42s
				
			Test / Hakurei (push) Successful in 46s
				
			Test / Hakurei (race detector) (push) Successful in 46s
				
			Test / Hpkg (push) Successful in 42s
				
			Test / Flake checks (push) Successful in 1m30s
				
			The handle is otherwise inaccessible without the compat interface. This change also moves compatibility methods to separate adapter structs to avoid inadvertently using them. Signed-off-by: Ophestra <cat@gensokyo.uk>
		
			
				
	
	
		
			194 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			194 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package store
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"iter"
 | 
						|
	"os"
 | 
						|
	"strconv"
 | 
						|
	"sync"
 | 
						|
 | 
						|
	"hakurei.app/container/check"
 | 
						|
	"hakurei.app/hst"
 | 
						|
	"hakurei.app/internal/lockedfile"
 | 
						|
)
 | 
						|
 | 
						|
// EntryHandle is a handle on a state entry retrieved from a [Handle].
 | 
						|
// Must only be used while its parent [Handle.Lock] is held.
 | 
						|
type EntryHandle struct {
 | 
						|
	// Error returned while decoding pathname.
 | 
						|
	// A non-nil value disables EntryHandle.
 | 
						|
	DecodeErr error
 | 
						|
 | 
						|
	// Checked pathname to entry file.
 | 
						|
	Pathname *check.Absolute
 | 
						|
 | 
						|
	hst.ID
 | 
						|
}
 | 
						|
 | 
						|
// open opens the underlying state entry file.
 | 
						|
// A non-nil error returned by open is of type [hst.AppError].
 | 
						|
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
 | 
						|
	if eh.DecodeErr != nil {
 | 
						|
		return nil, eh.DecodeErr
 | 
						|
	}
 | 
						|
 | 
						|
	if f, err := os.OpenFile(eh.Pathname.String(), flag, perm); err != nil {
 | 
						|
		return nil, &hst.AppError{Step: "open state entry", Err: err}
 | 
						|
	} else {
 | 
						|
		return f, nil
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Destroy removes the underlying state entry.
 | 
						|
// A non-nil error returned by Destroy is of type [hst.AppError].
 | 
						|
func (eh *EntryHandle) Destroy() error {
 | 
						|
	// destroy does not go through open
 | 
						|
	if eh.DecodeErr != nil {
 | 
						|
		return eh.DecodeErr
 | 
						|
	}
 | 
						|
 | 
						|
	if err := os.Remove(eh.Pathname.String()); err != nil {
 | 
						|
		return &hst.AppError{Step: "destroy state entry", Err: err}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// save encodes [hst.State] and writes it to the underlying file.
 | 
						|
// An error is returned if a file already exists with the same identifier.
 | 
						|
// save does not validate the embedded [hst.Config].
 | 
						|
// A non-nil error returned by save is of type [hst.AppError].
 | 
						|
func (eh *EntryHandle) save(state *hst.State) error {
 | 
						|
	f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	err = entryEncode(f, state)
 | 
						|
	if closeErr := f.Close(); closeErr != nil && err == nil {
 | 
						|
		err = &hst.AppError{Step: "close state file", Err: closeErr}
 | 
						|
	}
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// Load loads and validates the state entry header, and returns the [hst.Enablement] byte.
 | 
						|
// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v.
 | 
						|
// Load validates the embedded [hst.Config] value.
 | 
						|
// A non-nil error returned by Load is of type [hst.AppError].
 | 
						|
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
 | 
						|
	f, err := eh.open(os.O_RDONLY, 0)
 | 
						|
	if err != nil {
 | 
						|
		return 0, err
 | 
						|
	}
 | 
						|
 | 
						|
	var et hst.Enablement
 | 
						|
	if v != nil {
 | 
						|
		et, err = entryDecode(f, v)
 | 
						|
		if err == nil && v.ID != eh.ID {
 | 
						|
			err = &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
 | 
						|
				Msg: fmt.Sprintf("state entry %s has unexpected id %s", eh.ID.String(), v.ID.String())}
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		et, err = entryDecodeHeader(f)
 | 
						|
	}
 | 
						|
 | 
						|
	if closeErr := f.Close(); closeErr != nil && err == nil {
 | 
						|
		err = &hst.AppError{Step: "close state file", Err: closeErr}
 | 
						|
	}
 | 
						|
	return et, err
 | 
						|
}
 | 
						|
 | 
						|
// Handle is a handle on a [Store] segment.
 | 
						|
// Initialised by [Store.Handle].
 | 
						|
type Handle struct {
 | 
						|
	// Identity of instances tracked by this segment.
 | 
						|
	Identity int
 | 
						|
	// Pathname of directory that the segment referred to by Handle is rooted in.
 | 
						|
	Path *check.Absolute
 | 
						|
 | 
						|
	// Inter-process mutex to synchronise operations against resources in this segment.
 | 
						|
	// Must not be held directly, callers should use [Handle.Lock] instead.
 | 
						|
	fileMu *lockedfile.Mutex
 | 
						|
	// Must be held alongside fileMu.
 | 
						|
	mu sync.Mutex
 | 
						|
}
 | 
						|
 | 
						|
// Lock attempts to acquire a lock on [Handle].
 | 
						|
// If successful, Lock returns a non-nil unlock function.
 | 
						|
// A non-nil error returned by Lock is of type [hst.AppError].
 | 
						|
func (h *Handle) Lock() (unlock func(), err error) {
 | 
						|
	if unlock, err = h.fileMu.Lock(); err != nil {
 | 
						|
		return nil, &hst.AppError{Step: "acquire lock on store segment " + strconv.Itoa(h.Identity), Err: err}
 | 
						|
	}
 | 
						|
	return
 | 
						|
}
 | 
						|
 | 
						|
// Save attempts to save [hst.State] as a segment entry, and returns its [EntryHandle].
 | 
						|
// Must be called while holding [Handle.Lock].
 | 
						|
// An error is returned if an entry already exists with the same identifier.
 | 
						|
// Save does not validate the embedded [hst.Config].
 | 
						|
// A non-nil error returned by Save is of type [hst.AppError].
 | 
						|
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
 | 
						|
	eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
 | 
						|
	return &eh, eh.save(state)
 | 
						|
}
 | 
						|
 | 
						|
// Entries returns an iterator over all [EntryHandle] held in this segment.
 | 
						|
// Must be called while holding [Handle.Lock].
 | 
						|
// A non-nil error attached to a [EntryHandle] indicates a malformed identifier and is of type [hst.AppError].
 | 
						|
// A non-nil error returned by Entries is of type [hst.AppError].
 | 
						|
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
 | 
						|
	// for error reporting
 | 
						|
	const step = "read store segment entries"
 | 
						|
 | 
						|
	// read directory contents, should only contain storeMutexName and identifier
 | 
						|
	var entries []os.DirEntry
 | 
						|
	if pl, err := os.ReadDir(h.Path.String()); err != nil {
 | 
						|
		return nil, -1, &hst.AppError{Step: step, Err: err}
 | 
						|
	} else {
 | 
						|
		entries = pl
 | 
						|
	}
 | 
						|
 | 
						|
	// expects lock file
 | 
						|
	l := len(entries)
 | 
						|
	if l > 0 {
 | 
						|
		l--
 | 
						|
	}
 | 
						|
 | 
						|
	return func(yield func(*EntryHandle) bool) {
 | 
						|
		for _, ent := range entries {
 | 
						|
			var eh = EntryHandle{Pathname: h.Path.Append(ent.Name())}
 | 
						|
 | 
						|
			// this should never happen
 | 
						|
			if ent.IsDir() {
 | 
						|
				eh.DecodeErr = &hst.AppError{Step: step,
 | 
						|
					Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")}
 | 
						|
				goto out
 | 
						|
			}
 | 
						|
 | 
						|
			// silently skip lock file
 | 
						|
			if ent.Name() == MutexName {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			// this either indicates a serious bug or external interference
 | 
						|
			if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil {
 | 
						|
				eh.DecodeErr = &hst.AppError{Step: "decode store segment entry", Err: err}
 | 
						|
				goto out
 | 
						|
			}
 | 
						|
 | 
						|
		out:
 | 
						|
			if !yield(&eh) {
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}, l, nil
 | 
						|
}
 | 
						|
 | 
						|
// newHandle returns the address of a new segment [Handle] rooted in base.
 | 
						|
func newHandle(base *check.Absolute, identity int) *Handle {
 | 
						|
	h := Handle{Identity: identity, Path: base.Append(strconv.Itoa(identity))}
 | 
						|
	h.fileMu = lockedfile.MutexAt(h.Path.Append(MutexName).String())
 | 
						|
	return &h
 | 
						|
}
 |