nuts-foundation/nuts-node

View on GitHub
core/engine.go

Summary

Maintainability
A
2 hrs
Test Coverage
B
82%
/*
 * Nuts node
 * Copyright (C) 2021 Nuts community
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package core

import (
    "context"
    "fmt"
    "github.com/sirupsen/logrus"
    "github.com/spf13/pflag"
    "os"
    "strings"
)

// Routable enables connecting a REST API to the echo server. The API wrappers should implement this interface
type Routable interface {
    // Routes configures the HTTP routes on the given router
    Routes(router EchoRouter)
}

// NewSystem creates a new, empty System.
func NewSystem() *System {
    serverCfg := NewServerConfig()
    result := &System{
        engines: []Engine{},
        Config:  serverCfg,
        Routers: []Routable{},
    }
    return result
}

// System is the control structure where engines are registered.
type System struct {
    // engines is the slice of all registered engines
    engines []Engine
    // Config holds the global and raw config
    Config *ServerConfig
    // Routers is used to connect API handlers to the echo server
    Routers []Routable
    // Context is cancelled when the system shuts down.
    Context context.Context
    // ContextCancel is a function to signal the system should shut down.
    ContextCancel context.CancelFunc
}

var coreLogger = logrus.StandardLogger().WithField(LogFieldModule, "core")

// Load loads the config and injects config values into engines
func (system *System) Load(flags *pflag.FlagSet) error {
    if err := system.Config.Load(flags); err != nil {
        return err
    }

    // visit each engine and inject the config
    return system.VisitEnginesE(func(engine Engine) error {
        if m, ok := engine.(Injectable); ok {
            return system.Config.InjectIntoEngine(m)
        }

        return nil
    })
}

// Start starts all engines in the system.
func (system *System) Start() error {
    var err error
    return system.VisitEnginesE(func(engine Engine) error {
        if m, ok := engine.(Runnable); ok {
            name := engineName(engine)
            coreLogger.Infof("Starting %s...", name)
            err = m.Start()
            if err != nil {
                return fmt.Errorf("unable to start %s: %w", name, err)
            }
            coreLogger.Infof("Started %s", name)
        }
        return err
    })
}

// Shutdown shuts down all engines in the system.
func (system *System) Shutdown() error {
    var engines []Runnable
    system.VisitEngines(func(engine Engine) {
        if m, ok := engine.(Runnable); ok {
            engines = append(engines, m)
        }
    })
    for i := len(engines) - 1; i >= 0; i-- {
        curr := engines[i]
        name := engineName(curr)
        coreLogger.Infof("Stopping %s...", name)
        if err := curr.Shutdown(); err != nil {
            return fmt.Errorf("unable to shutdown %s: %w", name, err)
        }
        coreLogger.Infof("Stopped %s", name)
    }
    return nil
}

// Configure configures all engines in the system.
func (system *System) Configure() error {
    coreLogger.Debugf("Creating datadir: %s", system.Config.Datadir)
    var err error
    if err = os.MkdirAll(system.Config.Datadir, os.ModePerm); err != nil {
        return fmt.Errorf("unable to create datadir (dir=%s): %w", system.Config.Datadir, err)
    }
    return system.VisitEnginesE(func(engine Engine) error {
        // only if Engine is dynamically configurable
        name := engineName(engine)
        if m, ok := engine.(Configurable); ok {
            coreLogger.Debugf("Configuring %s", name)
            err = m.Configure(*system.Config)
            coreLogger.Debugf("Configured %s", name)
        }
        if err != nil {
            err = fmt.Errorf("unable to configure %s: %w", name, err)
        }
        return err
    })
}

// Migrate migrates data structures in an engine if needed.
func (system *System) Migrate() error {
    var err error
    return system.VisitEnginesE(func(engine Engine) error {
        // only if Engine is migratable
        if m, ok := engine.(Migratable); ok {
            name := engineName(engine)
            coreLogger.Debugf("Migrating %s", name)
            err = m.Migrate()
            if err != nil {
                return fmt.Errorf("unable to migrate %s: %w", name, err)
            }
            coreLogger.Debugf("Migrated %s", name)
        }
        return err
    })
}

// VisitEngines applies the given function on all engines in the system.
// It visits the engines in order they were registered.
func (system *System) VisitEngines(visitor func(engine Engine)) {
    _ = system.VisitEnginesE(func(engine Engine) error {
        visitor(engine)
        return nil
    })
}

// VisitEnginesE applies the given function on all engines in the system, stopping when an error is returned. The error
// is passed through.
// It visits the engines in order they were registered.
func (system *System) VisitEnginesE(visitor func(engine Engine) error) error {
    for _, e := range system.engines {
        if err := visitor(e); err != nil {
            return err
        }
    }

    return nil
}

// FindEngineByName looks up given target engine by name, or nil if not found.
func (system *System) FindEngineByName(target string) Engine {
    for _, curr := range system.engines {
        if strings.EqualFold(engineName(curr), strings.ToLower(target)) {
            return curr
        }
    }
    return nil
}

// RegisterEngine is a helper func to add an engine to the list of engines from a different lib/pkg
func (system *System) RegisterEngine(engine Engine) {
    system.engines = append(system.engines, engine)
}

// RegisterRoutes is a helper func to register API routers so they can be linked to the echo server
func (system *System) RegisterRoutes(router Routable) {
    system.Routers = append(system.Routers, router)
}

// Runnable is the interface that groups the Start and Shutdown methods.
// When an engine implements these they will be called on startup and shutdown.
// Start and Shutdown should not be called more than once
type Runnable interface {
    Start() error
    Shutdown() error
}

// Migratable is the interface that defines if an engine is migratable.
// If an engine is migratable, Migrate is called between Configure() and Start().
// Migrations may require their own DB connection, they are closed before Start() is called.
type Migratable interface {
    Migrate() error
}

// Configurable is the interface that contains the Configure method.
// When an engine implements the Configurable interface, it will be called before startup.
// Configure should only be called once per engine instance
type Configurable interface {
    Configure(config ServerConfig) error
}

// ViewableDiagnostics is used for engines that display diagnostics in an interface
type ViewableDiagnostics interface {
    Named
    Diagnosable
}

// Diagnosable allows the implementer, mostly engines, to return diagnostics.
type Diagnosable interface {
    Diagnostics() []DiagnosticResult
}

// Engine is the base interface for a modular design
type Engine interface {
}

// Named is the interface for all engines that have a name
type Named interface {
    // Name returns the name of the engine
    Name() string
}

// Injectable marks an engine capable of Config injection
type Injectable interface {
    Named
    // Config returns a pointer to the struct that holds the Config.
    Config() interface{}
}

// HealthCheckable is the interface for engines that can perform health checks, which result can be reported to monitoring tooling.
type HealthCheckable interface {
    Named
    // CheckHealth performs health checks and returns the result. The function should not perform expensive or slow operations
    // and should return as fast as possible.
    CheckHealth() map[string]Health
}

// HealthStatus defines the result status of a health check.
type HealthStatus string

// Severity returns an integer indicating the seriousness of a startup.
// 0 means all is OK, higher is worse.
func (h HealthStatus) Severity() int {
    switch h {
    case HealthStatusDown:
        return 2
    default:
        fallthrough
    case HealthStatusUnknown:
        return 1
    case HealthStatusUp:
        return 0
    }
}

const (
    // HealthStatusUp indicates the health check succeeded
    HealthStatusUp HealthStatus = "UP"
    // HealthStatusDown indicates the health check failed
    HealthStatusDown HealthStatus = "DOWN"
    // HealthStatusUnknown indicates the status of the health check is unknown
    HealthStatusUnknown HealthStatus = "UNKNOWN"
)

// Health is the result of a health check.
type Health struct {
    // Status contains the status of the health check.
    Status HealthStatus `json:"status"`
    // Details contains optional details of the health check, can be nil.
    Details interface{} `json:"details,omitempty"`
}

func engineName(engine Engine) string {
    var name string
    if named, ok := engine.(Named); ok {
        name = named.Name()
    } else {
        name = fmt.Sprintf("%T", engine)
    }
    return name
}