Skip to content

Commit

Permalink
Merge branch 'pedro/health_endpoint' into darren/admin-api-log-toggler
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenvechain committed Dec 6, 2024
2 parents 2392e96 + 34a8adc commit 186edc6
Show file tree
Hide file tree
Showing 18 changed files with 370 additions and 335 deletions.
5 changes: 2 additions & 3 deletions api/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ import (
"github.com/vechain/thor/v2/api/admin/apilogs"
healthAPI "github.com/vechain/thor/v2/api/admin/health"
"github.com/vechain/thor/v2/api/admin/loglevel"
"github.com/vechain/thor/v2/health"
)

func New(logLevel *slog.LevelVar, health *health.Health, apiLogsToggle *atomic.Bool) http.HandlerFunc {
func New(logLevel *slog.LevelVar, health *healthAPI.Health, apiLogsToggle *atomic.Bool) http.HandlerFunc {
router := mux.NewRouter()
subRouter := router.PathPrefix("/admin").Subrouter()

loglevel.New(logLevel).Mount(subRouter, "/loglevel")
healthAPI.New(health).Mount(subRouter, "/health")
healthAPI.NewAPI(health).Mount(subRouter, "/health")
apilogs.New(apiLogsToggle).Mount(subRouter, "/apilogs")

handler := handlers.CompressHandler(router)
Expand Down
103 changes: 81 additions & 22 deletions api/admin/health/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,101 @@
package health

import (
"net/http"
"sync"
"time"

"github.com/gorilla/mux"
"github.com/vechain/thor/v2/api/utils"
"github.com/vechain/thor/v2/health"
"github.com/vechain/thor/v2/chain"
"github.com/vechain/thor/v2/comm"
"github.com/vechain/thor/v2/thor"
)

type BlockIngestion struct {
ID *thor.Bytes32 `json:"id"`
Timestamp *time.Time `json:"timestamp"`
}

type Status struct {
Healthy bool `json:"healthy"`
BestBlockTimestamp *time.Time `json:"bestBlockTimestamp"`
WasChainSynced bool `json:"wasChainSynced"`
PeerCount int `json:"peerCount"`
}

type Health struct {
healthStatus *health.Health
lock sync.RWMutex
repo *chain.Repository
p2p *comm.Communicator
isNodeBootstrapped bool
}

func New(healthStatus *health.Health) *Health {
const (
defaultBlockTolerance = time.Duration(2*thor.BlockInterval) * time.Second // 2 blocks tolerance
defaultMinPeerCount = 2
)

func New(repo *chain.Repository, p2p *comm.Communicator) *Health {
return &Health{
healthStatus: healthStatus,
repo: repo,
p2p: p2p,
}
}

func (h *Health) handleGetHealth(w http.ResponseWriter, _ *http.Request) error {
acc, err := h.healthStatus.Status()
if err != nil {
return err
// isNetworkProgressing checks if the network is producing new blocks within the allowed interval.
func (h *Health) isNetworkProgressing(now time.Time, bestBlockTimestamp time.Time, blockTolerance time.Duration) bool {
return now.Sub(bestBlockTimestamp) <= blockTolerance
}

// hasNodeBootstrapped checks if the node has bootstrapped by comparing the block interval.
// Once it's marked as done, it never reverts.
func (h *Health) hasNodeBootstrapped(now time.Time, bestBlockTimestamp time.Time) bool {
if h.isNodeBootstrapped {
return true
}

if !acc.Healthy {
w.WriteHeader(http.StatusServiceUnavailable) // Set the status to 503
} else {
w.WriteHeader(http.StatusOK) // Set the status to 200
blockInterval := time.Duration(thor.BlockInterval) * time.Second
if bestBlockTimestamp.Add(blockInterval).After(now) {
h.isNodeBootstrapped = true
}
return utils.WriteJSON(w, acc)

return h.isNodeBootstrapped
}

func (h *Health) Mount(root *mux.Router, pathPrefix string) {
sub := root.PathPrefix(pathPrefix).Subrouter()
// isNodeConnectedP2P checks if the node is connected to peers
func (h *Health) isNodeConnectedP2P(peerCount int, minPeerCount int) bool {
return peerCount >= minPeerCount
}

func (h *Health) Status(blockTolerance time.Duration, minPeerCount int) (*Status, error) {
h.lock.RLock()
defer h.lock.RUnlock()

// Fetch the best block details
bestBlock := h.repo.BestBlockSummary()
bestBlockTimestamp := time.Unix(int64(bestBlock.Header.Timestamp()), 0)

// Fetch the current connected peers
var connectedPeerCount int
if h.p2p == nil {
connectedPeerCount = minPeerCount // ignore peers in solo mode
} else {
connectedPeerCount = h.p2p.PeerCount()
}

now := time.Now()

// Perform the checks
networkProgressing := h.isNetworkProgressing(now, bestBlockTimestamp, blockTolerance)
wasChainSynced := h.hasNodeBootstrapped(now, bestBlockTimestamp)
nodeConnected := h.isNodeConnectedP2P(connectedPeerCount, minPeerCount)

// Calculate overall health status
healthy := networkProgressing && wasChainSynced && nodeConnected

sub.Path("").
Methods(http.MethodGet).
Name("health").
HandlerFunc(utils.WrapHandlerFunc(h.handleGetHealth))
// Return the current status
return &Status{
Healthy: healthy,
BestBlockTimestamp: &bestBlockTimestamp,
WasChainSynced: wasChainSynced,
PeerCount: connectedPeerCount,
}, nil
}
68 changes: 68 additions & 0 deletions api/admin/health/health_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) 2024 The VeChainThor developers

// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
// file LICENSE or <https://www.gnu.org/licenses/lgpl-3.0.html>

package health

import (
"net/http"
"strconv"
"time"

"github.com/gorilla/mux"
"github.com/vechain/thor/v2/api/utils"
)

type API struct {
healthStatus *Health
}

func NewAPI(healthStatus *Health) *API {
return &API{
healthStatus: healthStatus,
}
}

func (h *API) handleGetHealth(w http.ResponseWriter, r *http.Request) error {
// Parse query parameters
query := r.URL.Query()

// Default to constants if query parameters are not provided
blockTolerance := defaultBlockTolerance
minPeerCount := defaultMinPeerCount

// Override with query parameters if they exist
if queryBlockTolerance := query.Get("blockTolerance"); queryBlockTolerance != "" {
if parsed, err := time.ParseDuration(queryBlockTolerance); err == nil {
blockTolerance = parsed
}
}

if queryMinPeerCount := query.Get("minPeerCount"); queryMinPeerCount != "" {
if parsed, err := strconv.Atoi(queryMinPeerCount); err == nil {
minPeerCount = parsed
}
}

acc, err := h.healthStatus.Status(blockTolerance, minPeerCount)
if err != nil {
return err
}

if !acc.Healthy {
w.WriteHeader(http.StatusServiceUnavailable) // Set the status to 503
} else {
w.WriteHeader(http.StatusOK) // Set the status to 200
}
return utils.WriteJSON(w, acc)
}

func (h *API) Mount(root *mux.Router, pathPrefix string) {
sub := root.PathPrefix(pathPrefix).Subrouter()

sub.Path("").
Methods(http.MethodGet).
Name("health").
HandlerFunc(utils.WrapHandlerFunc(h.handleGetHealth))
}
59 changes: 59 additions & 0 deletions api/admin/health/health_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2024 The VeChainThor developers

// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
// file LICENSE or <https://www.gnu.org/licenses/lgpl-3.0.html>

package health

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vechain/thor/v2/comm"
"github.com/vechain/thor/v2/test/testchain"
"github.com/vechain/thor/v2/txpool"
)

var ts *httptest.Server

func TestHealth(t *testing.T) {
initAPIServer(t)

var healthStatus Status
respBody, statusCode := httpGet(t, ts.URL+"/health")
require.NoError(t, json.Unmarshal(respBody, &healthStatus))
assert.False(t, healthStatus.Healthy)
assert.Equal(t, http.StatusServiceUnavailable, statusCode)
}

func initAPIServer(t *testing.T) {
thorChain, err := testchain.NewIntegrationTestChain()
require.NoError(t, err)

router := mux.NewRouter()
NewAPI(
New(thorChain.Repo(), comm.New(thorChain.Repo(), txpool.New(thorChain.Repo(), nil, txpool.Options{}))),
).Mount(router, "/health")

ts = httptest.NewServer(router)
}

func httpGet(t *testing.T, url string) ([]byte, int) {
res, err := http.Get(url) //#nosec G107
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()

r, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
return r, res.StatusCode
}
Loading

0 comments on commit 186edc6

Please sign in to comment.