mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-24 15:10:19 +00:00
* Config: Move assetEnabled upgrade to Version management * Assets: Do not error on asset not enabled, or disabled This became more messy with Disabling something that's defaulted to disabled. Taking an idealogical stance against erroring that what you want to have done is already done. * CurrencyManager: Set AssetEnabled when StorePairs(enabled) * RPCServer: Fix tests expecting StoreAssetPairFormat to enable the asset Also assertifies * Bitfinex: Fix tests for MarginFunding subs * GCTWrapper: Improve TestMain clarity * BTSE: Add futures to testconfig * Exchanges: Rename StoreAssetPairStore Previously we were calling it "Format", but accepting everything from the PairStore. We were also defaulting to turning the Asset on. Now callers need to get their AssetEnabled set as they want it, so there's no magic This change also moves responsibility for error wrapping outside to the caller. * Config: AssetEnabled upgrade should respect assetTypes Previously we ignored the field and just turned on everything. I think that was because we couldn't get at the old value. In either case, we have the option to do better, and respect the assetEnabled value * Config: Improve exchange config version upgrade error messages
247 lines
7.9 KiB
Go
247 lines
7.9 KiB
Go
// Package versions handles config upgrades and downgrades
|
|
/*
|
|
- Versions must be stateful, and not rely upon type definitions in the config pkg
|
|
|
|
- Instead versions should localise types into vN/types.go to avoid issues with subsequent changes
|
|
|
|
- Versions must upgrade to the next version. Do not retrospectively change versions to match new type changes. Create a new version
|
|
|
|
- Versions must implement ExchangeVersion or ConfigVersion, and may implement both
|
|
*/
|
|
package versions
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json" //nolint:depguard // Used instead of gct encoding/json so that we can ensure consistent library functionality between versions
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/buger/jsonparser"
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
)
|
|
|
|
// UseLatestVersion used as version param to Deploy to automatically use the latest version
|
|
const UseLatestVersion = math.MaxUint16
|
|
|
|
var (
|
|
errMissingVersion = errors.New("missing version")
|
|
errVersionIncompatible = errors.New("version does not implement ConfigVersion or ExchangeVersion")
|
|
errModifyingExchange = errors.New("error modifying exchange config")
|
|
errNoVersions = errors.New("error retrieving latest config version: No config versions are registered")
|
|
errApplyingVersion = errors.New("error applying version")
|
|
errTargetVersion = errors.New("target downgrade version is higher than the latest available version")
|
|
errConfigVersion = errors.New("invalid version in config")
|
|
errConfigVersionUnavail = errors.New("version is higher than the latest available version")
|
|
errConfigVersionNegative = errors.New("version is negative")
|
|
errConfigVersionMax = errors.New("version is above max versions")
|
|
)
|
|
|
|
// ConfigVersion is a version that affects the general configuration
|
|
type ConfigVersion interface {
|
|
UpgradeConfig(context.Context, []byte) ([]byte, error)
|
|
DowngradeConfig(context.Context, []byte) ([]byte, error)
|
|
}
|
|
|
|
// ExchangeVersion is a version that affects specific exchange configurations
|
|
type ExchangeVersion interface {
|
|
Exchanges() []string // Use `*` for all exchanges
|
|
UpgradeExchange(context.Context, []byte) ([]byte, error)
|
|
DowngradeExchange(context.Context, []byte) ([]byte, error)
|
|
}
|
|
|
|
// manager contains versions registerVersioned during import init
|
|
type manager struct {
|
|
m sync.RWMutex
|
|
versions []any
|
|
}
|
|
|
|
// Manager is a public instance of the config version manager
|
|
var Manager = &manager{}
|
|
|
|
// Deploy upgrades or downgrades the config between versions
|
|
// Pass UseLatestVersion for version to use the latest version automatically
|
|
// Prints an error an exits if the config file version or version param is not registered
|
|
func (m *manager) Deploy(ctx context.Context, j []byte, version uint16) ([]byte, error) {
|
|
if err := m.checkVersions(); err != nil {
|
|
return j, err
|
|
}
|
|
|
|
latest, err := m.latest()
|
|
if err != nil {
|
|
return j, err
|
|
}
|
|
|
|
target := latest
|
|
if version != UseLatestVersion {
|
|
target = version
|
|
}
|
|
|
|
m.m.RLock()
|
|
defer m.m.RUnlock()
|
|
|
|
current64, err := jsonparser.GetInt(j, "version")
|
|
switch {
|
|
case errors.Is(err, jsonparser.KeyPathNotFoundError):
|
|
// With no version first upgrade is to Version1; current64 is already 0
|
|
case err != nil:
|
|
return j, fmt.Errorf("%w: %w `version`: %w", errConfigVersion, common.ErrGettingField, err)
|
|
case current64 < 0:
|
|
return j, fmt.Errorf("%w: %w `version`: `%d`", errConfigVersion, errConfigVersionNegative, current64)
|
|
case current64 >= UseLatestVersion:
|
|
return j, fmt.Errorf("%w: %w `version`: `%d`", errConfigVersion, errConfigVersionMax, current64)
|
|
}
|
|
current := uint16(current64)
|
|
|
|
switch {
|
|
case target == current:
|
|
return j, nil
|
|
case latest < current:
|
|
err := fmt.Errorf("%w: %w", errConfigVersion, errConfigVersionUnavail)
|
|
warnVersionNotRegistered(current, latest, err)
|
|
return j, err
|
|
case target > latest:
|
|
warnVersionNotRegistered(target, latest, errTargetVersion)
|
|
return j, errTargetVersion
|
|
}
|
|
|
|
for current != target {
|
|
patchVersion := current + 1
|
|
action := "upgrade to"
|
|
configMethod := ConfigVersion.UpgradeConfig
|
|
exchMethod := ExchangeVersion.UpgradeExchange
|
|
|
|
if target < current {
|
|
patchVersion = current
|
|
action = "downgrade from"
|
|
configMethod = ConfigVersion.DowngradeConfig
|
|
exchMethod = ExchangeVersion.DowngradeExchange
|
|
}
|
|
|
|
log.Printf("Running %s config version %v\n", action, patchVersion)
|
|
|
|
patch := m.versions[patchVersion]
|
|
|
|
if cPatch, ok := patch.(ConfigVersion); ok {
|
|
if j, err = configMethod(cPatch, ctx, j); err != nil {
|
|
return j, fmt.Errorf("%w %s %v: %w", errApplyingVersion, action, patchVersion, err)
|
|
}
|
|
}
|
|
|
|
if ePatch, ok := patch.(ExchangeVersion); ok {
|
|
if j, err = exchangeDeploy(ctx, ePatch, exchMethod, j); err != nil {
|
|
return j, fmt.Errorf("%w %s %v: %w", errApplyingVersion, action, patchVersion, err)
|
|
}
|
|
}
|
|
|
|
current = patchVersion
|
|
if target < current {
|
|
current = patchVersion - 1
|
|
}
|
|
|
|
if j, err = jsonparser.Set(j, []byte(strconv.FormatUint(uint64(current), 10)), "version"); err != nil {
|
|
return j, fmt.Errorf("%w `version` during %s %v: %w", common.ErrSettingField, action, patchVersion, err)
|
|
}
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
if err = json.Indent(&out, j, "", " "); err != nil {
|
|
return j, fmt.Errorf("error formatting json: %w", err)
|
|
}
|
|
|
|
log.Println("Version management finished")
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
func exchangeDeploy(ctx context.Context, patch ExchangeVersion, method func(ExchangeVersion, context.Context, []byte) ([]byte, error), j []byte) ([]byte, error) {
|
|
var errs error
|
|
wanted := patch.Exchanges()
|
|
var i int
|
|
eFunc := func(exchOrig []byte, _ jsonparser.ValueType, _ int, _ error) {
|
|
defer func() { i++ }()
|
|
name, err := jsonparser.GetString(exchOrig, "name")
|
|
if err != nil {
|
|
errs = common.AppendError(errs, fmt.Errorf("%w [%d]: %w `name`: %w", errModifyingExchange, i, common.ErrGettingField, err))
|
|
return
|
|
}
|
|
for _, want := range wanted {
|
|
if want != "*" && want != name {
|
|
continue
|
|
}
|
|
exchNew, err := method(patch, ctx, exchOrig)
|
|
if err != nil {
|
|
errs = common.AppendError(errs, fmt.Errorf("%w for `%s`: %w", errModifyingExchange, name, err))
|
|
continue
|
|
}
|
|
if !bytes.Equal(exchNew, exchOrig) {
|
|
if j, err = jsonparser.Set(j, exchNew, "exchanges", "["+strconv.Itoa(i)+"]"); err != nil {
|
|
errs = common.AppendError(errs, fmt.Errorf("%w `%s`/exchanges[%d]: %w: %w", errModifyingExchange, name, i, common.ErrSettingField, err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
v, dataType, _, err := jsonparser.Get(j, "exchanges")
|
|
switch {
|
|
case errors.Is(err, jsonparser.KeyPathNotFoundError), dataType != jsonparser.Array:
|
|
return j, nil
|
|
case err != nil:
|
|
return j, fmt.Errorf("%w: %w `exchanges`: %w", errModifyingExchange, common.ErrGettingField, err)
|
|
}
|
|
if _, err := jsonparser.ArrayEach(bytes.Clone(v), eFunc); err != nil {
|
|
return j, err
|
|
}
|
|
return j, errs
|
|
}
|
|
|
|
// registerVersion takes instances of config versions and adds them to the registry
|
|
func (m *manager) registerVersion(ver int, v any) {
|
|
m.m.Lock()
|
|
defer m.m.Unlock()
|
|
if ver >= len(m.versions) {
|
|
m.versions = slices.Grow(m.versions, ver+1)[:ver+1]
|
|
}
|
|
m.versions[ver] = v
|
|
}
|
|
|
|
// latest returns the highest version number
|
|
func (m *manager) latest() (uint16, error) {
|
|
m.m.RLock()
|
|
defer m.m.RUnlock()
|
|
if len(m.versions) == 0 {
|
|
return 0, errNoVersions
|
|
}
|
|
return uint16(len(m.versions)) - 1, nil //nolint:gosec // Ignore this as we hardcode version numbers
|
|
}
|
|
|
|
// checkVersions ensures that registered versions are consistent
|
|
func (m *manager) checkVersions() error {
|
|
m.m.RLock()
|
|
defer m.m.RUnlock()
|
|
for ver, v := range m.versions {
|
|
switch v.(type) {
|
|
case ExchangeVersion, ConfigVersion:
|
|
default:
|
|
return fmt.Errorf("%w: %v", errVersionIncompatible, ver)
|
|
}
|
|
if v == nil {
|
|
return fmt.Errorf("%w: v%v", errMissingVersion, ver)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func warnVersionNotRegistered(current, latest uint16, msg error) {
|
|
fmt.Fprintf(os.Stderr, `
|
|
%s ('%d' > '%d')
|
|
Switch back to the version of GoCryptoTrader containing config version '%d' and run:
|
|
$ ./cmd/config downgrade %d
|
|
`, msg, current, latest, current, latest)
|
|
}
|