Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/curio/guidedsetup/guidedsetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ type MigrationData struct {
selectTemplates *promptui.SelectTemplates
MinerConfigPath string
DB *harmonydb.DB
HarmonyCfg config.HarmonyDB
HarmonyCfg harmonydb.Config
MinerID address.Address
full api.Chain
cctx *cli.Context
Expand Down
120 changes: 120 additions & 0 deletions deps/config/dynamic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package config

import (
"context"
"fmt"
"reflect"
"strings"
"sync"
"time"

"github.com/BurntSushi/toml"
"github.com/filecoin-project/curio/harmony/harmonydb"
logging "github.com/ipfs/go-log/v2"
)

var logger = logging.Logger("config-dynamic")
var DynamicMx sync.RWMutex

type Dynamic[T any] struct {
Value T
}

func NewDynamic[T any](value T) *Dynamic[T] {
return &Dynamic[T]{Value: value}
}

func (d *Dynamic[T]) Set(value T) {
DynamicMx.Lock()
defer DynamicMx.Unlock()
d.Value = value
}

func (d *Dynamic[T]) Get() T {
DynamicMx.RLock()
defer DynamicMx.RUnlock()
return d.Value
}

func (d *Dynamic[T]) UnmarshalText(text []byte) error {
DynamicMx.Lock()
defer DynamicMx.Unlock()
return toml.Unmarshal(text, d.Value)
}

type cfgRoot struct {
db *harmonydb.DB
layers []string
treeCopy *CurioConfig
}

func EnableChangeDetection(db *harmonydb.DB, obj *CurioConfig, layers []string) error {
r := &cfgRoot{db: db, treeCopy: obj, layers: layers}
err := r.copyWithOriginalDynamics(obj)
if err != nil {
return err
}
go r.changeMonitor()
return nil
}

// copyWithOriginalDynamics copies the original dynamics from the original object to the new object.
func (r *cfgRoot) copyWithOriginalDynamics(orig *CurioConfig) error {
typ := reflect.TypeOf(orig)
if typ.Kind() != reflect.Struct {
return fmt.Errorf("expected struct, got %s", typ.Kind())
}
result := reflect.New(typ)
// recursively walk the struct tree, and copy the dynamics from the original object to the new object.
var walker func(orig, result reflect.Value)
walker = func(orig, result reflect.Value) {
for i := 0; i < orig.NumField(); i++ {
field := orig.Field(i)
if field.Kind() == reflect.Struct {
walker(field, result.Field(i))
} else if field.Kind() == reflect.Ptr {
walker(field.Elem(), result.Field(i).Elem())
} else if field.Kind() == reflect.Interface {
walker(field.Elem(), result.Field(i).Elem())
} else {
result.Field(i).Set(field)
}
}
}
walker(reflect.ValueOf(orig), result)
r.treeCopy = result.Interface().(*CurioConfig)
return nil
}

func (r *cfgRoot) changeMonitor() {
lastTimestamp := time.Now().Add(-30 * time.Second) // plenty of time for start-up
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer some global flag for this. Large storage servers with millions of files on hard drives may need a few minutes for proper startup


for {
time.Sleep(30 * time.Second)
configCount := 0
err := r.db.QueryRow(context.Background(), `SELECT COUNT(*) FROM harmony_config WHERE timestamp > $1 AND title IN ($2)`, lastTimestamp, strings.Join(r.layers, ",")).Scan(&configCount)
if err != nil {
logger.Errorf("error selecting configs: %s", err)
continue
}
if configCount == 0 {
continue
}
lastTimestamp = time.Now()

// 1. get all configs
configs, err := GetConfigs(context.Background(), r.db, r.layers)
if err != nil {
logger.Errorf("error getting configs: %s", err)
continue
}

// 2. lock "dynamic" mutex
func() {
DynamicMx.Lock()
defer DynamicMx.Unlock()
ApplyLayers(context.Background(), r.treeCopy, configs)

Check failure on line 116 in deps/config/dynamic.go

View workflow job for this annotation

GitHub Actions / lint

Error return value is not checked (errcheck)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linter somewhat rightfully complains about a missing err check

}()
DynamicMx.Lock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray lock?

}
}
75 changes: 75 additions & 0 deletions deps/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package config

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"math/big"
Expand All @@ -14,9 +16,11 @@ import (
"unicode"

"github.com/BurntSushi/toml"
"github.com/filecoin-project/curio/harmony/harmonydb"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/kelseyhightower/envconfig"
"github.com/samber/lo"
"golang.org/x/xerrors"
)

Expand Down Expand Up @@ -564,3 +568,74 @@ func FixTOML(newText string, cfg *CurioConfig) error {
}
return nil
}

