Refacto FileStore to JSONStore
Generate check / check-changes (pull_request) Successful in 3s
Quality / check-changes (pull_request) Successful in 2s
Generate check / verify-generate (pull_request) Successful in 32s
Quality / static-checks (pull_request) Successful in 55s
Quality / run-tests (pull_request) Successful in 52s
Generate check / check-changes (pull_request) Successful in 3s
Quality / check-changes (pull_request) Successful in 2s
Generate check / verify-generate (pull_request) Successful in 32s
Quality / static-checks (pull_request) Successful in 55s
Quality / run-tests (pull_request) Successful in 52s
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSONStore is a Store backed by a single JSON file on disk. The on-disk
|
||||
// schema is the platform-agnostic State struct; per-broker Record data is
|
||||
// encoded and decoded through the supplied RecordSerializer.
|
||||
type JSONStore struct {
|
||||
filename string
|
||||
platform string
|
||||
serializer RecordSerializer
|
||||
}
|
||||
|
||||
// NewJSONStore constructs a JSONStore that reads from and writes to the given
|
||||
// filename. The JSONStore does not keep an *os.File open — Load and Save each
|
||||
// open the file themselves — so callers do not need to close it. The platform
|
||||
// string is recorded into the saved state and validated on Load so a state
|
||||
// file from a different broker cannot be loaded by mistake.
|
||||
func NewJSONStore(filename string, platform string, serializer RecordSerializer) (*JSONStore, error) {
|
||||
if filename == "" {
|
||||
return nil, fmt.Errorf("filename cannot be empty")
|
||||
}
|
||||
if serializer == nil {
|
||||
return nil, fmt.Errorf("serializer cannot be nil")
|
||||
}
|
||||
return &JSONStore{
|
||||
filename: filename,
|
||||
platform: platform,
|
||||
serializer: serializer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Load reads the state file and reconstructs the per-symbol FillerQueue map.
|
||||
// A missing file is not an error — it returns an empty map so the first run
|
||||
// against a new state file Just Works.
|
||||
func (fs *JSONStore) Load(ctx context.Context) (map[string]*FillerQueue, error) {
|
||||
data, err := os.ReadFile(fs.filename)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return make(map[string]*FillerQueue), nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading state file: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return make(map[string]*FillerQueue), nil
|
||||
}
|
||||
|
||||
var s State
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling state: %w", err)
|
||||
}
|
||||
|
||||
if s.Version != StateVersion {
|
||||
return nil, fmt.Errorf(
|
||||
"unexpected state version %q: expected %q",
|
||||
s.Version, StateVersion,
|
||||
)
|
||||
}
|
||||
|
||||
if s.Platform != fs.platform {
|
||||
return nil, fmt.Errorf(
|
||||
"unexpected state platform %q: expected %q",
|
||||
s.Platform, fs.platform,
|
||||
)
|
||||
}
|
||||
|
||||
queues := make(map[string]*FillerQueue)
|
||||
for symbol, persisted := range s.Queues {
|
||||
q := new(FillerQueue)
|
||||
for _, pf := range persisted {
|
||||
rec, err := fs.serializer.UnmarshalRecord(ctx, pf.RecordData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"unmarshalling record for symbol %q: %w", symbol, err,
|
||||
)
|
||||
}
|
||||
q.Push(NewFillerFromState(rec, pf.Quantity, pf.Price, pf.Filled))
|
||||
}
|
||||
queues[symbol] = q
|
||||
}
|
||||
return queues, nil
|
||||
}
|
||||
|
||||
// Save serialises the queue map to disk. The write is done via a temp file in
|
||||
// the same directory followed by an atomic rename so a crash mid-write cannot
|
||||
// leave a half-written state file.
|
||||
func (fs *JSONStore) Save(ctx context.Context, queue map[string]*FillerQueue) error {
|
||||
state := State{
|
||||
Version: StateVersion,
|
||||
Platform: fs.platform,
|
||||
Queues: make(map[string][]persistedFiller),
|
||||
}
|
||||
|
||||
for symbol, q := range queue {
|
||||
if q == nil || q.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
var persisted []persistedFiller
|
||||
for e := q.l.Front(); e != nil; e = e.Next() {
|
||||
f := e.Value.(*Filler)
|
||||
data, err := fs.serializer.MarshalRecord(ctx, f.Record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshalling record for symbol %q: %w", symbol, err)
|
||||
}
|
||||
persisted = append(persisted, persistedFiller{
|
||||
RecordData: data,
|
||||
Quantity: f.Quantity(),
|
||||
Price: f.Price(),
|
||||
Filled: f.Filled(),
|
||||
})
|
||||
}
|
||||
state.Queues[symbol] = persisted
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(state); err != nil {
|
||||
return fmt.Errorf("encoding state: %w", err)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(filepath.Dir(fs.filename), filepath.Base(fs.filename)+".*.tmp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating temp state file: %w", err)
|
||||
}
|
||||
tmpFilename := tmp.Name()
|
||||
defer os.Remove(tmpFilename)
|
||||
defer tmp.Close()
|
||||
|
||||
if _, err := io.Copy(tmp, buf); err != nil {
|
||||
return fmt.Errorf("writing temp state file: %w", err)
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
return fmt.Errorf("syncing temp state file: %w", err)
|
||||
}
|
||||
|
||||
// Closing the temp file before renaming/moving for cross-platform compatibility
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("closing temp state file: %w", err)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(fs.filename)
|
||||
name, _ := strings.CutSuffix(fs.filename, ext)
|
||||
backupFilename := name + "." + time.Now().Format("2006-01-02_150405") + ext
|
||||
if err := os.Rename(fs.filename, backupFilename); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("rename old state file: %w", err)
|
||||
}
|
||||
}
|
||||
if err := os.Rename(tmpFilename, fs.filename); err != nil {
|
||||
return fmt.Errorf("atomic rename of state file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user