This significantly reworks the CPU miner to make use of the background block template generator that was introduced in the previous release as well as to convert its lifecycle to use the expected pattern for running subsystems based on contexts. The switch to using the background block template generator provides all the benefits it offers to miners such as: - Intelligent vote propagation handling - Improved handling of chain reorganizations necessary when the current tip is unable to obtain enough votes - Current state synchronization - Near elimination of stale templates when new blocks and votes are received This is particularly important in testing scenarios, such as when using the simulation network, since the difficulty is so low that the code this is replacing is often able to mine blocks so fast it ends up mining multiple alternative blocks while waiting for the votes to show up which makes testing with it more difficult. This is no longer an issue when requesting template updates since the background block template generator has logic to provide the votes a chance to arrive before building and notifying the template. Another modification this makes is to also make use of template notifications when mining blocks mined via the discrete mining mode and to only count blocks if they successfully extend the main chain. This also significantly helps in testing scenarios since it means the testing code and/or testers using the simulation test network can rely on the requested number of blocks actually being added to the main chain even if more need to be generated to make that happen. Along similar lines, one area this change does not address, but does pave the way to support and would be useful in the future for the simulation test network is to add some additional logic to provide ticket purchases a chance to arrive when prior to stake validation height, since that is another area that often complicates testing. In terms of the lifecycle changes, this replaces the Start, Stop, and Wait methods with a single method named Run and arranges for it to block until the provided context is cancelled. This is more flexible for the caller since it can easily turn blocking code into async code while the reverse is not true. The new Run method waits for all goroutines that it starts to shutdown before returning to help ensure an orderly shutdown. In addition, the semantics of Run are different from Start/Stop in that the Start method launched the default number of mining worker goroutines whereas Run only starts the CPU miner with the main infrastructure goroutines and zero workers, meaning it will be idle until explicitly requested to mine by the caller setting the number of workers to use. Since there are exported methods that send messages to the CPU miner infrastructure goroutines via a channel and those goroutines are stopped during the shutdown process, all sends select across both the channel in question as well as a quit channel which is closed when the context is canceled. This ensures callers can't end up blocking on send to a goroutine that is no longer running without needing additional mutexes. As part of the conversion, it improves a lot of the comments to better describe the expected behavior and further integrates the context to support cancellation where it previously was not supported. Finally, all callers are updated to use the new API and the server is modified to take advantage of the new lifecycle semantics by taking ownership of the CPU miner code directly in its Run method where it more logically makes sense and only creating the template generate and CPU miner infrastructure if needed based on the configuration options.
379 lines
8.8 KiB
Go
379 lines
8.8 KiB
Go
// Copyright (c) 2016 The btcsuite developers
|
|
// Copyright (c) 2017-2019 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package rpctest
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/elliptic"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/decred/dcrd/certgen"
|
|
rpc "github.com/decred/dcrd/rpcclient/v6"
|
|
)
|
|
|
|
// nodeConfig contains all the args, and data required to launch a dcrd process
|
|
// and connect the rpc client to it.
|
|
type nodeConfig struct {
|
|
rpcUser string
|
|
rpcPass string
|
|
listen string
|
|
rpcListen string
|
|
rpcConnect string
|
|
dataDir string
|
|
logDir string
|
|
profile string
|
|
debugLevel string
|
|
extra []string
|
|
prefix string
|
|
|
|
exe string
|
|
endpoint string
|
|
certFile string
|
|
keyFile string
|
|
certificates []byte
|
|
}
|
|
|
|
// newConfig returns a newConfig with all default values.
|
|
func newConfig(prefix, certFile, keyFile string, extra []string) (*nodeConfig, error) {
|
|
a := &nodeConfig{
|
|
listen: "127.0.0.1:18555",
|
|
rpcListen: "127.0.0.1:18556",
|
|
rpcUser: "user",
|
|
rpcPass: "pass",
|
|
extra: extra,
|
|
prefix: prefix,
|
|
|
|
exe: "dcrd",
|
|
endpoint: "ws",
|
|
certFile: certFile,
|
|
keyFile: keyFile,
|
|
}
|
|
if err := a.setDefaults(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// setDefaults sets the default values of the config. It also creates the
|
|
// temporary data, and log directories which must be cleaned up with a call to
|
|
// cleanup().
|
|
func (n *nodeConfig) setDefaults() error {
|
|
n.dataDir = filepath.Join(n.prefix, "data")
|
|
n.logDir = filepath.Join(n.prefix, "logs")
|
|
cert, err := ioutil.ReadFile(n.certFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.certificates = cert
|
|
return nil
|
|
}
|
|
|
|
// arguments returns an array of arguments that be used to launch the dcrd
|
|
// process.
|
|
func (n *nodeConfig) arguments() []string {
|
|
args := []string{}
|
|
if n.rpcUser != "" {
|
|
// --rpcuser
|
|
args = append(args, fmt.Sprintf("--rpcuser=%s", n.rpcUser))
|
|
}
|
|
if n.rpcPass != "" {
|
|
// --rpcpass
|
|
args = append(args, fmt.Sprintf("--rpcpass=%s", n.rpcPass))
|
|
}
|
|
if n.listen != "" {
|
|
// --listen
|
|
args = append(args, fmt.Sprintf("--listen=%s", n.listen))
|
|
}
|
|
if n.rpcListen != "" {
|
|
// --rpclisten
|
|
args = append(args, fmt.Sprintf("--rpclisten=%s", n.rpcListen))
|
|
}
|
|
if n.rpcConnect != "" {
|
|
// --rpcconnect
|
|
args = append(args, fmt.Sprintf("--rpcconnect=%s", n.rpcConnect))
|
|
}
|
|
// --rpccert
|
|
args = append(args, fmt.Sprintf("--rpccert=%s", n.certFile))
|
|
// --rpckey
|
|
args = append(args, fmt.Sprintf("--rpckey=%s", n.keyFile))
|
|
// --txindex
|
|
args = append(args, "--txindex")
|
|
// --addrindex
|
|
args = append(args, "--addrindex")
|
|
if n.dataDir != "" {
|
|
// --datadir
|
|
args = append(args, fmt.Sprintf("--datadir=%s", n.dataDir))
|
|
}
|
|
if n.logDir != "" {
|
|
// --logdir
|
|
args = append(args, fmt.Sprintf("--logdir=%s", n.logDir))
|
|
}
|
|
if n.profile != "" {
|
|
// --profile
|
|
args = append(args, fmt.Sprintf("--profile=%s", n.profile))
|
|
}
|
|
if n.debugLevel != "" {
|
|
// --debuglevel
|
|
args = append(args, fmt.Sprintf("--debuglevel=%s", n.debugLevel))
|
|
}
|
|
// --allowunsyncedmining
|
|
args = append(args, "--allowunsyncedmining")
|
|
args = append(args, n.extra...)
|
|
return args
|
|
}
|
|
|
|
// command returns the exec.Cmd which will be used to start the dcrd process.
|
|
func (n *nodeConfig) command() *exec.Cmd {
|
|
return exec.Command(n.exe, n.arguments()...)
|
|
}
|
|
|
|
// rpcConnConfig returns the rpc connection config that can be used to connect
|
|
// to the dcrd process that is launched via Start().
|
|
func (n *nodeConfig) rpcConnConfig() rpc.ConnConfig {
|
|
return rpc.ConnConfig{
|
|
Host: n.rpcListen,
|
|
Endpoint: n.endpoint,
|
|
User: n.rpcUser,
|
|
Pass: n.rpcPass,
|
|
Certificates: n.certificates,
|
|
DisableAutoReconnect: true,
|
|
}
|
|
}
|
|
|
|
// String returns the string representation of this nodeConfig.
|
|
func (n *nodeConfig) String() string {
|
|
return n.prefix
|
|
}
|
|
|
|
// node houses the necessary state required to configure, launch, and manage a
|
|
// dcrd process.
|
|
type node struct {
|
|
config *nodeConfig
|
|
|
|
cmd *exec.Cmd
|
|
pidFile string
|
|
stderr io.ReadCloser
|
|
stdout io.ReadCloser
|
|
wg sync.WaitGroup
|
|
pid int
|
|
|
|
dataDir string
|
|
|
|
t *testing.T
|
|
}
|
|
|
|
// logf is identical to n.t.Logf but it prepends the pid of this node.
|
|
func (n *node) logf(format string, args ...interface{}) {
|
|
pid := strconv.Itoa(n.pid) + " "
|
|
logf(n.t, pid+format, args...)
|
|
}
|
|
|
|
// tracef is identical to debug.go.tracef but it prepends the pid of this
|
|
// node.
|
|
func (n *node) tracef(format string, args ...interface{}) {
|
|
if !trace {
|
|
return
|
|
}
|
|
pid := strconv.Itoa(n.pid) + " "
|
|
tracef(n.t, pid+format, args...)
|
|
}
|
|
|
|
// debugf is identical to debug.go.debugf but it prepends the pid of this
|
|
// node.
|
|
func (n *node) debugf(format string, args ...interface{}) {
|
|
if !debug {
|
|
return
|
|
}
|
|
pid := strconv.Itoa(n.pid) + " "
|
|
debugf(n.t, pid+format, args...)
|
|
}
|
|
|
|
// newNode creates a new node instance according to the passed config. dataDir
|
|
// will be used to hold a file recording the pid of the launched process, and
|
|
// as the base for the log and data directories for dcrd.
|
|
func newNode(t *testing.T, config *nodeConfig, dataDir string) *node {
|
|
return &node{
|
|
config: config,
|
|
dataDir: dataDir,
|
|
cmd: config.command(),
|
|
t: t,
|
|
}
|
|
}
|
|
|
|
// start creates a new dcrd process, and writes its pid in a file reserved for
|
|
// recording the pid of the launched process. This file can be used to
|
|
// terminate the process in case of a hang, or panic. In the case of a failing
|
|
// test case, or panic, it is important that the process be stopped via stop(),
|
|
// otherwise, it will persist unless explicitly killed.
|
|
func (n *node) start() error {
|
|
var err error
|
|
|
|
var pid sync.WaitGroup
|
|
pid.Add(1)
|
|
|
|
// Redirect stderr.
|
|
n.stderr, err = n.cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.wg.Add(1)
|
|
go func() {
|
|
defer n.wg.Done()
|
|
pid.Wait() // Block until pid is available
|
|
r := bufio.NewReader(n.stderr)
|
|
for {
|
|
line, err := r.ReadBytes('\n')
|
|
if err == io.EOF {
|
|
n.tracef("stderr: EOF")
|
|
return
|
|
}
|
|
n.logf("stderr: %s", line)
|
|
}
|
|
}()
|
|
|
|
// Redirect stdout.
|
|
n.stdout, err = n.cmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.wg.Add(1)
|
|
go func() {
|
|
defer n.wg.Done()
|
|
pid.Wait() // Block until pid is available
|
|
r := bufio.NewReader(n.stdout)
|
|
for {
|
|
line, err := r.ReadBytes('\n')
|
|
if err == io.EOF {
|
|
n.tracef("stdout: EOF")
|
|
return
|
|
}
|
|
n.tracef("stdout: %s", line)
|
|
}
|
|
}()
|
|
|
|
// Launch command and store pid.
|
|
if err := n.cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
n.pid = n.cmd.Process.Pid
|
|
|
|
// Unblock pipes now pid is available
|
|
pid.Done()
|
|
|
|
f, err := os.Create(filepath.Join(n.config.String(), "dcrd.pid"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.pidFile = f.Name()
|
|
if _, err = fmt.Fprintf(f, "%d\n", n.cmd.Process.Pid); err != nil {
|
|
return err
|
|
}
|
|
|
|
return f.Close()
|
|
}
|
|
|
|
// stop interrupts the running dcrd process, and waits until it exits
|
|
// properly. On windows, interrupt is not supported, so a kill signal is used
|
|
// instead
|
|
func (n *node) stop() error {
|
|
n.tracef("stop %p %p", n.cmd, n.cmd.Process)
|
|
defer n.tracef("stop done")
|
|
|
|
if n.cmd == nil || n.cmd.Process == nil {
|
|
// return if not properly initialized
|
|
// or error starting the process
|
|
return nil
|
|
}
|
|
|
|
// Send kill command
|
|
n.tracef("stop send kill")
|
|
var err error
|
|
if runtime.GOOS == "windows" {
|
|
err = n.cmd.Process.Signal(os.Kill)
|
|
} else {
|
|
err = n.cmd.Process.Signal(os.Interrupt)
|
|
}
|
|
if err != nil {
|
|
n.t.Logf("stop Signal error: %v", err)
|
|
}
|
|
|
|
// Wait for pipes.
|
|
n.tracef("stop wg")
|
|
n.wg.Wait()
|
|
|
|
// Wait for command to exit.
|
|
n.tracef("stop cmd.Wait")
|
|
err = n.cmd.Wait()
|
|
if err != nil {
|
|
n.t.Logf("stop cmd.Wait error: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cleanup cleanups process and args files. The file housing the pid of the
|
|
// created process will be deleted, as well as any directories created by the
|
|
// process.
|
|
func (n *node) cleanup() error {
|
|
n.tracef("cleanup")
|
|
defer n.tracef("cleanup done")
|
|
|
|
if n.pidFile != "" {
|
|
if err := os.Remove(n.pidFile); err != nil {
|
|
n.t.Logf("unable to remove file %s: %v", n.pidFile,
|
|
err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// shutdown terminates the running dcrd process, and cleans up all
|
|
// file/directories created by node.
|
|
func (n *node) shutdown() error {
|
|
n.tracef("shutdown")
|
|
defer n.tracef("shutdown done")
|
|
|
|
if err := n.stop(); err != nil {
|
|
n.t.Logf("shutdown stop error: %v", err)
|
|
return err
|
|
}
|
|
return n.cleanup()
|
|
}
|
|
|
|
// genCertPair generates a key/cert pair to the paths provided.
|
|
func genCertPair(certFile, keyFile string) error {
|
|
org := "rpctest autogenerated cert"
|
|
validUntil := time.Now().Add(10 * 365 * 24 * time.Hour)
|
|
cert, key, err := certgen.NewTLSCertPair(elliptic.P521(), org,
|
|
validUntil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write cert and key files.
|
|
if err = ioutil.WriteFile(certFile, cert, 0644); err != nil {
|
|
return err
|
|
}
|
|
if err = ioutil.WriteFile(keyFile, key, 0600); err != nil {
|
|
os.Remove(certFile)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|