Review follow-ups to 73a93f1. Bound in-flight calls. rpcclient's single sendPostHandler goroutine imposed an accidental ceiling of one concurrent RPC; removing it without putting anything in its place left no bound at all. grpc-go supplies none either -- this server sets no MaxConcurrentStreams, so the default is math.MaxUint32 -- and dragonxd answers RPC with 8 worker threads behind a 4096-deep queue, shared on the pool node with getblocktemplate. Overload would therefore surface as mining latency rather than as an error we could back off on. MaxConnsPerHost blocks the caller at the limit instead of dialling more, which is the backpressure wanted; MaxIdleConnsPerHost alone would only cap reuse and let us exceed the limit while churning connections. Default 8, matching the node's DEFAULT_HTTP_THREADS, tunable with -rpc-max-concurrent. Even 8 removes all of the head-of-line blocking this work set out to fix. IdleConnTimeout 90s -> 20s. dragonxd closes idle connections at 30s (DEFAULT_HTTP_SERVER_TIMEOUT, applied via evhttp_set_timeout and not overridden in DRAGONX.conf). At 90s we were always the second to close, so a request could be written into a connection the server had already sent a FIN for, and Go will not retry a POST once bytes are on the wire. Closing first removes the race. Reject a negative -rpc-timeout, which silently meant "unbounded", the same as the documented 0. The check has to run after flag.Parse(); it was initially placed before it and never fired. Also correct the coinsupply note: hush_coinsupply walks the block index back to genesis, loading each block from disk and memoising newcoins and zfunds into the CBlockIndex, so the first call pays for the whole chain and later ones are nearly free. It is not a UTXO-set scan, as the earlier comment claimed. The measured 48s/3s figures are unchanged. Verified: five concurrent GetLightdInfo calls all return grpc-status 0 with no errors, 46 blocks ingested, and the daemon holds 2 sockets to the node rather than one per request; a negative timeout exits 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
46 lines
1.8 KiB
Go
46 lines
1.8 KiB
Go
package frontend
|
|
|
|
import (
|
|
"net"
|
|
"time"
|
|
|
|
"git.hush.is/hush/lightwalletd/zrpc"
|
|
"github.com/pkg/errors"
|
|
ini "gopkg.in/ini.v1"
|
|
)
|
|
|
|
// DefaultRPCTimeout bounds a single JSON-RPC round trip to dragonxd.
|
|
//
|
|
// Why 120s and not something tighter: the slowest legitimate call this daemon
|
|
// makes is `coinsupply`, measured at 48s on first call and 3s afterwards.
|
|
// hush_coinsupply walks the block index back to genesis loading each block from
|
|
// disk, memoising newcoins/zfunds into the CBlockIndex as it goes, so the first
|
|
// call pays for the whole chain and later ones are nearly free. A timeout below
|
|
// that first-call cost would turn a slow-but-working call into a hard failure.
|
|
// 120s leaves ~2.5x headroom while still bounding a hang that is otherwise
|
|
// unbounded -- calls were seen running past five minutes.
|
|
const DefaultRPCTimeout = 120 * time.Second
|
|
|
|
// DefaultRPCMaxConcurrent matches dragonxd's DEFAULT_HTTP_THREADS. Asking for
|
|
// more in-flight calls than the node has worker threads only adds queueing.
|
|
const DefaultRPCMaxConcurrent = 8
|
|
|
|
func NewZRPCFromConf(confPath string, timeout time.Duration, maxConcurrent int) (*zrpc.Client, error) {
|
|
cfg, err := ini.Load(confPath)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read config file")
|
|
}
|
|
|
|
rpcaddr := cfg.Section("").Key("rpcbind").String()
|
|
rpcport := cfg.Section("").Key("rpcport").String()
|
|
username := cfg.Section("").Key("rpcuser").String()
|
|
password := cfg.Section("").Key("rpcpassword").String()
|
|
|
|
return NewZRPCFromCreds(net.JoinHostPort(rpcaddr, rpcport), username, password, timeout, maxConcurrent)
|
|
}
|
|
|
|
func NewZRPCFromCreds(addr, username, password string, timeout time.Duration, maxConcurrent int) (*zrpc.Client, error) {
|
|
// DragonX only supports HTTP POST mode and does not provide TLS by default.
|
|
return zrpc.New(addr, username, password, timeout, maxConcurrent), nil
|
|
}
|