internal/store: export new interface
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Test / Create distribution (push) Successful in 33s
				
			
		
			
				
	
				Test / Sandbox (push) Successful in 2m19s
				
			
		
			
				
	
				Test / Hakurei (push) Successful in 3m13s
				
			
		
			
				
	
				Test / Hpkg (push) Successful in 4m4s
				
			
		
			
				
	
				Test / Sandbox (race detector) (push) Successful in 4m16s
				
			
		
			
				
	
				Test / Hakurei (race detector) (push) Successful in 4m58s
				
			
		
			
				
	
				Test / Flake checks (push) Successful in 1m30s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Test / Create distribution (push) Successful in 33s
				
			Test / Sandbox (push) Successful in 2m19s
				
			Test / Hakurei (push) Successful in 3m13s
				
			Test / Hpkg (push) Successful in 4m4s
				
			Test / Sandbox (race detector) (push) Successful in 4m16s
				
			Test / Hakurei (race detector) (push) Successful in 4m58s
				
			Test / Flake checks (push) Successful in 1m30s
				
			This exposes store operations safe for direct access, and enables #19 to be implemented in internal/outcome. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
		
							parent
							
								
									b25ade5f3d
								
							
						
					
					
						commit
						b667fea1cb
					
				| @ -3,7 +3,6 @@ package store | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"maps" | 	"maps" | ||||||
| 	"strconv" |  | ||||||
| 
 | 
 | ||||||
