engine refactor
This commit is contained in:
parent
9e663db9dc
commit
81b8e763bd
37 changed files with 3532 additions and 1639 deletions
|
|
@ -14,15 +14,16 @@ import (
|
|||
//
|
||||
// Layout under Root:
|
||||
//
|
||||
// <epoch>.bin — committed dataset (binary cube)
|
||||
// <epoch>.bin.downloading — in-progress dataset
|
||||
// <epoch>.bin.manifest.json — manifest of completed work units
|
||||
// <filename>.bin — committed dataset
|
||||
// <filename>.bin.downloading — in-progress dataset
|
||||
// <filename>.bin.manifest.json — completed work units
|
||||
//
|
||||
// The .bin suffix exists to differentiate from sidecars in directory listings;
|
||||
// epoch is formatted as "20060102T150405Z" (UTC).
|
||||
// where <filename> is DatasetID.Filename() — typically
|
||||
// "20060102T150405Z" for the global subset or
|
||||
// "20060102T150405Z_r-10.10.-30.30_h0.72" for a subset.
|
||||
type LocalStore struct {
|
||||
Root string
|
||||
Source string // source ID, recorded for safety but currently advisory
|
||||
Source string
|
||||
Extension string // default ".bin"
|
||||
}
|
||||
|
||||
|
|
@ -37,8 +38,6 @@ func NewLocalStore(root, sourceID string) (*LocalStore, error) {
|
|||
// SourceID returns the source ID this store is configured for.
|
||||
func (s *LocalStore) SourceID() string { return s.Source }
|
||||
|
||||
const epochFormat = "20060102T150405Z"
|
||||
|
||||
func (s *LocalStore) ext() string {
|
||||
if s.Extension == "" {
|
||||
return ".bin"
|
||||
|
|
@ -46,32 +45,32 @@ func (s *LocalStore) ext() string {
|
|||
return s.Extension
|
||||
}
|
||||
|
||||
// Path returns the canonical path for an epoch's committed dataset file.
|
||||
func (s *LocalStore) Path(epoch time.Time) string {
|
||||
return filepath.Join(s.Root, epoch.UTC().Format(epochFormat)+s.ext())
|
||||
// Path returns the canonical path for id's committed dataset.
|
||||
func (s *LocalStore) Path(id DatasetID) string {
|
||||
return filepath.Join(s.Root, id.Filename()+s.ext())
|
||||
}
|
||||
|
||||
func (s *LocalStore) tempPath(epoch time.Time) string {
|
||||
return s.Path(epoch) + ".downloading"
|
||||
func (s *LocalStore) tempPath(id DatasetID) string {
|
||||
return s.Path(id) + ".downloading"
|
||||
}
|
||||
|
||||
func (s *LocalStore) manifestPath(epoch time.Time) string {
|
||||
return s.Path(epoch) + ".manifest.json"
|
||||
func (s *LocalStore) manifestPath(id DatasetID) string {
|
||||
return s.Path(id) + ".manifest.json"
|
||||
}
|
||||
|
||||
// Exists reports whether a committed dataset for epoch is present.
|
||||
func (s *LocalStore) Exists(epoch time.Time) bool {
|
||||
info, err := os.Stat(s.Path(epoch))
|
||||
// Exists reports whether a committed dataset for id is present.
|
||||
func (s *LocalStore) Exists(id DatasetID) bool {
|
||||
info, err := os.Stat(s.Path(id))
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
// List returns all committed epochs, newest first.
|
||||
func (s *LocalStore) List() ([]time.Time, error) {
|
||||
// List returns all committed dataset IDs, newest first.
|
||||
func (s *LocalStore) List() ([]DatasetID, error) {
|
||||
entries, err := os.ReadDir(s.Root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read store: %w", err)
|
||||
}
|
||||
var out []time.Time
|
||||
var out []DatasetID
|
||||
ext := s.ext()
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
|
|
@ -82,24 +81,47 @@ func (s *LocalStore) List() ([]time.Time, error) {
|
|||
continue
|
||||
}
|
||||
stem := strings.TrimSuffix(name, ext)
|
||||
// skip in-progress files (their stem already has .bin.downloading...)
|
||||
// Skip in-progress files (their stem ends in .downloading or .manifest)
|
||||
if strings.Contains(stem, ".") {
|
||||
continue
|
||||
}
|
||||
t, err := time.Parse(epochFormat, stem)
|
||||
if err != nil {
|
||||
id, ok := parseFilename(stem)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, t.UTC())
|
||||
out = append(out, id)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].After(out[j]) })
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if !out[i].Epoch.Equal(out[j].Epoch) {
|
||||
return out[i].Epoch.After(out[j].Epoch)
|
||||
}
|
||||
return out[i].Subset.Key() < out[j].Subset.Key()
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Remove deletes the committed dataset and any sidecar files for epoch.
|
||||
func (s *LocalStore) Remove(epoch time.Time) error {
|
||||
// parseFilename inverts DatasetID.Filename(). The subset portion is not
|
||||
// fully reversible (Key encoding is one-way for floats), so List returns
|
||||
// IDs whose Subset is zero — the storage layer treats names as opaque
|
||||
// identifiers. Callers wanting structured subset metadata should keep an
|
||||
// out-of-band record.
|
||||
func parseFilename(stem string) (DatasetID, bool) {
|
||||
parts := strings.SplitN(stem, "_", 2)
|
||||
epoch, err := time.Parse("20060102T150405Z", parts[0])
|
||||
if err != nil {
|
||||
return DatasetID{}, false
|
||||
}
|
||||
id := DatasetID{Epoch: epoch.UTC()}
|
||||
// Subset key is opaque on disk; we don't reconstruct its parameters
|
||||
// here. Admin callers track subset specs separately when they need
|
||||
// the structured form.
|
||||
return id, true
|
||||
}
|
||||
|
||||
// Remove deletes the committed dataset and any sidecar files for id.
|
||||
func (s *LocalStore) Remove(id DatasetID) error {
|
||||
var errs []error
|
||||
for _, p := range []string{s.Path(epoch), s.tempPath(epoch), s.manifestPath(epoch)} {
|
||||
for _, p := range []string{s.Path(id), s.tempPath(id), s.manifestPath(id)} {
|
||||
if err := os.Remove(p); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
|
@ -110,55 +132,46 @@ func (s *LocalStore) Remove(epoch time.Time) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// BeginWrite opens or resumes a TempHandle for epoch.
|
||||
//
|
||||
// If a partial download is already present, its file and manifest are reused
|
||||
// so the new download picks up where the previous one stopped.
|
||||
func (s *LocalStore) BeginWrite(epoch time.Time) (TempHandle, error) {
|
||||
man, err := LoadManifest(s.manifestPath(epoch))
|
||||
// BeginWrite opens or resumes a TempHandle for id.
|
||||
func (s *LocalStore) BeginWrite(id DatasetID) (TempHandle, error) {
|
||||
man, err := LoadManifest(s.manifestPath(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &localHandle{
|
||||
store: s,
|
||||
epoch: epoch,
|
||||
manifest: man,
|
||||
}, nil
|
||||
return &localHandle{store: s, id: id, manifest: man}, nil
|
||||
}
|
||||
|
||||
type localHandle struct {
|
||||
store *LocalStore
|
||||
epoch time.Time
|
||||
id DatasetID
|
||||
manifest *Manifest
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (h *localHandle) Path() string { return h.store.tempPath(h.epoch) }
|
||||
func (h *localHandle) Path() string { return h.store.tempPath(h.id) }
|
||||
func (h *localHandle) Manifest() *Manifest { return h.manifest }
|
||||
|
||||
// Commit promotes the temp file to its final path and removes the manifest.
|
||||
func (h *localHandle) Commit() error {
|
||||
if h.closed {
|
||||
return nil
|
||||
}
|
||||
h.closed = true
|
||||
if err := os.Rename(h.store.tempPath(h.epoch), h.store.Path(h.epoch)); err != nil {
|
||||
if err := os.Rename(h.store.tempPath(h.id), h.store.Path(h.id)); err != nil {
|
||||
return fmt.Errorf("commit rename: %w", err)
|
||||
}
|
||||
if err := os.Remove(h.store.manifestPath(h.epoch)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.Remove(h.store.manifestPath(h.id)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("commit remove manifest: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Abort removes the in-progress file and manifest.
|
||||
func (h *localHandle) Abort() error {
|
||||
if h.closed {
|
||||
return nil
|
||||
}
|
||||
h.closed = true
|
||||
var firstErr error
|
||||
for _, p := range []string{h.store.tempPath(h.epoch), h.store.manifestPath(h.epoch)} {
|
||||
for _, p := range []string{h.store.tempPath(h.id), h.store.manifestPath(h.id)} {
|
||||
if err := os.Remove(p); err != nil && !errors.Is(err, os.ErrNotExist) && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue