Include latest and earliest record times in state (#34)
Badges / coveralls (push) Successful in 1m1s

Reviewed-on: #34
Co-authored-by: Natercio Moniz <[email protected]>
This commit was merged in pull request #34.
This commit is contained in:
natercio authored and Natercio Moniz committed 2026-10-12 23:09:39 +01:00
1 parent 47f38fce2b
commit 41b287c804
3 files changed
+47 -11

No files matched your search

+19 -6
View File
@@ -96,11 +96,9 @@ func (fs *JSONStore) Load(ctx context.Context) (map[string]*FillerQueue, error)
// the same directory followed by an atomic rename so a crash mid-write cannot // the same directory followed by an atomic rename so a crash mid-write cannot
// leave a half-written state file. // leave a half-written state file.
func (fs *JSONStore) Save(ctx context.Context, queue map[string]*FillerQueue) error { func (fs *JSONStore) Save(ctx context.Context, queue map[string]*FillerQueue) error {
state := State{ queues := make(map[string][]persistedFiller)
Version: StateVersion, latest := time.Time{}
Platform: fs.platform, earliest := time.Time{}
Queues: make(map[string][]persistedFiller),
}
for symbol, q := range queue { for symbol, q := range queue {
if q == nil || q.Len() == 0 { if q == nil || q.Len() == 0 {
@@ -119,8 +117,23 @@ func (fs *JSONStore) Save(ctx context.Context, queue map[string]*FillerQueue) er
Price: f.Price(), Price: f.Price(),
Filled: f.Filled(), Filled: f.Filled(),
}) })
if f.Timestamp().After(latest) {
latest = f.Timestamp()
}
if f.Timestamp().Before(earliest) || earliest.IsZero() {
earliest = f.Timestamp()
}
} }
state.Queues[symbol] = persisted queues[symbol] = persisted
}
state := State{
Version: StateVersion,
Platform: fs.platform,
Queues: queues,
Latest: latest,
Earliest: earliest,
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
+25 -5
View File
@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"github.com/nmoniz/any2anexoj/internal" "github.com/nmoniz/any2anexoj/internal"
"github.com/nmoniz/any2anexoj/internal/mocks" "github.com/nmoniz/any2anexoj/internal/mocks"
@@ -84,13 +85,25 @@ func TestJSONStore_RoundTrip(t *testing.T) {
// confirming the inline form is parseable guards against accidentally // confirming the inline form is parseable guards against accidentally
// double-encoding it again in the future). // double-encoding it again in the future).
var parsed struct { var parsed struct {
Queues map[string][]struct { Latest time.Time `json:"latest"`
Earliest time.Time `json:"earliest"`
Queues map[string][]struct {
RecordData json.RawMessage `json:"record_data"` RecordData json.RawMessage `json:"record_data"`
} `json:"queues"` } `json:"queues"`
} }
if err := json.Unmarshal(data, &parsed); err != nil { if err := json.Unmarshal(data, &parsed); err != nil {
t.Fatalf("saved state file is not valid JSON: %v", err) t.Fatalf("saved state file is not valid JSON: %v", err)
} }
// The latest/earliest bounds must be persisted alongside the queues.
// Every record in this test returns testRecordTime, so both bounds must
// equal it exactly.
if !parsed.Latest.Equal(testRecordTime) {
t.Errorf("state latest = %v; want %v", parsed.Latest, testRecordTime)
}
if !parsed.Earliest.Equal(testRecordTime) {
t.Errorf("state earliest = %v; want %v", parsed.Earliest, testRecordTime)
}
for symbol, lots := range parsed.Queues { for symbol, lots := range parsed.Queues {
for i, lot := range lots { for i, lot := range lots {
if len(lot.RecordData) == 0 { if len(lot.RecordData) == 0 {
@@ -341,13 +354,20 @@ func TestNewJSONStore_ValidatesArguments(t *testing.T) {
} }
} }
// newRecord builds a MockRecord whose Symbol() returns the given symbol. // testRecordTime is the deterministic timestamp returned by every record
// The JSONStore only reads Symbol() off the loaded Record during tests // built in this file. Using a fixed value keeps the latest/earliest bounds
// (quantity/price/filled come from the persisted struct fields), so all // written by JSONStore.Save predictable and assertable.
// other Record methods can be left as default-mocked values. var testRecordTime = time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
// newRecord builds a MockRecord whose Symbol() returns the given symbol and
// whose Timestamp() returns testRecordTime. The JSONStore only reads Symbol()
// and Timestamp() off the Record during tests (quantity/price/filled come
// from the persisted struct fields), so all other Record methods can be left
// as default-mocked values.
func newRecord(ctrl *gomock.Controller, symbol string) *mocks.MockRecord { func newRecord(ctrl *gomock.Controller, symbol string) *mocks.MockRecord {
r := mocks.NewMockRecord(ctrl) r := mocks.NewMockRecord(ctrl)
r.EXPECT().Symbol().Return(symbol).AnyTimes() r.EXPECT().Symbol().Return(symbol).AnyTimes()
r.EXPECT().Timestamp().Return(testRecordTime).AnyTimes()
return r return r
} }
+3
View File
@@ -3,6 +3,7 @@ package internal
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"time"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
) )
@@ -15,6 +16,8 @@ const StateVersion = "2"
type State struct { type State struct {
Version string `json:"version"` Version string `json:"version"`
Platform string `json:"platform"` Platform string `json:"platform"`
Latest time.Time `json:"latest"`
Earliest time.Time `json:"earliest"`
Queues map[string][]persistedFiller `json:"queues"` Queues map[string][]persistedFiller `json:"queues"`
} }