| 	"hakurei.app/container/check" | 	"hakurei.app/container/check" | ||||||
| 	"hakurei.app/hst" | 	"hakurei.app/hst" | ||||||
| @ -23,29 +22,29 @@ type Compat interface { | |||||||
| 	List() (identities []int, err error) | 	List() (identities []int, err error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *stateStore) Do(identity int, f func(c Cursor)) (bool, error) { | func (s *Store) Do(identity int, f func(c Cursor)) (bool, error) { | ||||||
| 	if h, err := s.identityHandle(identity); err != nil { | 	if h, err := s.Handle(identity); err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} else { | 	} else { | ||||||
| 		return h.do(f) | 		return h.do(f) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // storeAdapter satisfies [Store] via stateStore. | // storeAdapter satisfies [Compat] via [Store]. | ||||||
| type storeAdapter struct { | type storeAdapter struct { | ||||||
| 	msg message.Msg | 	msg message.Msg | ||||||
| 	*stateStore | 	*Store | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s storeAdapter) List() ([]int, error) { | func (s storeAdapter) List() ([]int, error) { | ||||||
| 	segments, n, err := s.segments() | 	segments, n, err := s.Segments() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	identities := make([]int, 0, n) | 	identities := make([]int, 0, n) | ||||||
| 	for si := range segments { | 	for si := range segments { | ||||||
| 		if si.err != nil { | 		if si.Err != nil { | ||||||
| 			if m, ok := message.GetMessage(err); ok { | 			if m, ok := message.GetMessage(err); ok { | ||||||
| 				s.msg.Verbose(m) | 				s.msg.Verbose(m) | ||||||
| 			} else { | 			} else { | ||||||
| @ -54,14 +53,14 @@ func (s storeAdapter) List() ([]int, error) { | |||||||
| 			} | 			} | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		identities = append(identities, si.identity) | 		identities = append(identities, si.Identity) | ||||||
| 	} | 	} | ||||||
| 	return identities, nil | 	return identities, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewMulti returns an instance of the multi-file store. | // NewMulti returns an instance of the multi-file store. | ||||||
| func NewMulti(msg message.Msg, prefix *check.Absolute) Compat { | func NewMulti(msg message.Msg, prefix *check.Absolute) Compat { | ||||||
| 	return storeAdapter{msg, newStore(prefix.Append("state"))} | 	return storeAdapter{msg, New(prefix.Append("state"))} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Cursor provides access to the store of an identity. | // Cursor provides access to the store of an identity. | ||||||
| @ -72,10 +71,10 @@ type Cursor interface { | |||||||
| 	Len() (int, error) | 	Len() (int, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // do implements stateStore.Do on storeHandle. | // do implements [Compat.Do] on [Handle]. | ||||||
| func (h *storeHandle) do(f func(c Cursor)) (bool, error) { | func (h *Handle) do(f func(c Cursor)) (bool, error) { | ||||||
| 	if unlock, err := h.fileMu.Lock(); err != nil { | 	if unlock, err := h.Lock(); err != nil { | ||||||
| 		return false, &hst.AppError{Step: "acquire lock on store segment " + strconv.Itoa(h.identity), Err: err} | 		return false, err | ||||||
| 	} else { | 	} else { | ||||||
| 		defer unlock() | 		defer unlock() | ||||||
| 	} | 	} | ||||||
| @ -86,28 +85,28 @@ func (h *storeHandle) do(f func(c Cursor)) (bool, error) { | |||||||
| 
 | 
 | ||||||
| /* these compatibility methods must only be called while fileMu is held */ | /* these compatibility methods must only be called while fileMu is held */ | ||||||
| 
 | 
 | ||||||
| func (h *storeHandle) Save(state *hst.State) error { | func (h *Handle) Save(state *hst.State) error { | ||||||
| 	return (&stateEntryHandle{nil, h.path.Append(state.ID.String()), state.ID}).save(state) | 	return (&EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}).Save(state) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (h *storeHandle) Destroy(id hst.ID) error { | func (h *Handle) Destroy(id hst.ID) error { | ||||||
| 	return (&stateEntryHandle{nil, h.path.Append(id.String()), id}).destroy() | 	return (&EntryHandle{nil, h.Path.Append(id.String()), id}).Destroy() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (h *storeHandle) Load() (map[hst.ID]*hst.State, error) { | func (h *Handle) Load() (map[hst.ID]*hst.State, error) { | ||||||
| 	entries, n, err := h.entries() | 	entries, n, err := h.Entries() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := make(map[hst.ID]*hst.State, n) | 	r := make(map[hst.ID]*hst.State, n) | ||||||
| 	for eh := range entries { | 	for eh := range entries { | ||||||
| 		if eh.decodeErr != nil { | 		if eh.DecodeErr != nil { | ||||||
| 			err = eh.decodeErr | 			err = eh.DecodeErr | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 		var s hst.State | 		var s hst.State | ||||||
| 		if _, err = eh.load(&s); err != nil { | 		if _, err = eh.Load(&s); err != nil { | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 		r[eh.ID] = &s | 		r[eh.ID] = &s | ||||||
| @ -115,16 +114,16 @@ func (h *storeHandle) Load() (map[hst.ID]*hst.State, error) { | |||||||
| 	return r, err | 	return r, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (h *storeHandle) Len() (int, error) { | func (h *Handle) Len() (int, error) { | ||||||
| 	entries, _, err := h.entries() | 	entries, _, err := h.Entries() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return -1, err | 		return -1, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var n int | 	var n int | ||||||
| 	for eh := range entries { | 	for eh := range entries { | ||||||
| 		if eh.decodeErr != nil { | 		if eh.DecodeErr != nil { | ||||||
| 			err = eh.decodeErr | 			err = eh.DecodeErr | ||||||
| 		} | 		} | ||||||
| 		n++ | 		n++ | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -13,49 +13,52 @@ import ( | |||||||
| 	"hakurei.app/internal/lockedfile" | 	"hakurei.app/internal/lockedfile" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // stateEntryHandle is a handle on a state entry retrieved from a storeHandle. | // EntryHandle is a handle on a state entry retrieved from a [Handle]. | ||||||
| // Must only be used while its parent storeHandle.fileMu is held. | // Must only be used while its parent [Handle.Lock] is held. | ||||||
| type stateEntryHandle struct { | type EntryHandle struct { | ||||||
| 	// Error returned while decoding pathname. | 	// Error returned while decoding pathname. | ||||||
| 	// A non-nil value disables stateEntryHandle. | 	// A non-nil value disables EntryHandle. | ||||||
| 	decodeErr error | 	DecodeErr error | ||||||
| 
 | 
 | ||||||
| 	// Checked path to entry file. | 	// Checked pathname to entry file. | ||||||
| 	pathname *check.Absolute | 	Pathname *check.Absolute | ||||||
| 
 | 
 | ||||||
| 	hst.ID | 	hst.ID | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // open opens the underlying state entry file, returning [hst.AppError] for a non-nil error. | // open opens the underlying state entry file. | ||||||
| func (eh *stateEntryHandle) open(flag int, perm os.FileMode) (*os.File, error) { | // A non-nil error returned by open is of type [hst.AppError]. | ||||||
| 	if eh.decodeErr != nil { | func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) { | ||||||
| 		return nil, eh.decodeErr | 	if eh.DecodeErr != nil { | ||||||
|  | 		return nil, eh.DecodeErr | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if f, err := os.OpenFile(eh.pathname.String(), flag, perm); err != nil { | 	if f, err := os.OpenFile(eh.Pathname.String(), flag, perm); err != nil { | ||||||
| 		return nil, &hst.AppError{Step: "open state entry", Err: err} | 		return nil, &hst.AppError{Step: "open state entry", Err: err} | ||||||
| 	} else { | 	} else { | ||||||
| 		return f, nil | 		return f, nil | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // destroy removes the underlying state entry file, returning [hst.AppError] for a non-nil error. | // Destroy removes the underlying state entry. | ||||||
| func (eh *stateEntryHandle) destroy() error { | // A non-nil error returned by Destroy is of type [hst.AppError]. | ||||||
|  | func (eh *EntryHandle) Destroy() error { | ||||||
| 	// destroy does not go through open | 	// destroy does not go through open | ||||||
| 	if eh.decodeErr != nil { | 	if eh.DecodeErr != nil { | ||||||
| 		return eh.decodeErr | 		return eh.DecodeErr | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := os.Remove(eh.pathname.String()); err != nil { | 	if err := os.Remove(eh.Pathname.String()); err != nil { | ||||||
| 		return &hst.AppError{Step: "destroy state entry", Err: err} | 		return &hst.AppError{Step: "destroy state entry", Err: err} | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // save encodes [hst.State] and writes it to the underlying file. | // Save encodes [hst.State] and writes it to the underlying file. | ||||||
| // An error is returned if a file already exists with the same identifier. | // An error is returned if a file already exists with the same identifier. | ||||||
| // save does not validate the embedded [hst.Config]. | // Save does not validate the embedded [hst.Config]. | ||||||
| func (eh *stateEntryHandle) save(state *hst.State) error { | // 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) | 	f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -68,10 +71,11 @@ func (eh *stateEntryHandle) save(state *hst.State) error { | |||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // load loads and validates the state entry header, and returns the [hst.Enablement] byte. | // 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. | // 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. | // Load validates the embedded [hst.Config] value. | ||||||
| func (eh *stateEntryHandle) load(v *hst.State) (hst.Enablement, error) { | // 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) | 	f, err := eh.open(os.O_RDONLY, 0) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, err | 		return 0, err | ||||||
| @ -94,31 +98,42 @@ func (eh *stateEntryHandle) load(v *hst.State) (hst.Enablement, error) { | |||||||
| 	return et, err | 	return et, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // storeHandle is a handle on a stateStore segment. | // Handle is a handle on a [Store] segment. | ||||||
| // Initialised by stateStore.identityHandle. | // Initialised by [Store.Handle]. | ||||||
| type storeHandle struct { | type Handle struct { | ||||||
| 	// Identity of instances tracked by this segment. | 	// Identity of instances tracked by this segment. | ||||||
| 	identity int | 	Identity int | ||||||
| 	// Pathname of directory that the segment referred to by storeHandle is rooted in. | 	// Pathname of directory that the segment referred to by Handle is rooted in. | ||||||
| 	path *check.Absolute | 	Path *check.Absolute | ||||||
| 	// Inter-process mutex to synchronise operations against resources in this segment. |  | ||||||
| 	fileMu *lockedfile.Mutex |  | ||||||
| 
 | 
 | ||||||
|  | 	// 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. | 	// Must be held alongside fileMu. | ||||||
| 	mu sync.Mutex | 	mu sync.Mutex | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // entries returns an iterator over all stateEntryHandle held in this segment. | // Lock attempts to acquire a lock on [Handle]. | ||||||
| // Must be called while holding a lock on mu and fileMu. | // If successful, Lock returns a non-nil unlock function. | ||||||
| // A non-nil error attached to a stateEntryHandle indicates a malformed identifier and is of type [hst.AppError]. | // A non-nil error returned by Lock is of type [hst.AppError]. | ||||||
| // A non-nil error returned by entries is of type [hst.AppError]. | func (h *Handle) Lock() (unlock func(), err error) { | ||||||
| func (h *storeHandle) entries() (iter.Seq[*stateEntryHandle], int, 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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 | 	// for error reporting | ||||||
| 	const step = "read store segment entries" | 	const step = "read store segment entries" | ||||||
| 
 | 
 | ||||||
| 	// read directory contents, should only contain storeMutexName and identifier | 	// read directory contents, should only contain storeMutexName and identifier | ||||||
| 	var entries []os.DirEntry | 	var entries []os.DirEntry | ||||||
| 	if pl, err := os.ReadDir(h.path.String()); err != nil { | 	if pl, err := os.ReadDir(h.Path.String()); err != nil { | ||||||
| 		return nil, -1, &hst.AppError{Step: step, Err: err} | 		return nil, -1, &hst.AppError{Step: step, Err: err} | ||||||
| 	} else { | 	} else { | ||||||
| 		entries = pl | 		entries = pl | ||||||
| @ -130,25 +145,25 @@ func (h *storeHandle) entries() (iter.Seq[*stateEntryHandle], int, error) { | |||||||
| 		l-- | 		l-- | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return func(yield func(*stateEntryHandle) bool) { | 	return func(yield func(*EntryHandle) bool) { | ||||||
| 		for _, ent := range entries { | 		for _, ent := range entries { | ||||||
| 			var eh = stateEntryHandle{pathname: h.path.Append(ent.Name())} | 			var eh = EntryHandle{Pathname: h.Path.Append(ent.Name())} | ||||||
| 
 | 
 | ||||||
| 			// this should never happen | 			// this should never happen | ||||||
| 			if ent.IsDir() { | 			if ent.IsDir() { | ||||||
| 				eh.decodeErr = &hst.AppError{Step: step, | 				eh.DecodeErr = &hst.AppError{Step: step, | ||||||
| 					Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")} | 					Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")} | ||||||
| 				goto out | 				goto out | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// silently skip lock file | 			// silently skip lock file | ||||||
| 			if ent.Name() == storeMutexName { | 			if ent.Name() == MutexName { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// this either indicates a serious bug or external interference | 			// this either indicates a serious bug or external interference | ||||||
| 			if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil { | 			if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil { | ||||||
| 				eh.decodeErr = &hst.AppError{Step: "decode store segment entry", Err: err} | 				eh.DecodeErr = &hst.AppError{Step: "decode store segment entry", Err: err} | ||||||
| 				goto out | 				goto out | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| @ -159,3 +174,10 @@ func (h *storeHandle) entries() (iter.Seq[*stateEntryHandle], int, error) { | |||||||
| 		} | 		} | ||||||
| 	}, l, nil | 	}, 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 | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| package store | package store_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"io" | ||||||
| 	"iter" | 	"iter" | ||||||
| 	"os" | 	"os" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| @ -9,31 +10,44 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	_ "unsafe" | ||||||
| 
 | 
 | ||||||
| 	"hakurei.app/container/check" | 	"hakurei.app/container/check" | ||||||
| 	"hakurei.app/container/stub" | 	"hakurei.app/container/stub" | ||||||
| 	"hakurei.app/hst" | 	"hakurei.app/hst" | ||||||
| 	"hakurei.app/internal/lockedfile" | 	"hakurei.app/internal/store" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | //go:linkname newTemplateState hakurei.app/internal/store.newTemplateState | ||||||
|  | func newTemplateState() *hst.State | ||||||
|  | 
 | ||||||
|  | //go:linkname entryDecode hakurei.app/internal/store.entryDecode | ||||||
|  | func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) | ||||||
|  | 
 | ||||||
|  | //go:linkname newHandle hakurei.app/internal/store.newHandle | ||||||
|  | func newHandle(base *check.Absolute, identity int) *store.Handle | ||||||
|  | 
 | ||||||
|  | //go:linkname open hakurei.app/internal/store.(*EntryHandle).open | ||||||
|  | func open(eh *store.EntryHandle, flag int, perm os.FileMode) (*os.File, error) | ||||||
|  | 
 | ||||||
| func TestStateEntryHandle(t *testing.T) { | func TestStateEntryHandle(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
| 
 | 
 | ||||||
| 	t.Run("lockout", func(t *testing.T) { | 	t.Run("lockout", func(t *testing.T) { | ||||||
| 		t.Parallel() | 		t.Parallel() | ||||||
| 		wantErr := func() error { return stub.UniqueError(0) } | 		wantErr := func() error { return stub.UniqueError(0) } | ||||||
| 		eh := stateEntryHandle{decodeErr: wantErr(), pathname: check.MustAbs("/proc/nonexistent")} | 		eh := store.EntryHandle{DecodeErr: wantErr(), Pathname: check.MustAbs("/proc/nonexistent")} | ||||||
| 
 | 
 | ||||||
| 		if _, err := eh.open(-1, 0); !reflect.DeepEqual(err, wantErr()) { | 		if _, err := open(&eh, -1, 0); !reflect.DeepEqual(err, wantErr()) { | ||||||
| 			t.Errorf("open: error = %v, want %v", err, wantErr()) | 			t.Errorf("open: error = %v, want %v", err, wantErr()) | ||||||
| 		} | 		} | ||||||
| 		if err := eh.destroy(); !reflect.DeepEqual(err, wantErr()) { | 		if err := eh.Destroy(); !reflect.DeepEqual(err, wantErr()) { | ||||||
| 			t.Errorf("destroy: error = %v, want %v", err, wantErr()) | 			t.Errorf("destroy: error = %v, want %v", err, wantErr()) | ||||||
| 		} | 		} | ||||||
| 		if err := eh.save(nil); !reflect.DeepEqual(err, wantErr()) { | 		if err := eh.Save(nil); !reflect.DeepEqual(err, wantErr()) { | ||||||
| 			t.Errorf("save: error = %v, want %v", err, wantErr()) | 			t.Errorf("save: error = %v, want %v", err, wantErr()) | ||||||
| 		} | 		} | ||||||
| 		if _, err := eh.load(nil); !reflect.DeepEqual(err, wantErr()) { | 		if _, err := eh.Load(nil); !reflect.DeepEqual(err, wantErr()) { | ||||||
| 			t.Errorf("load: error = %v, want %v", err, wantErr()) | 			t.Errorf("load: error = %v, want %v", err, wantErr()) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| @ -42,30 +56,30 @@ func TestStateEntryHandle(t *testing.T) { | |||||||
| 		t.Parallel() | 		t.Parallel() | ||||||
| 
 | 
 | ||||||
| 		{ | 		{ | ||||||
| 			eh := stateEntryHandle{pathname: check.MustAbs(t.TempDir()).Append("entry")} | 			eh := store.EntryHandle{Pathname: check.MustAbs(t.TempDir()).Append("entry")} | ||||||
| 			if f, err := eh.open(os.O_CREATE|syscall.O_EXCL, 0); err != nil { | 			if f, err := open(&eh, os.O_CREATE|syscall.O_EXCL, 0); err != nil { | ||||||
| 				t.Fatalf("open: error = %v", err) | 				t.Fatalf("open: error = %v", err) | ||||||
| 			} else if err = f.Close(); err != nil { | 			} else if err = f.Close(); err != nil { | ||||||
| 				t.Errorf("Close: error = %v", err) | 				t.Errorf("Close: error = %v", err) | ||||||
| 			} | 			} | ||||||
| 			if err := eh.destroy(); err != nil { | 			if err := eh.Destroy(); err != nil { | ||||||
| 				t.Fatalf("destroy: error = %v", err) | 				t.Fatalf("destroy: error = %v", err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		t.Run("nonexistent", func(t *testing.T) { | 		t.Run("nonexistent", func(t *testing.T) { | ||||||
| 			t.Parallel() | 			t.Parallel() | ||||||
| 			eh := stateEntryHandle{pathname: check.MustAbs("/proc/nonexistent")} | 			eh := store.EntryHandle{Pathname: check.MustAbs("/proc/nonexistent")} | ||||||
| 
 | 
 | ||||||
| 			wantErrOpen := &hst.AppError{Step: "open state entry", | 			wantErrOpen := &hst.AppError{Step: "open state entry", | ||||||
| 				Err: &os.PathError{Op: "open", Path: "/proc/nonexistent", Err: syscall.ENOENT}} | 				Err: &os.PathError{Op: "open", Path: "/proc/nonexistent", Err: syscall.ENOENT}} | ||||||
| 			if _, err := eh.open(os.O_CREATE|syscall.O_EXCL, 0); !reflect.DeepEqual(err, wantErrOpen) { | 			if _, err := open(&eh, os.O_CREATE|syscall.O_EXCL, 0); !reflect.DeepEqual(err, wantErrOpen) { | ||||||
| 				t.Errorf("open: error = %#v, want %#v", err, wantErrOpen) | 				t.Errorf("open: error = %#v, want %#v", err, wantErrOpen) | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			wantErrDestroy := &hst.AppError{Step: "destroy state entry", | 			wantErrDestroy := &hst.AppError{Step: "destroy state entry", | ||||||
| 				Err: &os.PathError{Op: "remove", Path: "/proc/nonexistent", Err: syscall.ENOENT}} | 				Err: &os.PathError{Op: "remove", Path: "/proc/nonexistent", Err: syscall.ENOENT}} | ||||||
| 			if err := eh.destroy(); !reflect.DeepEqual(err, wantErrDestroy) { | 			if err := eh.Destroy(); !reflect.DeepEqual(err, wantErrDestroy) { | ||||||
| 				t.Errorf("destroy: error = %#v, want %#v", err, wantErrDestroy) | 				t.Errorf("destroy: error = %#v, want %#v", err, wantErrDestroy) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| @ -73,10 +87,10 @@ func TestStateEntryHandle(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	t.Run("saveload", func(t *testing.T) { | 	t.Run("saveload", func(t *testing.T) { | ||||||
| 		t.Parallel() | 		t.Parallel() | ||||||
| 		eh := stateEntryHandle{pathname: check.MustAbs(t.TempDir()).Append("entry"), | 		eh := store.EntryHandle{Pathname: check.MustAbs(t.TempDir()).Append("entry"), | ||||||
| 			ID: newTemplateState().ID} | 			ID: newTemplateState().ID} | ||||||
| 
 | 
 | ||||||
| 		if err := eh.save(newTemplateState()); err != nil { | 		if err := eh.Save(newTemplateState()); err != nil { | ||||||
| 			t.Fatalf("save: error = %v", err) | 			t.Fatalf("save: error = %v", err) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @ -87,7 +101,7 @@ func TestStateEntryHandle(t *testing.T) { | |||||||
| 				t.Parallel() | 				t.Parallel() | ||||||
| 
 | 
 | ||||||
| 				var got hst.State | 				var got hst.State | ||||||
| 				if f, err := os.Open(eh.pathname.String()); err != nil { | 				if f, err := os.Open(eh.Pathname.String()); err != nil { | ||||||
| 					t.Fatal(err.Error()) | 					t.Fatal(err.Error()) | ||||||
| 				} else if _, err = entryDecode(f, &got); err != nil { | 				} else if _, err = entryDecode(f, &got); err != nil { | ||||||
| 					t.Fatalf("entryDecode: error = %v", err) | 					t.Fatalf("entryDecode: error = %v", err) | ||||||
| @ -103,7 +117,7 @@ func TestStateEntryHandle(t *testing.T) { | |||||||
| 			t.Run("load header only", func(t *testing.T) { | 			t.Run("load header only", func(t *testing.T) { | ||||||
| 				t.Parallel() | 				t.Parallel() | ||||||
| 
 | 
 | ||||||
| 				if et, err := eh.load(nil); err != nil { | 				if et, err := eh.Load(nil); err != nil { | ||||||
| 					t.Fatalf("load: error = %v", err) | 					t.Fatalf("load: error = %v", err) | ||||||
| 				} else if want := newTemplateState().Enablements.Unwrap(); et != want { | 				} else if want := newTemplateState().Enablements.Unwrap(); et != want { | ||||||
| 					t.Errorf("load: et = %x, want %x", et, want) | 					t.Errorf("load: et = %x, want %x", et, want) | ||||||
| @ -114,7 +128,7 @@ func TestStateEntryHandle(t *testing.T) { | |||||||
| 				t.Parallel() | 				t.Parallel() | ||||||
| 
 | 
 | ||||||
| 				var got hst.State | 				var got hst.State | ||||||
| 				if _, err := eh.load(&got); err != nil { | 				if _, err := eh.Load(&got); err != nil { | ||||||
| 					t.Fatalf("load: error = %v", err) | 					t.Fatalf("load: error = %v", err) | ||||||
| 				} else if want := newTemplateState(); !reflect.DeepEqual(&got, want) { | 				} else if want := newTemplateState(); !reflect.DeepEqual(&got, want) { | ||||||
| 					t.Errorf("load: %#v, want %#v", &got, want) | 					t.Errorf("load: %#v, want %#v", &got, want) | ||||||
| @ -126,8 +140,8 @@ func TestStateEntryHandle(t *testing.T) { | |||||||
| 				wantErr := &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid, | 				wantErr := &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid, | ||||||
| 					Msg: "state entry 00000000000000000000000000000000 has unexpected id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} | 					Msg: "state entry 00000000000000000000000000000000 has unexpected id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} | ||||||
| 
 | 
 | ||||||
| 				ehi := stateEntryHandle{pathname: eh.pathname} | 				ehi := store.EntryHandle{Pathname: eh.Pathname} | ||||||
| 				if _, err := ehi.load(new(hst.State)); !reflect.DeepEqual(err, wantErr) { | 				if _, err := ehi.Load(new(hst.State)); !reflect.DeepEqual(err, wantErr) { | ||||||
| 					t.Errorf("load: error = %#v, want %#v", err, wantErr) | 					t.Errorf("load: error = %#v, want %#v", err, wantErr) | ||||||
| 				} | 				} | ||||||
| 			}) | 			}) | ||||||
| @ -141,8 +155,8 @@ func TestStoreHandle(t *testing.T) { | |||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		name string | 		name string | ||||||
| 		ents [2][]string | 		ents [2][]string | ||||||
| 		want func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle | 		want func(newEh func(err error, name string) *store.EntryHandle) []*store.EntryHandle | ||||||
| 		ext  func(t *testing.T, entries iter.Seq[*stateEntryHandle], n int) | 		ext  func(t *testing.T, entries iter.Seq[*store.EntryHandle], n int) | ||||||
| 	}{ | 	}{ | ||||||
| 		{"errors", [2][]string{{ | 		{"errors", [2][]string{{ | ||||||
| 			"e81eb203b4190ac5c3842ef44d429945", | 			"e81eb203b4190ac5c3842ef44d429945", | ||||||
| @ -150,8 +164,8 @@ func TestStoreHandle(t *testing.T) { | |||||||
| 			"f0-invalid", | 			"f0-invalid", | ||||||
| 		}, { | 		}, { | ||||||
| 			"f1-directory", | 			"f1-directory", | ||||||
| 		}}, func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle { | 		}}, func(newEh func(err error, name string) *store.EntryHandle) []*store.EntryHandle { | ||||||
| 			return []*stateEntryHandle{ | 			return []*store.EntryHandle{ | ||||||
| 				newEh(nil, "e81eb203b4190ac5c3842ef44d429945"), | 				newEh(nil, "e81eb203b4190ac5c3842ef44d429945"), | ||||||
| 				newEh(&hst.AppError{Step: "decode store segment entry", | 				newEh(&hst.AppError{Step: "decode store segment entry", | ||||||
| 					Err: hst.IdentifierDecodeError{Err: hst.ErrIdentifierLength}}, "f0-invalid"), | 					Err: hst.IdentifierDecodeError{Err: hst.ErrIdentifierLength}}, "f0-invalid"), | ||||||
| @ -167,17 +181,17 @@ func TestStoreHandle(t *testing.T) { | |||||||
| 			"c8c8e2c4aea5c32fe47240ff8caa874e", | 			"c8c8e2c4aea5c32fe47240ff8caa874e", | ||||||
| 			"fa0d30b249d80f155a1f80ceddcc32f2", | 			"fa0d30b249d80f155a1f80ceddcc32f2", | ||||||
| 			"lock", | 			"lock", | ||||||
| 		}}, func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle { | 		}}, func(newEh func(err error, name string) *store.EntryHandle) []*store.EntryHandle { | ||||||
| 			return []*stateEntryHandle{ | 			return []*store.EntryHandle{ | ||||||
| 				newEh(nil, "7958cfbb9272d9cf9cfd61c85afa13f1"), | 				newEh(nil, "7958cfbb9272d9cf9cfd61c85afa13f1"), | ||||||
| 				newEh(nil, "c8c8e2c4aea5c32fe47240ff8caa874e"), | 				newEh(nil, "c8c8e2c4aea5c32fe47240ff8caa874e"), | ||||||
| 				newEh(nil, "d0b5f7446dd5bd3424ff2f7ac9cace1e"), | 				newEh(nil, "d0b5f7446dd5bd3424ff2f7ac9cace1e"), | ||||||
| 				newEh(nil, "e81eb203b4190ac5c3842ef44d429945"), | 				newEh(nil, "e81eb203b4190ac5c3842ef44d429945"), | ||||||
| 				newEh(nil, "fa0d30b249d80f155a1f80ceddcc32f2"), | 				newEh(nil, "fa0d30b249d80f155a1f80ceddcc32f2"), | ||||||
| 			} | 			} | ||||||
| 		}, func(t *testing.T, entries iter.Seq[*stateEntryHandle], n int) { | 		}, func(t *testing.T, entries iter.Seq[*store.EntryHandle], n int) { | ||||||
| 			if n != 5 { | 			if n != 5 { | ||||||
| 				t.Fatalf("entries: n = %d", n) | 				t.Fatalf("Entries: n = %d", n) | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// check partial drain | 			// check partial drain | ||||||
| @ -190,29 +204,26 @@ func TestStoreHandle(t *testing.T) { | |||||||
| 		t.Run(tc.name, func(t *testing.T) { | 		t.Run(tc.name, func(t *testing.T) { | ||||||
| 			t.Parallel() | 			t.Parallel() | ||||||
| 
 | 
 | ||||||
| 			p := check.MustAbs(t.TempDir()).Append("segment") | 			base := check.MustAbs(t.TempDir()).Append("store") | ||||||
| 			if err := os.Mkdir(p.String(), 0700); err != nil { | 			segment := base.Append("9") | ||||||
|  | 			if err := os.MkdirAll(segment.String(), 0700); err != nil { | ||||||
| 				t.Fatal(err.Error()) | 				t.Fatal(err.Error()) | ||||||
| 			} | 			} | ||||||
| 			createEntries(t, p, tc.ents) | 			createEntries(t, segment, tc.ents) | ||||||
| 
 | 
 | ||||||
| 			var got []*stateEntryHandle | 			var got []*store.EntryHandle | ||||||
| 			if entries, n, err := (&storeHandle{ | 			if entries, n, err := newHandle(base, 9).Entries(); err != nil { | ||||||
| 				identity: -0xbad, | 				t.Fatalf("Entries: error = %v", err) | ||||||
| 				path:     p, |  | ||||||
| 				fileMu:   lockedfile.MutexAt(p.Append("lock").String()), |  | ||||||
| 			}).entries(); err != nil { |  | ||||||
| 				t.Fatalf("entries: error = %v", err) |  | ||||||
| 			} else { | 			} else { | ||||||
| 				got = slices.AppendSeq(make([]*stateEntryHandle, 0, n), entries) | 				got = slices.AppendSeq(make([]*store.EntryHandle, 0, n), entries) | ||||||
| 				if tc.ext != nil { | 				if tc.ext != nil { | ||||||
| 					tc.ext(t, entries, n) | 					tc.ext(t, entries, n) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			slices.SortFunc(got, func(a, b *stateEntryHandle) int { return strings.Compare(a.pathname.String(), b.pathname.String()) }) | 			slices.SortFunc(got, func(a, b *store.EntryHandle) int { return strings.Compare(a.Pathname.String(), b.Pathname.String()) }) | ||||||
| 			want := tc.want(func(err error, name string) *stateEntryHandle { | 			want := tc.want(func(err error, name string) *store.EntryHandle { | ||||||
| 				eh := stateEntryHandle{decodeErr: err, pathname: p.Append(name)} | 				eh := store.EntryHandle{DecodeErr: err, Pathname: segment.Append(name)} | ||||||
| 				if err == nil { | 				if err == nil { | ||||||
| 					if err = eh.UnmarshalText([]byte(name)); err != nil { | 					if err = eh.UnmarshalText([]byte(name)); err != nil { | ||||||
| 						t.Fatalf("UnmarshalText: error = %v", err) | 						t.Fatalf("UnmarshalText: error = %v", err) | ||||||
| @ -222,7 +233,7 @@ func TestStoreHandle(t *testing.T) { | |||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			if !reflect.DeepEqual(got, want) { | 			if !reflect.DeepEqual(got, want) { | ||||||
| 				t.Errorf("entries: %q, want %q", got, want) | 				t.Errorf("Entries: %q, want %q", got, want) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| @ -233,11 +244,11 @@ func TestStoreHandle(t *testing.T) { | |||||||
| 			Path: "/proc/nonexistent", | 			Path: "/proc/nonexistent", | ||||||
| 			Err:  syscall.ENOENT, | 			Err:  syscall.ENOENT, | ||||||
| 		}} | 		}} | ||||||
| 		if _, _, err := (&storeHandle{ | 		if _, _, err := (&store.Handle{ | ||||||
| 			identity: -0xbad, | 			Identity: -0xbad, | ||||||
| 			path:     check.MustAbs("/proc/nonexistent"), | 			Path:     check.MustAbs("/proc/nonexistent"), | ||||||
| 		}).entries(); !reflect.DeepEqual(err, wantErr) { | 		}).Entries(); !reflect.DeepEqual(err, wantErr) { | ||||||
| 			t.Fatalf("entries: error = %#v, want %#v", err, wantErr) | 			t.Fatalf("Entries: error = %#v, want %#v", err, wantErr) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | // Package store implements cross-process state tracking for hakurei container instances. | ||||||
| package store | package store | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| @ -14,16 +15,16 @@ import ( | |||||||
| 	"hakurei.app/internal/lockedfile" | 	"hakurei.app/internal/lockedfile" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // storeMutexName is the pathname of the file backing [lockedfile.Mutex] of a stateStore and storeHandle. | // MutexName is the pathname of the file backing [lockedfile.Mutex] of a [Store] and [Handle]. | ||||||
| const storeMutexName = "lock" | const MutexName = "lock" | ||||||
| 
 | 
 | ||||||
| // A stateStore keeps track of [hst.State] via a well-known filesystem accessible to all hakurei priv-side processes. | // A Store keeps track of [hst.State] via a well-known filesystem accessible to all hakurei priv-side processes. | ||||||
| // Access to store data and related resources are synchronised on a per-segment basis via storeHandle. | // Access to store data and related resources are synchronised on a per-segment basis via [Handle]. | ||||||
| type stateStore struct { | type Store struct { | ||||||
| 	// Pathname of directory that the store is rooted in. | 	// Pathname of directory that the store is rooted in. | ||||||
| 	base *check.Absolute | 	base *check.Absolute | ||||||
| 
 | 
 | ||||||
| 	// All currently known instances of storeHandle, keyed by their identity. | 	// All currently known instances of Handle, keyed by their identity. | ||||||
| 	handles sync.Map | 	handles sync.Map | ||||||
| 
 | 
 | ||||||
| 	// Inter-process mutex to synchronise operations against the entire store. | 	// Inter-process mutex to synchronise operations against the entire store. | ||||||
| @ -37,9 +38,9 @@ type stateStore struct { | |||||||
| 	mkdirErr error | 	mkdirErr error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // bigLock acquires fileMu on stateStore. | // bigLock acquires fileMu on [Store]. | ||||||
| // A non-nil error returned by bigLock is of type [hst.AppError]. | // A non-nil error returned by bigLock is of type [hst.AppError]. | ||||||
| func (s *stateStore) bigLock() (unlock func(), err error) { | func (s *Store) bigLock() (unlock func(), err error) { | ||||||
| 	s.mkdirOnce.Do(func() { s.mkdirErr = os.MkdirAll(s.base.String(), 0700) }) | 	s.mkdirOnce.Do(func() { s.mkdirErr = os.MkdirAll(s.base.String(), 0700) }) | ||||||
| 	if s.mkdirErr != nil { | 	if s.mkdirErr != nil { | ||||||
| 		return nil, &hst.AppError{Step: "create state store directory", Err: s.mkdirErr} | 		return nil, &hst.AppError{Step: "create state store directory", Err: s.mkdirErr} | ||||||
| @ -51,14 +52,14 @@ func (s *stateStore) bigLock() (unlock func(), err error) { | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // identityHandle loads or initialises a storeHandle for identity. | // Handle loads or initialises a [Handle] for identity. | ||||||
| // A non-nil error returned by identityHandle is of type [hst.AppError]. | // A non-nil error returned by Handle is of type [hst.AppError]. | ||||||
| func (s *stateStore) identityHandle(identity int) (*storeHandle, error) { | func (s *Store) Handle(identity int) (*Handle, error) { | ||||||
| 	h := new(storeHandle) | 	h := newHandle(s.base, identity) | ||||||
| 	h.mu.Lock() | 	h.mu.Lock() | ||||||
| 
 | 
 | ||||||
| 	if v, ok := s.handles.LoadOrStore(identity, h); ok { | 	if v, ok := s.handles.LoadOrStore(identity, h); ok { | ||||||
| 		h = v.(*storeHandle) | 		h = v.(*Handle) | ||||||
| 	} else { | 	} else { | ||||||
| 		// acquire big lock to initialise previously unknown segment handle | 		// acquire big lock to initialise previously unknown segment handle | ||||||
| 		if unlock, err := s.bigLock(); err != nil { | 		if unlock, err := s.bigLock(); err != nil { | ||||||
| @ -67,11 +68,7 @@ func (s *stateStore) identityHandle(identity int) (*storeHandle, error) { | |||||||
| 			defer unlock() | 			defer unlock() | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		h.identity = identity | 		err := os.MkdirAll(h.Path.String(), 0700) | ||||||
| 		h.path = s.base.Append(strconv.Itoa(identity)) |  | ||||||
| 		h.fileMu = lockedfile.MutexAt(h.path.Append(storeMutexName).String()) |  | ||||||
| 
 |  | ||||||
| 		err := os.MkdirAll(h.path.String(), 0700) |  | ||||||
| 		h.mu.Unlock() | 		h.mu.Unlock() | ||||||
| 		if err != nil && !errors.Is(err, fs.ErrExist) { | 		if err != nil && !errors.Is(err, fs.ErrExist) { | ||||||
| 			// handle methods will likely return ENOENT | 			// handle methods will likely return ENOENT | ||||||
| @ -82,18 +79,18 @@ func (s *stateStore) identityHandle(identity int) (*storeHandle, error) { | |||||||
| 	return h, nil | 	return h, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // segmentIdentity is produced by the iterator returned by stateStore.segments. | // SegmentIdentity is produced by the iterator returned by [Store.Segments]. | ||||||
| type segmentIdentity struct { | type SegmentIdentity struct { | ||||||
| 	// Identity of the current segment. | 	// Identity of the current segment. | ||||||
| 	identity int | 	Identity int | ||||||
| 	// Error encountered while processing this segment. | 	// Error encountered while processing this segment. | ||||||
| 	err error | 	Err error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // segments returns an iterator over all segmentIdentity known to the store. | // Segments returns an iterator over all [SegmentIdentity] known to the [Store]. | ||||||
| // To obtain a storeHandle on a segment, caller must then call identityHandle. | // To obtain a [Handle] on a segment, caller must then call [Store.Handle]. | ||||||
| // A non-nil error returned by segments is of type [hst.AppError]. | // A non-nil error returned by segments is of type [hst.AppError]. | ||||||
| func (s *stateStore) segments() (iter.Seq[segmentIdentity], int, error) { | func (s *Store) Segments() (iter.Seq[SegmentIdentity], int, error) { | ||||||
| 	// read directory contents, should only contain storeMutexName and identity | 	// read directory contents, should only contain storeMutexName and identity | ||||||
| 	var entries []os.DirEntry | 	var entries []os.DirEntry | ||||||
| 
 | 
 | ||||||
| @ -115,36 +112,36 @@ func (s *stateStore) segments() (iter.Seq[segmentIdentity], int, error) { | |||||||
| 		l-- | 		l-- | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return func(yield func(segmentIdentity) bool) { | 	return func(yield func(SegmentIdentity) bool) { | ||||||
| 		// for error reporting | 		// for error reporting | ||||||
| 		const step = "process store segment" | 		const step = "process store segment" | ||||||
| 
 | 
 | ||||||
| 		for _, ent := range entries { | 		for _, ent := range entries { | ||||||
| 			si := segmentIdentity{identity: -1} | 			si := SegmentIdentity{Identity: -1} | ||||||
| 
 | 
 | ||||||
| 			// should only be the big lock | 			// should only be the big lock | ||||||
| 			if !ent.IsDir() { | 			if !ent.IsDir() { | ||||||
| 				if ent.Name() == storeMutexName { | 				if ent.Name() == MutexName { | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				// this should never happen | 				// this should never happen | ||||||
| 				si.err = &hst.AppError{Step: step, Err: syscall.EISDIR, | 				si.Err = &hst.AppError{Step: step, Err: syscall.EISDIR, | ||||||
| 					Msg: "skipped non-directory entry " + strconv.Quote(ent.Name())} | 					Msg: "skipped non-directory entry " + strconv.Quote(ent.Name())} | ||||||
| 				goto out | 				goto out | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// failure paths either indicates a serious bug or external interference | 			// failure paths either indicates a serious bug or external interference | ||||||
| 			if v, err := strconv.Atoi(ent.Name()); err != nil { | 			if v, err := strconv.Atoi(ent.Name()); err != nil { | ||||||
| 				si.err = &hst.AppError{Step: step, Err: err, | 				si.Err = &hst.AppError{Step: step, Err: err, | ||||||
| 					Msg: "skipped non-identity entry " + strconv.Quote(ent.Name())} | 					Msg: "skipped non-identity entry " + strconv.Quote(ent.Name())} | ||||||
| 				goto out | 				goto out | ||||||
| 			} else if v < hst.IdentityMin || v > hst.IdentityMax { | 			} else if v < hst.IdentityMin || v > hst.IdentityMax { | ||||||
| 				si.err = &hst.AppError{Step: step, Err: syscall.ERANGE, | 				si.Err = &hst.AppError{Step: step, Err: syscall.ERANGE, | ||||||
| 					Msg: "skipped out of bounds entry " + strconv.Itoa(v)} | 					Msg: "skipped out of bounds entry " + strconv.Itoa(v)} | ||||||
| 				goto out | 				goto out | ||||||
| 			} else { | 			} else { | ||||||
| 				si.identity = v | 				si.Identity = v | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 		out: | 		out: | ||||||
| @ -155,8 +152,8 @@ func (s *stateStore) segments() (iter.Seq[segmentIdentity], int, error) { | |||||||
| 	}, l, nil | 	}, l, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // newStore returns the address of a new instance of stateStore. | // New returns the address of a new instance of [Store]. | ||||||
| // Multiple instances of stateStore rooted in the same directory is supported, but discouraged. | // Multiple instances of [Store] rooted in the same directory is possible, but unsupported. | ||||||
| func newStore(base *check.Absolute) *stateStore { | func New(base *check.Absolute) *Store { | ||||||
| 	return &stateStore{base: base, fileMu: lockedfile.MutexAt(base.Append(storeMutexName).String())} | 	return &Store{base: base, fileMu: lockedfile.MutexAt(base.Append(MutexName).String())} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| package store | package store_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"cmp" | 	"cmp" | ||||||
| @ -10,18 +10,23 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	_ "unsafe" | ||||||
| 
 | 
 | ||||||
| 	"hakurei.app/container/check" | 	"hakurei.app/container/check" | ||||||
| 	"hakurei.app/hst" | 	"hakurei.app/hst" | ||||||
|  | 	"hakurei.app/internal/store" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | //go:linkname bigLock hakurei.app/internal/store.(*Store).bigLock | ||||||
|  | func bigLock(s *store.Store) (unlock func(), err error) | ||||||
|  | 
 | ||||||
| func TestStateStoreBigLock(t *testing.T) { | func TestStateStoreBigLock(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
| 
 | 
 | ||||||
| 	{ | 	{ | ||||||
| 		s := newStore(check.MustAbs(t.TempDir()).Append("state")) | 		s := store.New(check.MustAbs(t.TempDir()).Append("state")) | ||||||
| 		for i := 0; i < 2; i++ { // check once behaviour | 		for i := 0; i < 2; i++ { // check once behaviour | ||||||
| 			if unlock, err := s.bigLock(); err != nil { | 			if unlock, err := bigLock(s); err != nil { | ||||||
| 				t.Fatalf("bigLock: error = %v", err) | 				t.Fatalf("bigLock: error = %v", err) | ||||||
| 			} else { | 			} else { | ||||||
| 				unlock() | 				unlock() | ||||||
| @ -35,7 +40,7 @@ func TestStateStoreBigLock(t *testing.T) { | |||||||
| 		wantErr := &hst.AppError{Step: "create state store directory", | 		wantErr := &hst.AppError{Step: "create state store directory", | ||||||
| 			Err: &os.PathError{Op: "mkdir", Path: "/proc/nonexistent", Err: syscall.ENOENT}} | 			Err: &os.PathError{Op: "mkdir", Path: "/proc/nonexistent", Err: syscall.ENOENT}} | ||||||
| 		for i := 0; i < 2; i++ { // check once behaviour | 		for i := 0; i < 2; i++ { // check once behaviour | ||||||
| 			if _, err := newStore(check.MustAbs("/proc/nonexistent")).bigLock(); !reflect.DeepEqual(err, wantErr) { | 			if _, err := bigLock(store.New(check.MustAbs("/proc/nonexistent"))); !reflect.DeepEqual(err, wantErr) { | ||||||
| 				t.Errorf("bigLock: error = %#v, want %#v", err, wantErr) | 				t.Errorf("bigLock: error = %#v, want %#v", err, wantErr) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -50,35 +55,35 @@ func TestStateStoreBigLock(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		wantErr := &hst.AppError{Step: "acquire lock on the state store", | 		wantErr := &hst.AppError{Step: "acquire lock on the state store", | ||||||
| 			Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}} | 			Err: &os.PathError{Op: "open", Path: base.Append(store.MutexName).String(), Err: syscall.EACCES}} | ||||||
| 		if _, err := newStore(base).bigLock(); !reflect.DeepEqual(err, wantErr) { | 		if _, err := bigLock(store.New(base)); !reflect.DeepEqual(err, wantErr) { | ||||||
| 			t.Errorf("bigLock: error = %#v, want %#v", err, wantErr) | 			t.Errorf("bigLock: error = %#v, want %#v", err, wantErr) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestStateStoreIdentityHandle(t *testing.T) { | func TestStateStoreHandle(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
| 
 | 
 | ||||||
| 	t.Run("loadstore", func(t *testing.T) { | 	t.Run("loadstore", func(t *testing.T) { | ||||||
| 		t.Parallel() | 		t.Parallel() | ||||||
| 
 | 
 | ||||||
| 		s := newStore(check.MustAbs(t.TempDir()).Append("store")) | 		s := store.New(check.MustAbs(t.TempDir()).Append("store")) | ||||||
| 
 | 
 | ||||||
| 		var handleAddr [8]*storeHandle | 		var handleAddr [8]*store.Handle | ||||||
| 		checkHandle := func(identity int, load bool) { | 		checkHandle := func(identity int, load bool) { | ||||||
| 			if h, err := s.identityHandle(identity); err != nil { | 			if h, err := s.Handle(identity); err != nil { | ||||||
| 				t.Fatalf("identityHandle: error = %v", err) | 				t.Fatalf("Handle: error = %v", err) | ||||||
| 			} else if load != (handleAddr[identity] != nil) { | 			} else if load != (handleAddr[identity] != nil) { | ||||||
| 				t.Fatalf("identityHandle: load = %v, want %v", load, handleAddr[identity] != nil) | 				t.Fatalf("Handle: load = %v, want %v", load, handleAddr[identity] != nil) | ||||||
| 			} else if !load { | 			} else if !load { | ||||||
| 				handleAddr[identity] = h | 				handleAddr[identity] = h | ||||||
| 
 | 
 | ||||||
| 				if h.identity != identity { | 				if h.Identity != identity { | ||||||
| 					t.Errorf("identityHandle: identity = %d, want %d", h.identity, identity) | 					t.Errorf("Handle: identity = %d, want %d", h.Identity, identity) | ||||||
| 				} | 				} | ||||||
| 			} else if h != handleAddr[identity] { | 			} else if h != handleAddr[identity] { | ||||||
| 				t.Fatalf("identityHandle: %p, want %p", h, handleAddr[identity]) | 				t.Fatalf("Handle: %p, want %p", h, handleAddr[identity]) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @ -103,9 +108,9 @@ func TestStateStoreIdentityHandle(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		wantErr := &hst.AppError{Step: "acquire lock on the state store", | 		wantErr := &hst.AppError{Step: "acquire lock on the state store", | ||||||
| 			Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}} | 			Err: &os.PathError{Op: "open", Path: base.Append(store.MutexName).String(), Err: syscall.EACCES}} | ||||||
| 		if _, err := newStore(base).identityHandle(0); !reflect.DeepEqual(err, wantErr) { | 		if _, err := store.New(base).Handle(0); !reflect.DeepEqual(err, wantErr) { | ||||||
| 			t.Errorf("identityHandle: error = %#v, want %#v", err, wantErr) | 			t.Errorf("Handle: error = %#v, want %#v", err, wantErr) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| @ -116,7 +121,7 @@ func TestStateStoreIdentityHandle(t *testing.T) { | |||||||
| 		if err := os.MkdirAll(base.String(), 0700); err != nil { | 		if err := os.MkdirAll(base.String(), 0700); err != nil { | ||||||
| 			t.Fatal(err.Error()) | 			t.Fatal(err.Error()) | ||||||
| 		} | 		} | ||||||
| 		if f, err := os.Create(base.Append(storeMutexName).String()); err != nil { | 		if f, err := os.Create(base.Append(store.MutexName).String()); err != nil { | ||||||
| 			t.Fatal(err.Error()) | 			t.Fatal(err.Error()) | ||||||
| 		} else if err = f.Close(); err != nil { | 		} else if err = f.Close(); err != nil { | ||||||
| 			t.Fatal(err.Error()) | 			t.Fatal(err.Error()) | ||||||
| @ -132,8 +137,8 @@ func TestStateStoreIdentityHandle(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 		wantErr := &hst.AppError{Step: "create store segment directory", | 		wantErr := &hst.AppError{Step: "create store segment directory", | ||||||
| 			Err: &os.PathError{Op: "mkdir", Path: base.Append("0").String(), Err: syscall.EACCES}} | 			Err: &os.PathError{Op: "mkdir", Path: base.Append("0").String(), Err: syscall.EACCES}} | ||||||
| 		if _, err := newStore(base).identityHandle(0); !reflect.DeepEqual(err, wantErr) { | 		if _, err := store.New(base).Handle(0); !reflect.DeepEqual(err, wantErr) { | ||||||
| 			t.Errorf("identityHandle: error = %#v, want %#v", err, wantErr) | 			t.Errorf("Handle: error = %#v, want %#v", err, wantErr) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| @ -144,8 +149,8 @@ func TestStateStoreSegments(t *testing.T) { | |||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		name string | 		name string | ||||||
| 		ents [2][]string | 		ents [2][]string | ||||||
| 		want []segmentIdentity | 		want []store.SegmentIdentity | ||||||
| 		ext  func(t *testing.T, segments iter.Seq[segmentIdentity], n int) | 		ext  func(t *testing.T, segments iter.Seq[store.SegmentIdentity], n int) | ||||||
| 	}{ | 	}{ | ||||||
| 		{"errors", [2][]string{{ | 		{"errors", [2][]string{{ | ||||||
| 			"f0-invalid-file", | 			"f0-invalid-file", | ||||||
| @ -153,7 +158,7 @@ func TestStateStoreSegments(t *testing.T) { | |||||||
| 			"f1-invalid-syntax", | 			"f1-invalid-syntax", | ||||||
| 			"9999", | 			"9999", | ||||||
| 			"16384", | 			"16384", | ||||||
| 		}}, []segmentIdentity{ | 		}}, []store.SegmentIdentity{ | ||||||
| 			{-1, &hst.AppError{Step: "process store segment", Err: syscall.EISDIR, | 			{-1, &hst.AppError{Step: "process store segment", Err: syscall.EISDIR, | ||||||
| 				Msg: `skipped non-directory entry "f0-invalid-file"`}}, | 				Msg: `skipped non-directory entry "f0-invalid-file"`}}, | ||||||
| 			{-1, &hst.AppError{Step: "process store segment", Err: syscall.ERANGE, | 			{-1, &hst.AppError{Step: "process store segment", Err: syscall.ERANGE, | ||||||
| @ -180,7 +185,7 @@ func TestStateStoreSegments(t *testing.T) { | |||||||
| 			"20", | 			"20", | ||||||
| 			"31", | 			"31", | ||||||
| 			"197", | 			"197", | ||||||
| 		}}, []segmentIdentity{ | 		}}, []store.SegmentIdentity{ | ||||||
| 			{0, nil}, | 			{0, nil}, | ||||||
| 			{1, nil}, | 			{1, nil}, | ||||||
| 			{2, nil}, | 			{2, nil}, | ||||||
| @ -194,9 +199,9 @@ func TestStateStoreSegments(t *testing.T) { | |||||||
| 			{20, nil}, | 			{20, nil}, | ||||||
| 			{31, nil}, | 			{31, nil}, | ||||||
| 			{197, nil}, | 			{197, nil}, | ||||||
| 		}, func(t *testing.T, segments iter.Seq[segmentIdentity], n int) { | 		}, func(t *testing.T, segments iter.Seq[store.SegmentIdentity], n int) { | ||||||
| 			if n != 13 { | 			if n != 13 { | ||||||
| 				t.Fatalf("segments: n = %d", n) | 				t.Fatalf("Segments: n = %d", n) | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// check partial drain | 			// check partial drain | ||||||
| @ -215,24 +220,24 @@ func TestStateStoreSegments(t *testing.T) { | |||||||
| 			} | 			} | ||||||
| 			createEntries(t, base, tc.ents) | 			createEntries(t, base, tc.ents) | ||||||
| 
 | 
 | ||||||
| 			var got []segmentIdentity | 			var got []store.SegmentIdentity | ||||||
| 			if segments, n, err := newStore(base).segments(); err != nil { | 			if segments, n, err := store.New(base).Segments(); err != nil { | ||||||
| 				t.Fatalf("segments: error = %v", err) | 				t.Fatalf("Segments: error = %v", err) | ||||||
| 			} else { | 			} else { | ||||||
| 				got = slices.AppendSeq(make([]segmentIdentity, 0, n), segments) | 				got = slices.AppendSeq(make([]store.SegmentIdentity, 0, n), segments) | ||||||
| 				if tc.ext != nil { | 				if tc.ext != nil { | ||||||
| 					tc.ext(t, segments, n) | 					tc.ext(t, segments, n) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			slices.SortFunc(got, func(a, b segmentIdentity) int { | 			slices.SortFunc(got, func(a, b store.SegmentIdentity) int { | ||||||
| 				if a.identity == b.identity { | 				if a.Identity == b.Identity { | ||||||
| 					return strings.Compare(a.err.Error(), b.err.Error()) | 					return strings.Compare(a.Err.Error(), b.Err.Error()) | ||||||
| 				} | 				} | ||||||
| 				return cmp.Compare(a.identity, b.identity) | 				return cmp.Compare(a.Identity, b.Identity) | ||||||
| 			}) | 			}) | ||||||
| 			if !reflect.DeepEqual(got, tc.want) { | 			if !reflect.DeepEqual(got, tc.want) { | ||||||
| 				t.Errorf("segments: %#v, want %#v", got, tc.want) | 				t.Errorf("Segments: %#v, want %#v", got, tc.want) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| @ -246,9 +251,9 @@ func TestStateStoreSegments(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		wantErr := &hst.AppError{Step: "acquire lock on the state store", | 		wantErr := &hst.AppError{Step: "acquire lock on the state store", | ||||||
| 			Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}} | 			Err: &os.PathError{Op: "open", Path: base.Append(store.MutexName).String(), Err: syscall.EACCES}} | ||||||
| 		if _, _, err := newStore(base).segments(); !reflect.DeepEqual(err, wantErr) { | 		if _, _, err := store.New(base).Segments(); !reflect.DeepEqual(err, wantErr) { | ||||||
| 			t.Errorf("segments: error = %#v, want %#v", err, wantErr) | 			t.Errorf("Segments: error = %#v, want %#v", err, wantErr) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user