refactor(rpc): streamline RPC node selection and remove hardcoded defaults

This commit is contained in:
line-6000
2026-04-23 02:08:18 +08:00
parent 09514a6946
commit 91936d580f
13 changed files with 326 additions and 101 deletions
+12 -5
View File
@@ -47,8 +47,8 @@ func DeleteRpcNodeByID(id uint64) error {
// SelectRpcNode picks a healthy RPC endpoint for a (network, type) pair.
// Strategy: weighted random among rows where enabled=true AND status=ok.
// Falls back to any enabled row when none are healthy yet (bootstrap case
// where the health check hasn't run). Returns nil if nothing is defined.
// Falls back to enabled rows with status=unknown when no health check has
// run yet. Explicitly down rows are not selected.
func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) {
var rows []mdb.RpcNode
err := dao.Mdb.Model(&mdb.RpcNode{}).
@@ -62,15 +62,22 @@ func SelectRpcNode(network, nodeType string) (*mdb.RpcNode, error) {
if len(rows) == 0 {
return nil, nil
}
healthy := rows[:0]
healthy := make([]mdb.RpcNode, 0, len(rows))
bootstrap := make([]mdb.RpcNode, 0, len(rows))
for _, r := range rows {
if r.Status == mdb.RpcNodeStatusOk {
switch r.Status {
case mdb.RpcNodeStatusOk:
healthy = append(healthy, r)
case "", mdb.RpcNodeStatusUnknown:
bootstrap = append(bootstrap, r)
}
}
candidates := healthy
if len(candidates) == 0 {
candidates = rows // bootstrap: no health check has run yet
candidates = bootstrap
}
if len(candidates) == 0 {
return nil, nil
}
return pickWeighted(candidates), nil
}
+101
View File
@@ -0,0 +1,101 @@
package data
import (
"testing"
"github.com/assimon/luuu/internal/testutil"
"github.com/assimon/luuu/model/dao"
"github.com/assimon/luuu/model/mdb"
)
func TestSelectRpcNodeUsesHealthyRow(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkSolana,
Url: "https://unknown.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Status: mdb.RpcNodeStatusUnknown,
}).Error; err != nil {
t.Fatalf("seed unknown rpc_node: %v", err)
}
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkSolana,
Url: "https://ok.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Status: mdb.RpcNodeStatusOk,
}).Error; err != nil {
t.Fatalf("seed ok rpc_node: %v", err)
}
got, err := SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectRpcNode(): %v", err)
}
if got == nil || got.Url != "https://ok.example.com" {
t.Fatalf("SelectRpcNode() = %#v, want ok row", got)
}
}
func TestSelectRpcNodeFallsBackToUnknownOnly(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkSolana,
Url: "https://unknown.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Status: mdb.RpcNodeStatusUnknown,
}).Error; err != nil {
t.Fatalf("seed unknown rpc_node: %v", err)
}
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkSolana,
Url: "https://down.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Status: mdb.RpcNodeStatusDown,
}).Error; err != nil {
t.Fatalf("seed down rpc_node: %v", err)
}
got, err := SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectRpcNode(): %v", err)
}
if got == nil || got.Url != "https://unknown.example.com" {
t.Fatalf("SelectRpcNode() = %#v, want unknown row", got)
}
}
func TestSelectRpcNodeIgnoresDownRows(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := dao.Mdb.Create(&mdb.RpcNode{
Network: mdb.NetworkSolana,
Url: "https://down.example.com",
Type: mdb.RpcNodeTypeHttp,
Weight: 1,
Enabled: true,
Status: mdb.RpcNodeStatusDown,
}).Error; err != nil {
t.Fatalf("seed down rpc_node: %v", err)
}
got, err := SelectRpcNode(mdb.NetworkSolana, mdb.RpcNodeTypeHttp)
if err != nil {
t.Fatalf("SelectRpcNode(): %v", err)
}
if got != nil {
t.Fatalf("SelectRpcNode() = %#v, want nil", got)
}
}