storagenode/orders: begin implementation of file store for order limits
* Will replace order limits database. * This change adds functionality for storing and listing unsent orders. * The next change will add functionality for order archival after submission. Change-Id: Ic5e2abc63991513245b6851a968ff2f2e18ce48d
This commit is contained in:
parent
6b447a415b
commit
c8c0a42269
279
storagenode/orders/store.go
Normal file
279
storagenode/orders/store.go
Normal file
@ -0,0 +1,279 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package orders
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/pb"
|
||||
"storj.io/common/storj"
|
||||
)
|
||||
|
||||
const (
|
||||
unsentStagingFileName = "unsent-orders-staging"
|
||||
unsentReadyFilePrefix = "unsent-orders-ready-"
|
||||
)
|
||||
|
||||
// FileStore implements the orders.Store interface by appending orders to flat files.
|
||||
type FileStore struct {
|
||||
ordersDir string
|
||||
unsentDir string
|
||||
archiveDir string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewFileStore creates a new orders file store.
|
||||
func NewFileStore(ordersDir string) *FileStore {
|
||||
return &FileStore{
|
||||
ordersDir: ordersDir,
|
||||
unsentDir: filepath.Join(ordersDir, "unsent"),
|
||||
archiveDir: filepath.Join(ordersDir, "archive"),
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue inserts order to the list of orders needing to be sent to the satellite.
|
||||
func (store *FileStore) Enqueue(info *Info) (err error) {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
f, err := store.getUnsentStagingFile()
|
||||
if err != nil {
|
||||
return OrderError.Wrap(err)
|
||||
}
|
||||
defer func() {
|
||||
err = errs.Combine(err, f.Close())
|
||||
}()
|
||||
|
||||
err = writeLimit(f, info.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeOrder(f, info.Order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUnsentBySatellite returns orders that haven't been sent yet grouped by satellite.
|
||||
// It copies the staging file to a read-only "ready to send" file first.
|
||||
// It should never be called concurrently with DeleteReadyToSendFiles.
|
||||
func (store *FileStore) ListUnsentBySatellite() (infoMap map[storj.NodeID][]*Info, err error) {
|
||||
err = store.convertUnsentStagingToReady()
|
||||
if err != nil {
|
||||
return infoMap, err
|
||||
}
|
||||
|
||||
var errList error
|
||||
infoMap = make(map[storj.NodeID][]*Info)
|
||||
|
||||
err = filepath.Walk(store.unsentDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
errList = errs.Combine(errList, err)
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.Name() == unsentStagingFileName {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return OrderError.Wrap(err)
|
||||
}
|
||||
defer func() {
|
||||
err = errs.Combine(err, f.Close())
|
||||
}()
|
||||
|
||||
for {
|
||||
limit, err := readLimit(f)
|
||||
if err != nil {
|
||||
if errs.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
order, err := readOrder(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newInfo := &Info{
|
||||
Limit: limit,
|
||||
Order: order,
|
||||
}
|
||||
infoList := infoMap[limit.SatelliteId]
|
||||
infoList = append(infoList, newInfo)
|
||||
infoMap[limit.SatelliteId] = infoList
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errList = errs.Combine(errList, err)
|
||||
}
|
||||
|
||||
return infoMap, errList
|
||||
}
|
||||
|
||||
// DeleteReadyToSendFiles deletes all non-staging files in the "unsent" directory.
|
||||
// It should be called after the order limits have been sent.
|
||||
// It should never be called concurrently with ListUnsentBySatellite.
|
||||
func (store *FileStore) DeleteReadyToSendFiles() (err error) {
|
||||
var errList error
|
||||
|
||||
err = filepath.Walk(store.unsentDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
errList = errs.Combine(errList, OrderError.Wrap(err))
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if info.Name() == unsentStagingFileName {
|
||||
return nil
|
||||
}
|
||||
// delete all non-staging files
|
||||
return OrderError.Wrap(os.Remove(path))
|
||||
})
|
||||
if err != nil {
|
||||
errList = errs.Combine(errList, err)
|
||||
}
|
||||
|
||||
return errList
|
||||
}
|
||||
|
||||
// convertUnsentStagingToReady converts the unsent staging file to be read only, and renames it.
|
||||
func (store *FileStore) convertUnsentStagingToReady() error {
|
||||
// lock mutex so no one tries to write to the file while we do this
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
oldFileName := unsentStagingFileName
|
||||
oldFilePath := filepath.Join(store.unsentDir, oldFileName)
|
||||
if _, err := os.Stat(oldFilePath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// set file to readonly
|
||||
err := os.Chmod(oldFilePath, 0444)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// make new file suffix the current time in case there are other "ready" files already
|
||||
timeStr := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
newFilePath := filepath.Join(store.unsentDir, unsentReadyFilePrefix+timeStr)
|
||||
// rename file
|
||||
return os.Rename(oldFilePath, newFilePath)
|
||||
}
|
||||
|
||||
// getUnsentStagingFile creates or gets the order limit file for appending unsent orders to.
|
||||
// it expects the caller to lock the store's mutex before calling, and to handle closing the returned file.
|
||||
func (store *FileStore) getUnsentStagingFile() (*os.File, error) {
|
||||
if _, err := os.Stat(store.unsentDir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(store.unsentDir, 0700)
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
fileName := unsentStagingFileName
|
||||
filePath := filepath.Join(store.unsentDir, fileName)
|
||||
// create file if not exists or append
|
||||
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// writeLimit writes the size of the order limit bytes, followed by the order limit bytes.
|
||||
// it expects the caller to have locked the mutex.
|
||||
func writeLimit(f io.Writer, limit *pb.OrderLimit) error {
|
||||
limitSerialized, err := pb.Marshal(limit)
|
||||
if err != nil {
|
||||
return OrderError.Wrap(err)
|
||||
}
|
||||
|
||||
sizeBytes := [4]byte{}
|
||||
binary.LittleEndian.PutUint32(sizeBytes[:], uint32(len(limitSerialized)))
|
||||
if _, err = f.Write(sizeBytes[:]); err != nil {
|
||||
return OrderError.New("Error writing serialized limit size: %w", err)
|
||||
}
|
||||
|
||||
if _, err = f.Write(limitSerialized); err != nil {
|
||||
return OrderError.New("Error writing serialized limit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLimit reads the size of the limit followed by the serialized limit, and returns the unmarshalled limit.
|
||||
func readLimit(f io.Reader) (*pb.OrderLimit, error) {
|
||||
sizeBytes := [4]byte{}
|
||||
_, err := io.ReadFull(f, sizeBytes[:])
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
limitSize := binary.LittleEndian.Uint32(sizeBytes[:])
|
||||
limitSerialized := make([]byte, limitSize)
|
||||
_, err = io.ReadFull(f, limitSerialized)
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
limit := &pb.OrderLimit{}
|
||||
err = pb.Unmarshal(limitSerialized, limit)
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
return limit, nil
|
||||
}
|
||||
|
||||
// writeOrder writes the size of the order bytes, followed by the order bytes.
|
||||
// it expects the caller to have locked the mutex.
|
||||
func writeOrder(f io.Writer, order *pb.Order) error {
|
||||
orderSerialized, err := pb.Marshal(order)
|
||||
if err != nil {
|
||||
return OrderError.Wrap(err)
|
||||
}
|
||||
|
||||
sizeBytes := [4]byte{}
|
||||
binary.LittleEndian.PutUint32(sizeBytes[:], uint32(len(orderSerialized)))
|
||||
if _, err = f.Write(sizeBytes[:]); err != nil {
|
||||
return OrderError.New("Error writing serialized order size: %w", err)
|
||||
}
|
||||
if _, err = f.Write(orderSerialized); err != nil {
|
||||
return OrderError.New("Error writing serialized order: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readOrder reads the size of the order followed by the serialized order, and returns the unmarshalled order.
|
||||
func readOrder(f io.Reader) (*pb.Order, error) {
|
||||
sizeBytes := [4]byte{}
|
||||
_, err := io.ReadFull(f, sizeBytes[:])
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
orderSize := binary.LittleEndian.Uint32(sizeBytes[:])
|
||||
orderSerialized := make([]byte, orderSize)
|
||||
_, err = io.ReadFull(f, orderSerialized)
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
order := &pb.Order{}
|
||||
err = pb.Unmarshal(orderSerialized, order)
|
||||
if err != nil {
|
||||
return nil, OrderError.Wrap(err)
|
||||
}
|
||||
return order, nil
|
||||
}
|
155
storagenode/orders/store_test.go
Normal file
155
storagenode/orders/store_test.go
Normal file
@ -0,0 +1,155 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package orders_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"storj.io/common/pb"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/storj/storagenode/orders"
|
||||
)
|
||||
|
||||
func TestOrdersStore(t *testing.T) {
|
||||
ctx := testcontext.New(t)
|
||||
defer ctx.Cleanup()
|
||||
dirName := ctx.Dir("test-orders")
|
||||
|
||||
ordersStore := orders.NewFileStore(dirName)
|
||||
|
||||
numSatellites := 3
|
||||
serialsPerSat := 5
|
||||
originalInfos, err := storeNewOrders(ordersStore, numSatellites, serialsPerSat)
|
||||
require.NoError(t, err)
|
||||
|
||||
unsentMap, err := ordersStore.ListUnsentBySatellite()
|
||||
require.NoError(t, err)
|
||||
|
||||
// go through order limits and make sure information is accurate
|
||||
require.Len(t, unsentMap, numSatellites)
|
||||
for _, unsentSatList := range unsentMap {
|
||||
require.Len(t, unsentSatList, serialsPerSat)
|
||||
|
||||
for _, unsentInfo := range unsentSatList {
|
||||
sn := unsentInfo.Limit.SerialNumber
|
||||
originalInfo := originalInfos[sn]
|
||||
|
||||
verifyInfosEqual(t, unsentInfo, originalInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// add some more orders and list again
|
||||
// we should see everything we added before, plus the new ones
|
||||
newInfos, err := storeNewOrders(ordersStore, numSatellites, serialsPerSat)
|
||||
require.NoError(t, err)
|
||||
for sn, info := range newInfos {
|
||||
originalInfos[sn] = info
|
||||
}
|
||||
// because we have stored two times, we have twice as many serials...
|
||||
require.Len(t, originalInfos, 2*numSatellites*serialsPerSat)
|
||||
|
||||
unsentMap, err = ordersStore.ListUnsentBySatellite()
|
||||
require.NoError(t, err)
|
||||
// ...and twice as many satellites.
|
||||
require.Len(t, unsentMap, 2*numSatellites)
|
||||
for _, unsentSatList := range unsentMap {
|
||||
require.Len(t, unsentSatList, serialsPerSat)
|
||||
|
||||
for _, unsentInfo := range unsentSatList {
|
||||
sn := unsentInfo.Limit.SerialNumber
|
||||
originalInfo := originalInfos[sn]
|
||||
|
||||
verifyInfosEqual(t, unsentInfo, originalInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// now, add another order, delete ready to send files, and list
|
||||
// we should only see the new order
|
||||
originalInfos, err = storeNewOrders(ordersStore, 1, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ordersStore.DeleteReadyToSendFiles()
|
||||
require.NoError(t, err)
|
||||
|
||||
unsentMap, err = ordersStore.ListUnsentBySatellite()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, unsentMap, 1)
|
||||
for _, unsentSatList := range unsentMap {
|
||||
require.Len(t, unsentSatList, 1)
|
||||
for _, unsentInfo := range unsentSatList {
|
||||
sn := unsentInfo.Limit.SerialNumber
|
||||
originalInfo := originalInfos[sn]
|
||||
|
||||
verifyInfosEqual(t, unsentInfo, originalInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO test order archival
|
||||
}
|
||||
|
||||
func verifyInfosEqual(t *testing.T, a, b *orders.Info) {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, a)
|
||||
require.NotNil(t, b)
|
||||
|
||||
require.Equal(t, a.Limit.SerialNumber, b.Limit.SerialNumber)
|
||||
require.Equal(t, a.Limit.SatelliteId, b.Limit.SatelliteId)
|
||||
require.Equal(t, a.Limit.OrderExpiration.UTC(), b.Limit.OrderExpiration.UTC())
|
||||
require.Equal(t, a.Limit.Action, b.Limit.Action)
|
||||
|
||||
require.Equal(t, a.Order.Amount, b.Order.Amount)
|
||||
require.Equal(t, a.Order.SerialNumber, b.Order.SerialNumber)
|
||||
|
||||
}
|
||||
|
||||
func storeNewOrders(ordersStore *orders.FileStore, numSatellites, numOrdersPerSatellite int) (map[storj.SerialNumber]*orders.Info, error) {
|
||||
actions := []pb.PieceAction{
|
||||
pb.PieceAction_GET,
|
||||
pb.PieceAction_PUT_REPAIR,
|
||||
pb.PieceAction_GET_AUDIT,
|
||||
}
|
||||
originalInfos := make(map[storj.SerialNumber]*orders.Info)
|
||||
for i := 0; i < numSatellites; i++ {
|
||||
satellite := testrand.NodeID()
|
||||
|
||||
// for each satellite, half of the orders will expire in an hour
|
||||
// and half will expire in three hours.
|
||||
for j := 0; j < numOrdersPerSatellite; j++ {
|
||||
expiration := time.Now().Add(time.Hour)
|
||||
if j < 2 {
|
||||
expiration = time.Now().Add(3 * time.Hour)
|
||||
}
|
||||
amount := testrand.Int63n(1000)
|
||||
sn := testrand.SerialNumber()
|
||||
action := actions[j%len(actions)]
|
||||
|
||||
newInfo := &orders.Info{
|
||||
Limit: &pb.OrderLimit{
|
||||
SerialNumber: sn,
|
||||
SatelliteId: satellite,
|
||||
Action: action,
|
||||
OrderExpiration: expiration,
|
||||
},
|
||||
Order: &pb.Order{
|
||||
SerialNumber: sn,
|
||||
Amount: amount,
|
||||
},
|
||||
}
|
||||
originalInfos[sn] = newInfo
|
||||
|
||||
// store the new info in the orders store
|
||||
err := ordersStore.Enqueue(newInfo)
|
||||
if err != nil {
|
||||
return originalInfos, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalInfos, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user