Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Changelog for NeoFS Node

### Fixed
- SN could panic on forwarding GET/HEAD/RANGE request (#4120)
- gRPC SIGHUP reload no longer stops a running public API before replacement configuration is verified (#4113)
- Storage node shuts down when SIGHUP configuration reload fails (#4113)
- Invalid node attributes on SIGHUP no longer terminate a storage node (#4113)

### Changed
- SNs exchange TLS certificates on inter-node connections (#4097)
Expand Down
14 changes: 9 additions & 5 deletions cmd/neofs-node/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,26 @@ import (
"go.uber.org/zap"
)

func parseAttributes(c *cfg) {
func parseAttributes(c *cfg) error {
if c.appCfg.Node.Relay {
return
return nil
}

fatalOnErr(attributes.ReadNodeAttributes(&c.cfgNodeInfo.localInfo, c.appCfg.Node.Attributes))
if err := attributes.ReadNodeAttributes(&c.cfgNodeInfo.localInfo, c.appCfg.Node.Attributes); err != nil {
return err
}

// expand UN/LOCODE attribute if any found; keep user's attributes
// if any conflicts appear

locAttr := c.cfgNodeInfo.localInfo.LOCODE()
if locAttr == "" {
return
return nil
}

record, err := getRecord(locAttr)
if err != nil {
fatalOnErr(fmt.Errorf("could not get locode record from DB: %w", err))
return fmt.Errorf("could not get locode record from DB: %w", err)
}

countryCode := locAttr[:locodedb.CountryCodeLen]
Expand Down Expand Up @@ -84,6 +86,8 @@ func parseAttributes(c *cfg) {
} else {
setIfNotEmpty(n.SetSubdivisionName, record.SubDivName)
}

return nil
}

func getRecord(lc string) (locodedb.Record, error) {
Expand Down
107 changes: 57 additions & 50 deletions cmd/neofs-node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ type cfgGRPC struct {

listeners []net.Listener
servers []*grpc.Server
config grpcConfigSnapshot

// serviceRegistrators stores functions that register gRPC service
// implementations into a gRPC server.
Expand All @@ -251,7 +252,9 @@ func (g *cfgGRPC) registerService(f func(*grpc.Server)) {

g.serviceRegistrators = append(g.serviceRegistrators, f)
for _, srv := range g.servers {
f(srv)
if srv != nil {
f(srv)
}
}
}

Expand Down Expand Up @@ -627,7 +630,6 @@ func (c *cfg) needBootstrap() bool {
}

func (c *cfg) configWatcher(ctx context.Context) {
var err error
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP)

Expand All @@ -640,74 +642,79 @@ func (c *cfg) configWatcher(ctx context.Context) {
c.log.Warn("failed to notify systemd about reloading", zap.Error(err))
}

oldMetrics := writeMetricConfig(c.appCfg)
oldProfiler := writeProfilerConfig(c.appCfg)
oldGRPC := writeGRPCConfig(c.appCfg)
if err := c.reloadConfig(); err != nil {
c.internalErr <- fmt.Errorf("configuration reload: %w", err)
return
}

c.appCfg, err = config.New(config.WithConfigFile(c.appCfg.Path()))
if err != nil {
c.log.Error("configuration reading", zap.Error(err))
continue
c.log.Info("configuration has been reloaded successfully")

if err := sdnotify.Send(sdnotify.Ready); err != nil {
c.log.Warn("failed to notify systemd about readiness after reload", zap.Error(err))
}
case <-ctx.Done():
return
}
}
}

// Prometheus and pprof
//nolint:contextcheck // Reloading HTTP services does not receive a request context.
func (c *cfg) reloadConfig() error {
oldCfg := c.appCfg
oldMetrics := writeMetricConfig(oldCfg)
oldProfiler := writeProfilerConfig(oldCfg)

// nolint:contextcheck
c.reloadMetricsAndPprof(oldMetrics, oldProfiler)
newCfg, err := config.New(config.WithConfigFile(oldCfg.Path()))
if err != nil {
return fmt.Errorf("read configuration: %w", err)
}
if err := validateConfig(newCfg); err != nil {
return fmt.Errorf("validate configuration: %w", err)

@carpawell carpawell Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not sure if running with an incorrect config is ok. maybe app should die? the only feedback we have there is log message, otherwise: app is healthy, and systemd thinks we are "Ready"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aren't we running with an old config in this case?

@carpawell carpawell Aug 5, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, but we have a new config file (envs), and the app is still running with already unknown old config

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree. A failed reload can leave the runtime state out of sync with the configuration, so reporting READY=1 is misleading. I think stopping the node on reload failure would be a good idea.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i am fully ok with stopping

}
c.appCfg = newCfg

// Logger
// Prometheus and pprof

err = c.logLevel.UnmarshalText([]byte(c.appCfg.Logger.Level))
if err != nil {
c.log.Error("invalid logger level configuration", zap.Error(err))
continue
}
c.reloadMetricsAndPprof(oldMetrics, oldProfiler)

// Policer
// Logger

c.policer.Reload(c.policerOpts()...)
if err := c.logLevel.UnmarshalText([]byte(c.appCfg.Logger.Level)); err != nil {
return fmt.Errorf("set logger level: %w", err)
}

// Storage Engine
// Policer

var rcfg engine.ReConfiguration
for _, optsWithID := range c.shardOpts() {
rcfg.AddShard(optsWithID.configID, optsWithID.shOpts)
}
c.policer.Reload(c.policerOpts()...)

err = c.cfgObject.cfgLocalStorage.localStorage.Reload(rcfg)
if err != nil {
c.log.Error("storage engine configuration update", zap.Error(err))
continue
}
// Storage Engine

// Morph
var rcfg engine.ReConfiguration
for _, optsWithID := range c.shardOpts() {
rcfg.AddShard(optsWithID.configID, optsWithID.shOpts)
}

c.cli.Reload(client.WithEndpoints(c.appCfg.FSChain.Endpoints))
if err := c.cfgObject.cfgLocalStorage.localStorage.Reload(rcfg); err != nil {
return fmt.Errorf("update storage engine configuration: %w", err)
}

// Node
// Morph

err = c.reloadNodeAttributes()
if err != nil {
c.log.Error("invalid node attributes configuration", zap.Error(err))
continue
}
c.cli.Reload(client.WithEndpoints(c.appCfg.FSChain.Endpoints))

// gRPC
// Node

if err = reloadGRPC(c, oldGRPC); err != nil {
c.log.Error("gRPC configuration reload", zap.Error(err))
continue
}
if err := c.reloadNodeAttributes(); err != nil {
return fmt.Errorf("update node attributes: %w", err)
}

c.log.Info("configuration has been reloaded successfully")
// gRPC

if err := sdnotify.Send(sdnotify.Ready); err != nil {
c.log.Warn("failed to notify systemd about readiness after reload", zap.Error(err))
}
case <-ctx.Done():
return
}
if err := reloadGRPC(c); err != nil {
return fmt.Errorf("reload gRPC configuration: %w", err)
}

return nil
}

// writeSystemAttributes writes app version as defined at compilation
Expand Down
Loading
Loading