Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions common/policies/inquire/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ type comparablePrincipalSetPair struct {
containing ComparablePrincipalSet
}

// EnsurePlurality returns a ComparablePrincipalSet such that plurality requirements over
// the contained ComparablePrincipalSet in the comparablePrincipalSetPair hold
// MergeWithPlurality returns a ComparablePrincipalSet that merges the contained and containing
// ComparablePrincipalSets in the comparablePrincipalSetPair while satisfying plurality requirements
func (pair comparablePrincipalSetPair) MergeWithPlurality() ComparablePrincipalSet {
var principalsToAdd []*ComparablePrincipal
used := make(map[int]struct{})
Expand Down
90 changes: 90 additions & 0 deletions core/operations/detailed_health_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package operations

import (
"context"
"encoding/json"
"net/http"
"time"

libhealthz "github.com/hyperledger/fabric-lib-go/healthz"
"github.com/hyperledger/fabric/core/operations/healthz"
)

// DetailedHealthHandler handles /healthz/detailed. Access should be restricted
// via TLS/client authentication.
type DetailedHealthHandler struct {
readinessHandler *healthz.ReadinessHandler
healthHandler HealthHandler
timeout time.Duration
}

type HealthHandler interface {
RunChecks(context.Context) []libhealthz.FailedCheck
}

func NewDetailedHealthHandler(readinessHandler *healthz.ReadinessHandler, healthHandler HealthHandler, timeout time.Duration) *DetailedHealthHandler {
return &DetailedHealthHandler{
readinessHandler: readinessHandler,
healthHandler: healthHandler,
timeout: timeout,
}
}

func (h *DetailedHealthHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
rw.WriteHeader(http.StatusMethodNotAllowed)
return
}

ctx, cancel := context.WithTimeout(req.Context(), h.timeout)
defer cancel()

detailedStatus := h.readinessHandler.GetDetailedStatus(ctx)

livenessChecks := h.healthHandler.RunChecks(ctx)
if len(livenessChecks) > 0 {
for _, check := range livenessChecks {
detailedStatus.FailedChecks = append(detailedStatus.FailedChecks, healthz.FailedCheck{
Component: check.Component,
Reason: check.Reason,
})
if _, exists := detailedStatus.Components[check.Component]; !exists {
detailedStatus.Components[check.Component] = healthz.ComponentStatus{
Status: healthz.StatusUnavailable,
Message: check.Reason,
}
}
}
if detailedStatus.Status == healthz.StatusOK {
detailedStatus.Status = healthz.StatusUnavailable
}
}

rw.Header().Set("Content-Type", "application/json")

var statusCode int
switch detailedStatus.Status {
case healthz.StatusOK:
statusCode = http.StatusOK
case healthz.StatusDegraded:
statusCode = http.StatusOK
default:
statusCode = http.StatusServiceUnavailable
}

resp, err := json.Marshal(detailedStatus)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}

rw.WriteHeader(statusCode)
rw.Write(resp)
}

85 changes: 85 additions & 0 deletions core/operations/healthcheckers/gossip_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package healthcheckers

import (
"context"
"fmt"
"time"

"github.com/hyperledger/fabric/core/operations/healthz"
"github.com/hyperledger/fabric/gossip/service"
)

// GossipChecker verifies gossip service connectivity. When minPeers = 0 (default),
// only checks that gossip is initialized, avoiding false negatives in dev setups.
// When minPeers > 0, enforces minimum peer count. DEGRADED at minimum is informational.
type GossipChecker struct {
gossipService *service.GossipService
minPeers int
timeout time.Duration
}

// NewGossipChecker creates a new GossipChecker. If minPeers is 0, only verifies
// gossip is initialized.
func NewGossipChecker(gossipService *service.GossipService, minPeers int, timeout time.Duration) *GossipChecker {
return &GossipChecker{
gossipService: gossipService,
minPeers: minPeers,
timeout: timeout,
}
}

func (g *GossipChecker) ReadinessCheck(ctx context.Context) error {
if g.gossipService == nil {
return fmt.Errorf("gossip service not initialized")
}

peers := g.gossipService.Peers()
connectedCount := len(peers)

if g.minPeers > 0 && connectedCount < g.minPeers {
return fmt.Errorf("insufficient peers connected: %d (minimum: %d)", connectedCount, g.minPeers)
}

return nil
}

func (g *GossipChecker) GetStatus() healthz.ComponentStatus {
if g.gossipService == nil {
return healthz.ComponentStatus{
Status: healthz.StatusUnavailable,
Message: "Gossip service not initialized",
}
}

peers := g.gossipService.Peers()
connectedCount := len(peers)

status := healthz.StatusOK
message := fmt.Sprintf("Connected to %d peers", connectedCount)

if g.minPeers > 0 && connectedCount < g.minPeers {
status = healthz.StatusUnavailable
message = fmt.Sprintf("Insufficient peers: %d (minimum: %d)", connectedCount, g.minPeers)
} else if g.minPeers > 0 && connectedCount == g.minPeers {
status = healthz.StatusDegraded
message = fmt.Sprintf("Connected to minimum required peers: %d", connectedCount)
}

details := map[string]interface{}{
"connected_peers": connectedCount,
"min_peers": g.minPeers,
}

return healthz.ComponentStatus{
Status: status,
Message: message,
Details: details,
}
}

87 changes: 87 additions & 0 deletions core/operations/healthcheckers/gossip_checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package healthcheckers

import (
"context"
"testing"

"github.com/hyperledger/fabric/core/operations/healthz"
"github.com/hyperledger/fabric/gossip/service"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGossipChecker_ReadinessCheck(t *testing.T) {
tests := []struct {
name string
gossip *service.GossipService
minPeers int
expectError bool
errorMsg string
}{
{
name: "nil gossip service",
gossip: nil,
minPeers: 0,
expectError: true,
errorMsg: "gossip service not initialized",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checker := &GossipChecker{
gossipService: tt.gossip,
minPeers: tt.minPeers,
timeout: 5,
}

err := checker.ReadinessCheck(context.Background())

if tt.expectError {
require.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
} else {
require.NoError(t, err)
}
})
}
}

func TestGossipChecker_GetStatus(t *testing.T) {
tests := []struct {
name string
gossip *service.GossipService
minPeers int
expectedStatus string
}{
{
name: "nil gossip service",
gossip: nil,
minPeers: 0,
expectedStatus: healthz.StatusUnavailable,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checker := &GossipChecker{
gossipService: tt.gossip,
minPeers: tt.minPeers,
timeout: 5,
}

status := checker.GetStatus()
assert.Equal(t, tt.expectedStatus, status.Status)
assert.NotEmpty(t, status.Message)
})
}
}

Loading
Loading