github.com/minio/madmin-go@v1.7.5/cluster-health.go (about)

     1  //
     2  // MinIO Object Storage (c) 2022 MinIO, Inc.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //      http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  //
    16  
    17  package madmin
    18  
    19  import (
    20  	"context"
    21  	"net/http"
    22  	"net/http/httptrace"
    23  	"net/url"
    24  	"strconv"
    25  	"sync"
    26  	"time"
    27  )
    28  
    29  const (
    30  	minioWriteQuorumHeader     = "x-minio-write-quorum"
    31  	minIOHealingDrives         = "x-minio-healing-drives"
    32  	clusterCheckEndpoint       = "/minio/health/cluster"
    33  	clusterReadCheckEndpoint   = "/minio/health/cluster/read"
    34  	maintanenceURLParameterKey = "maintenance"
    35  )
    36  
    37  // HealthResult represents the cluster health result
    38  type HealthResult struct {
    39  	Healthy         bool
    40  	MaintenanceMode bool
    41  	WriteQuorum     int
    42  	HealingDrives   int
    43  }
    44  
    45  // HealthOpts represents the input options for the health check
    46  type HealthOpts struct {
    47  	ClusterRead bool
    48  	Maintenance bool
    49  }
    50  
    51  // Healthy will hit `/minio/health/cluster` and `/minio/health/cluster/ready` anonymous APIs to check the cluster health
    52  func (an *AnonymousClient) Healthy(ctx context.Context, opts HealthOpts) (result HealthResult, err error) {
    53  	if opts.ClusterRead {
    54  		return an.clusterReadCheck(ctx)
    55  	}
    56  	return an.clusterCheck(ctx, opts.Maintenance)
    57  }
    58  
    59  func (an *AnonymousClient) clusterCheck(ctx context.Context, maintenance bool) (result HealthResult, err error) {
    60  	urlValues := make(url.Values)
    61  	if maintenance {
    62  		urlValues.Set(maintanenceURLParameterKey, "true")
    63  	}
    64  
    65  	resp, err := an.executeMethod(ctx, http.MethodGet, requestData{
    66  		relPath:     clusterCheckEndpoint,
    67  		queryValues: urlValues,
    68  	}, nil)
    69  	defer closeResponse(resp)
    70  	if err != nil {
    71  		return result, err
    72  	}
    73  
    74  	if resp != nil {
    75  		writeQuorumStr := resp.Header.Get(minioWriteQuorumHeader)
    76  		if writeQuorumStr != "" {
    77  			result.WriteQuorum, err = strconv.Atoi(writeQuorumStr)
    78  			if err != nil {
    79  				return result, err
    80  			}
    81  		}
    82  		healingDrivesStr := resp.Header.Get(minIOHealingDrives)
    83  		if healingDrivesStr != "" {
    84  			result.HealingDrives, err = strconv.Atoi(healingDrivesStr)
    85  			if err != nil {
    86  				return result, err
    87  			}
    88  		}
    89  		switch resp.StatusCode {
    90  		case http.StatusOK:
    91  			result.Healthy = true
    92  		case http.StatusPreconditionFailed:
    93  			result.MaintenanceMode = true
    94  		default:
    95  			// Not Healthy
    96  		}
    97  	}
    98  	return result, nil
    99  }
   100  
   101  func (an *AnonymousClient) clusterReadCheck(ctx context.Context) (result HealthResult, err error) {
   102  	resp, err := an.executeMethod(ctx, http.MethodGet, requestData{
   103  		relPath: clusterReadCheckEndpoint,
   104  	}, nil)
   105  	defer closeResponse(resp)
   106  	if err != nil {
   107  		return result, err
   108  	}
   109  
   110  	if resp != nil {
   111  		switch resp.StatusCode {
   112  		case http.StatusOK:
   113  			result.Healthy = true
   114  		default:
   115  			// Not Healthy
   116  		}
   117  	}
   118  	return result, nil
   119  }
   120  
   121  // AliveOpts customizing liveness check.
   122  type AliveOpts struct {
   123  	Readiness bool // send request to /minio/health/ready
   124  }
   125  
   126  // AliveResult returns the time spent getting a response
   127  // back from the server on /minio/health/live endpoint
   128  type AliveResult struct {
   129  	Endpoint       *url.URL      `json:"endpoint"`
   130  	ResponseTime   time.Duration `json:"responseTime"`
   131  	DNSResolveTime time.Duration `json:"dnsResolveTime"`
   132  	Online         bool          `json:"online"` // captures x-minio-server-status
   133  	Error          error         `json:"error"`
   134  }
   135  
   136  // Alive will hit `/minio/health/live` to check if server is reachable, optionally returns
   137  // the amount of time spent getting a response back from the server.
   138  func (an *AnonymousClient) Alive(ctx context.Context, opts AliveOpts, servers ...ServerProperties) (resultsCh chan AliveResult) {
   139  	resource := "/minio/health/live"
   140  	if opts.Readiness {
   141  		resource = "/minio/health/ready"
   142  	}
   143  
   144  	scheme := "http"
   145  	if an.endpointURL != nil {
   146  		scheme = an.endpointURL.Scheme
   147  	}
   148  
   149  	resultsCh = make(chan AliveResult)
   150  	go func() {
   151  		defer close(resultsCh)
   152  		if len(servers) == 0 {
   153  			an.alive(ctx, an.endpointURL, resource, resultsCh)
   154  		} else {
   155  			var wg sync.WaitGroup
   156  			wg.Add(len(servers))
   157  			for _, server := range servers {
   158  				server := server
   159  				go func() {
   160  					defer wg.Done()
   161  					sscheme := server.Scheme
   162  					if sscheme == "" {
   163  						sscheme = scheme
   164  					}
   165  					u, err := url.Parse(sscheme + "://" + server.Endpoint)
   166  					if err != nil {
   167  						resultsCh <- AliveResult{
   168  							Error: err,
   169  						}
   170  						return
   171  					}
   172  					an.alive(ctx, u, resource, resultsCh)
   173  				}()
   174  			}
   175  			wg.Wait()
   176  		}
   177  	}()
   178  
   179  	return resultsCh
   180  }
   181  
   182  func (an *AnonymousClient) alive(ctx context.Context, u *url.URL, resource string, resultsCh chan AliveResult) {
   183  	var (
   184  		dnsStartTime, dnsDoneTime   time.Time
   185  		reqStartTime, firstByteTime time.Time
   186  	)
   187  
   188  	trace := &httptrace.ClientTrace{
   189  		DNSStart: func(_ httptrace.DNSStartInfo) {
   190  			dnsStartTime = time.Now()
   191  		},
   192  		DNSDone: func(_ httptrace.DNSDoneInfo) {
   193  			dnsDoneTime = time.Now()
   194  		},
   195  		GetConn: func(_ string) {
   196  			// GetConn is called again when trace is ON
   197  			// https://github.com/golang/go/issues/44281
   198  			if reqStartTime.IsZero() {
   199  				reqStartTime = time.Now()
   200  			}
   201  		},
   202  		GotFirstResponseByte: func() {
   203  			firstByteTime = time.Now()
   204  		},
   205  	}
   206  
   207  	resp, err := an.executeMethod(ctx, http.MethodGet, requestData{
   208  		relPath:          resource,
   209  		endpointOverride: u,
   210  	}, trace)
   211  	closeResponse(resp)
   212  	var respTime time.Duration
   213  	if firstByteTime.IsZero() {
   214  		respTime = time.Since(reqStartTime)
   215  	} else {
   216  		respTime = firstByteTime.Sub(reqStartTime) - dnsDoneTime.Sub(dnsStartTime)
   217  	}
   218  
   219  	result := AliveResult{
   220  		Endpoint:       u,
   221  		ResponseTime:   respTime,
   222  		DNSResolveTime: dnsDoneTime.Sub(dnsStartTime),
   223  	}
   224  	if err != nil {
   225  		result.Error = err
   226  	} else {
   227  		result.Online = resp.StatusCode == http.StatusOK && resp.Header.Get("x-minio-server-status") != "offline"
   228  	}
   229  
   230  	select {
   231  	case <-ctx.Done():
   232  		return
   233  	case resultsCh <- result:
   234  	}
   235  }