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 }