func LoadConfigWithUpgrades(text string, curioConfigWithDefaults *CurioConfig) (toml.MetaData, error) {
// allow migration from old config format that was limited to 1 wallet setup.
newText := strings.Join(lo.Map(strings.Split(text, "\n"), func(line string, _ int) string {
if strings.EqualFold(line, "[addresses]") {
return "[[addresses]]"
}
return line
}), "\n")

err := FixTOML(newText, curioConfigWithDefaults)
if err != nil {
return toml.MetaData{}, err
}

return toml.Decode(newText, &curioConfigWithDefaults)
}

type ConfigText struct {
Title string
Config string
}

// GetConfigs returns the configs in the order of the layers
func GetConfigs(ctx context.Context, db *harmonydb.DB, layers []string) ([]ConfigText, error) {
inputMap := map[string]int{}
for i, layer := range layers {
inputMap[layer] = i
}

layers = append([]string{"base"}, layers...) // Always stack on top of "base" layer

var configs []ConfigText
err := db.Select(ctx, &configs, `SELECT title, config FROM harmony_config WHERE title IN ($1)`, strings.Join(layers, ","))
if err != nil {
return nil, err
}
result := make([]ConfigText, len(layers))
for _, config := range configs {
index, ok := inputMap[config.Title]
if !ok {
if config.Title == "base" {
return nil, errors.New(`curio defaults to a layer named 'base'.
Either use 'migrate' command or edit a base.toml and upload it with: curio config set base.toml`)

}
return nil, fmt.Errorf("missing layer %s", config.Title)
}
result[index] = config
}
return result, nil
}

func ApplyLayers(ctx context.Context, curioConfig *CurioConfig, layers []ConfigText) error {
have := []string{}
for _, layer := range layers {
meta, err := LoadConfigWithUpgrades(layer.Config, curioConfig)
if err != nil {
return fmt.Errorf("could not read layer, bad toml %s: %w", layer, err)
}
for _, k := range meta.Keys() {
have = append(have, strings.Join(k, " "))
}
logger.Debugf("Using layer %s, config %v", layer, curioConfig)
}
_ = have // FUTURE: verify that required fields are here.
// If config includes 3rd-party config, consider JSONSchema as a way that
// 3rd-parties can dynamically include config requirements and we can
// validate the config. Because of layering, we must validate @ startup.
return nil
}
26 changes: 3 additions & 23 deletions deps/config/old_lotus_miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/ipfs/go-cid"

"github.com/filecoin-project/curio/harmony/harmonydb"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/builtin"
"github.com/filecoin-project/go-state-types/network"
Expand All @@ -14,27 +15,6 @@ import (
"github.com/filecoin-project/lotus/chain/types"
)

type HarmonyDB struct {
// HOSTS is a list of hostnames to nodes running YugabyteDB
// in a cluster. Only 1 is required
Hosts []string

// The Yugabyte server's username with full credentials to operate on Lotus' Database. Blank for default.
Username string

// The password for the related username. Blank for default.
Password string

// The database (logical partition) within Yugabyte. Blank for default.
Database string

// The port to find Yugabyte. Blank for default.
Port string

// Load Balance the connection over multiple nodes
LoadBalance bool
}

