frostfs-node/pkg/morph/client/constructor.go
Evgenii Stratonikov a4da1da767
All checks were successful
Vulncheck / Vulncheck (push) Successful in 1m19s
Pre-commit hooks / Pre-commit (push) Successful in 1m41s
Build / Build Components (push) Successful in 1m43s
Tests and linters / Run gofumpt (push) Successful in 3m38s
Tests and linters / gopls check (push) Successful in 3m41s
Tests and linters / Lint (push) Successful in 3m50s
Tests and linters / Staticcheck (push) Successful in 4m6s
Tests and linters / Tests with -race (push) Successful in 4m22s
Tests and linters / Tests (push) Successful in 4m32s
OCI image / Build container images (push) Successful in 5m0s
[#905] morph/client: Fetch NNS hash once on init
NNS contract hash is taken from the contract with ID=1.
Because morph client is expected to work with the same chain,
and because contract hash doesn't change on update, there is no need to
fetch it from each new endpoint.

Change-Id: Ic6dc18283789da076d6a0b3701139b97037714cc
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2025-03-21 15:13:54 +00:00

319 lines
8.2 KiB
Go

package client
import (
"context"
"errors"
"fmt"
"net"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/metrics"
morphmetrics "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/metrics"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"go.uber.org/zap"
)
// Option is a client configuration change function.
type Option func(*cfg)
// Callback is a function that is going to be called
// on certain Client's state.
type Callback func()
// groups the configurations with default values.
type cfg struct {
dialTimeout time.Duration // client dial timeout
logger *logger.Logger // logging component
metrics morphmetrics.Register
waitInterval time.Duration
signer *transaction.Signer
endpoints []Endpoint
inactiveModeCb Callback
switchInterval time.Duration
morphCacheMetrics metrics.MorphCacheMetrics
dialerSource DialerSource
}
const (
defaultDialTimeout = 5 * time.Second
defaultWaitInterval = 500 * time.Millisecond
)
var ErrNoHealthyEndpoint = errors.New("no healthy endpoint")
func defaultConfig() *cfg {
return &cfg{
dialTimeout: defaultDialTimeout,
logger: logger.NewLoggerWrapper(zap.L()),
metrics: morphmetrics.NoopRegister{},
waitInterval: defaultWaitInterval,
signer: &transaction.Signer{
Scopes: transaction.Global,
},
morphCacheMetrics: &morphmetrics.NoopMorphCacheMetrics{},
dialerSource: &noopDialerSource{},
}
}
// New creates, initializes and returns the Client instance.
// Notary support should be enabled with EnableNotarySupport client
// method separately.
//
// If private key is nil, it panics.
//
// Other values are set according to provided options, or by default:
// - client context: Background;
// - dial timeout: 5s;
// - blockchain network type: netmode.PrivNet;
// - signer with the global scope;
// - wait interval: 500ms;
// - logger: &logger.Logger{Logger: zap.L()}.
// - metrics: metrics.NoopRegister
//
// If desired option satisfies the default value, it can be omitted.
// If multiple options of the same config value are supplied,
// the option with the highest index in the arguments will be used.
// If the list of endpoints provided - uses first alive.
// If there are no healthy endpoint - returns ErrNoHealthyEndpoint.
func New(ctx context.Context, key *keys.PrivateKey, opts ...Option) (*Client, error) {
if key == nil {
panic("empty private key")
}
acc := wallet.NewAccountFromPrivateKey(key)
accAddr := key.GetScriptHash()
// build default configuration
cfg := defaultConfig()
// apply options
for _, opt := range opts {
opt(cfg)
}
if len(cfg.endpoints) == 0 {
return nil, errors.New("no endpoints were provided")
}
cli := &Client{
cache: newClientCache(cfg.morphCacheMetrics),
logger: cfg.logger,
metrics: cfg.metrics,
acc: acc,
accAddr: accAddr,
cfg: *cfg,
closeChan: make(chan struct{}),
}
cli.endpoints.init(cfg.endpoints)
var err error
var act *actor.Actor
var endpoint Endpoint
for cli.endpoints.curr, endpoint = range cli.endpoints.list {
cli.client, act, err = cli.newCli(ctx, endpoint)
if err != nil {
cli.logger.Warn(ctx, logs.FrostFSIRCouldntCreateRPCClientForEndpoint,
zap.Error(err), zap.String("endpoint", endpoint.Address))
} else {
cli.logger.Info(ctx, logs.FrostFSIRCreatedRPCClientForEndpoint,
zap.String("endpoint", endpoint.Address))
if cli.endpoints.curr > 0 && cli.cfg.switchInterval != 0 {
cli.switchIsActive.Store(true)
go cli.switchToMostPrioritized(ctx)
}
break
}
}
if cli.client == nil {
return nil, ErrNoHealthyEndpoint
}
cs, err := cli.client.GetContractStateByID(nnsContractID)
if err != nil {
return nil, fmt.Errorf("resolve nns hash: %w", err)
}
cli.nnsHash = cs.Hash
cli.setActor(act)
go cli.closeWaiter(ctx)
return cli, nil
}
func (c *Client) newCli(ctx context.Context, endpoint Endpoint) (*rpcclient.WSClient, *actor.Actor, error) {
cfg, err := endpoint.MTLSConfig.parse()
if err != nil {
return nil, nil, fmt.Errorf("read mtls certificates: %w", err)
}
cli, err := rpcclient.NewWS(ctx, endpoint.Address, rpcclient.WSOptions{
Options: rpcclient.Options{
DialTimeout: c.cfg.dialTimeout,
TLSClientConfig: cfg,
NetDialContext: c.cfg.dialerSource.NetContextDialer(),
},
})
if err != nil {
return nil, nil, fmt.Errorf("WS client creation: %w", err)
}
defer func() {
if err != nil {
cli.Close()
}
}()
err = cli.Init()
if err != nil {
return nil, nil, fmt.Errorf("WS client initialization: %w", err)
}
act, err := newActor(cli, c.acc, c.cfg)
if err != nil {
return nil, nil, fmt.Errorf("RPC actor creation: %w", err)
}
return cli, act, nil
}
func newActor(ws *rpcclient.WSClient, acc *wallet.Account, cfg cfg) (*actor.Actor, error) {
return actor.New(ws, []actor.SignerAccount{{
Signer: transaction.Signer{
Account: acc.PrivateKey().PublicKey().GetScriptHash(),
Scopes: cfg.signer.Scopes,
AllowedContracts: cfg.signer.AllowedContracts,
AllowedGroups: cfg.signer.AllowedGroups,
},
Account: acc,
}})
}
func newClientCache(morphCacheMetrics metrics.MorphCacheMetrics) cache {
c, _ := lru.New[util.Uint256, uint32](100) // returns error only if size is negative
return cache{
txHeights: c,
metrics: morphCacheMetrics,
}
}
// WithDialTimeout returns a client constructor option
// that specifies neo-go client dial timeout duration.
//
// Ignores non-positive value. Has no effect if WithSingleClient
// is provided.
//
// If option not provided, 5s timeout is used.
func WithDialTimeout(dur time.Duration) Option {
return func(c *cfg) {
if dur > 0 {
c.dialTimeout = dur
}
}
}
// WithLogger returns a client constructor option
// that specifies the component for writing log messages.
//
// Ignores nil value.
//
// If option not provided, &logger.Logger{Logger: zap.L()} is used.
func WithLogger(logger *logger.Logger) Option {
return func(c *cfg) {
if logger != nil {
c.logger = logger
}
}
}
// WithMetrics returns a client constructor option
// that specifies the component for reporting metrics.
//
// Ignores nil value.
//
// If option not provided, NoopMetrics is used.
func WithMetrics(metrics morphmetrics.Register) Option {
return func(c *cfg) {
if metrics != nil {
c.metrics = metrics
}
}
}
// WithSigner returns a client constructor option
// that specifies the signer and the scope of the transaction.
//
// Ignores nil value.
//
// If option not provided, signer with global scope is used.
func WithSigner(signer *transaction.Signer) Option {
return func(c *cfg) {
if signer != nil {
c.signer = signer
}
}
}
// WithEndpoints returns a client constructor option
// that specifies additional Neo rpc endpoints.
func WithEndpoints(endpoints ...Endpoint) Option {
return func(c *cfg) {
c.endpoints = append(c.endpoints, endpoints...)
}
}
// WithConnLostCallback return a client constructor option
// that specifies a callback that is called when Client
// unsuccessfully tried to connect to all the specified
// endpoints.
func WithConnLostCallback(cb Callback) Option {
return func(c *cfg) {
c.inactiveModeCb = cb
}
}
// WithSwitchInterval returns a client constructor option
// that specifies a wait interval b/w attempts to reconnect
// to an RPC node with the highest priority.
func WithSwitchInterval(i time.Duration) Option {
return func(c *cfg) {
c.switchInterval = i
}
}
func WithMorphCacheMetrics(morphCacheMetrics metrics.MorphCacheMetrics) Option {
return func(c *cfg) {
c.morphCacheMetrics = morphCacheMetrics
}
}
type DialerSource interface {
NetContextDialer() func(context.Context, string, string) (net.Conn, error)
}
type noopDialerSource struct{}
func (ds *noopDialerSource) NetContextDialer() func(context.Context, string, string) (net.Conn, error) {
return nil
}
func WithDialerSource(ds DialerSource) Option {
return func(c *cfg) {
c.dialerSource = ds
}
}