// StorageMiner is a miner config
type StorageMiner struct {
Common
Expand All @@ -49,7 +29,7 @@ type StorageMiner struct {
Addresses MinerAddressConfig
DAGStore DAGStoreConfig

HarmonyDB HarmonyDB
HarmonyDB harmonydb.Config
}

type DAGStoreConfig struct {
Expand Down Expand Up @@ -683,7 +663,7 @@ func DefaultStorageMiner() *StorageMiner {
MaxConcurrentUnseals: 5,
GCInterval: time.Minute,
},
HarmonyDB: HarmonyDB{
HarmonyDB: harmonydb.Config{
Hosts: []string{"127.0.0.1"},
Username: "yugabyte",
Password: "yugabyte",
Expand Down
64 changes: 19 additions & 45 deletions deps/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ var log = logging.Logger("curio/deps")
func MakeDB(cctx *cli.Context) (*harmonydb.DB, error) {
// #1 CLI opts
fromCLI := func() (*harmonydb.DB, error) {
dbConfig := config.HarmonyDB{
dbConfig := harmonydb.Config{
Username: cctx.String("db-user"),
Password: cctx.String("db-password"),
Hosts: strings.Split(cctx.String("db-host"), ","),
Expand Down Expand Up @@ -261,7 +261,7 @@ func (deps *Deps) PopulateRemainingDeps(ctx context.Context, cctx *cli.Context,
}

if deps.EthClient == nil {
deps.EthClient = lazy.MakeLazy[*ethclient.Client](func() (*ethclient.Client, error) {
deps.EthClient = lazy.MakeLazy(func() (*ethclient.Client, error) {
cfgApiInfo := deps.Cfg.Apis.ChainApiInfo
if v := os.Getenv("FULLNODE_API_INFO"); v != "" {
cfgApiInfo = []string{v}
Expand Down Expand Up @@ -419,20 +419,7 @@ func sealProofType(maddr dtypes.MinerAddress, fnapi api.Chain) (abi.RegisteredSe
}

func LoadConfigWithUpgrades(text string, curioConfigWithDefaults *config.CurioConfig) (toml.MetaData, error) {
// allow migration from old config format that was limited to 1 wallet setup.
newText := strings.Join(lo.Map(strings.Split(text, "\n"), func(line string, _ int) string {
if strings.EqualFold(line, "[addresses]") {
return "[[addresses]]"
}
return line
}), "\n")

err := config.FixTOML(newText, curioConfigWithDefaults)
if err != nil {
return toml.MetaData{}, err
}

return toml.Decode(newText, &curioConfigWithDefaults)
return config.LoadConfigWithUpgrades(text, curioConfigWithDefaults)
}

func GetConfig(ctx context.Context, layers []string, db *harmonydb.DB) (*config.CurioConfig, error) {
Expand All @@ -442,38 +429,25 @@ func GetConfig(ctx context.Context, layers []string, db *harmonydb.DB) (*config.
}

curioConfig := config.DefaultCurioConfig()
have := []string{}
layers = append([]string{"base"}, layers...) // Always stack on top of "base" layer
for _, layer := range layers {
text := ""
err := db.QueryRow(ctx, `SELECT config FROM harmony_config WHERE title=$1`, layer).Scan(&text)
if err != nil {
if strings.Contains(err.Error(), pgx.ErrNoRows.Error()) {
return nil, fmt.Errorf("missing layer '%s' ", layer)
}
if layer == "base" {
return nil, errors.New(`curio defaults to a layer named 'base'.
Either use 'migrate' command or edit a base.toml and upload it with: curio config set base.toml`)
}
return nil, fmt.Errorf("could not read layer '%s': %w", layer, err)
}

meta, err := LoadConfigWithUpgrades(text, curioConfig)
if err != nil {
return curioConfig, fmt.Errorf("could not read layer, bad toml %s: %w", layer, err)
}
for _, k := range meta.Keys() {
have = append(have, strings.Join(k, " "))
}
log.Debugw("Using layer", "layer", layer, "config", curioConfig)
err = ApplyLayers(ctx, db, curioConfig, layers)
if err != nil {
return nil, err
}
err = config.EnableChangeDetection(db, curioConfig, layers)
if err != nil {
return nil, err
}
_ = have // FUTURE: verify that required fields are here.
// If config includes 3rd-party config, consider JSONSchema as a way that
// 3rd-parties can dynamically include config requirements and we can
// validate the config. Because of layering, we must validate @ startup.
return curioConfig, nil
}

func ApplyLayers(ctx context.Context, db *harmonydb.DB, curioConfig *config.CurioConfig, layers []string) error {
configs, err := config.GetConfigs(ctx, db, layers)
if err != nil {
return err
}
return config.ApplyLayers(ctx, curioConfig, configs)
}

func updateBaseLayer(ctx context.Context, db *harmonydb.DB) error {
_, err := db.BeginTransaction(ctx, func(tx *harmonydb.Tx) (commit bool, err error) {
// Get existing base from DB
Expand Down Expand Up @@ -631,7 +605,7 @@ func GetAPI(ctx context.Context, cctx *cli.Context) (*harmonydb.DB, *config.Curi
return nil, nil, nil, nil, nil, err
}

ethClient := lazy.MakeLazy[*ethclient.Client](func() (*ethclient.Client, error) {
ethClient := lazy.MakeLazy(func() (*ethclient.Client, error) {
return GetEthClient(cctx, cfgApiInfo)
})

Expand Down
Loading
Loading