health.go 3.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Copyright 2018 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

F
fatedier 已提交
15
package health
16 17

import (
F
fatedier 已提交
18
	"context"
F
fatedier 已提交
19 20
	"errors"
	"fmt"
21 22
	"io"
	"io/ioutil"
F
fatedier 已提交
23 24 25
	"net"
	"net/http"
	"time"
F
fatedier 已提交
26

F
fatedier 已提交
27
	"github.com/fatedier/frp/pkg/util/xlog"
F
fatedier 已提交
28 29 30 31
)

var (
	ErrHealthCheckType = errors.New("error health check type")
32 33
)

F
fatedier 已提交
34
type Monitor struct {
F
fatedier 已提交
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
	checkType      string
	interval       time.Duration
	timeout        time.Duration
	maxFailedTimes int

	// For tcp
	addr string

	// For http
	url string

	failedTimes    uint64
	statusOK       bool
	statusNormalFn func()
	statusFailedFn func()

	ctx    context.Context
	cancel context.CancelFunc
53 54
}

F
fatedier 已提交
55
func NewMonitor(ctx context.Context, checkType string,
F
fatedier 已提交
56 57
	intervalS int, timeoutS int, maxFailedTimes int,
	addr string, url string,
F
fatedier 已提交
58
	statusNormalFn func(), statusFailedFn func()) *Monitor {
F
fatedier 已提交
59 60 61 62 63 64 65 66 67 68

	if intervalS <= 0 {
		intervalS = 10
	}
	if timeoutS <= 0 {
		timeoutS = 3
	}
	if maxFailedTimes <= 0 {
		maxFailedTimes = 1
	}
F
fatedier 已提交
69
	newctx, cancel := context.WithCancel(ctx)
F
fatedier 已提交
70
	return &Monitor{
F
fatedier 已提交
71 72 73 74 75 76 77 78 79
		checkType:      checkType,
		interval:       time.Duration(intervalS) * time.Second,
		timeout:        time.Duration(timeoutS) * time.Second,
		maxFailedTimes: maxFailedTimes,
		addr:           addr,
		url:            url,
		statusOK:       false,
		statusNormalFn: statusNormalFn,
		statusFailedFn: statusFailedFn,
F
fatedier 已提交
80
		ctx:            newctx,
F
fatedier 已提交
81
		cancel:         cancel,
82 83 84
	}
}

F
fatedier 已提交
85
func (monitor *Monitor) Start() {
F
fatedier 已提交
86 87 88
	go monitor.checkWorker()
}

F
fatedier 已提交
89
func (monitor *Monitor) Stop() {
F
fatedier 已提交
90 91 92
	monitor.cancel()
}

F
fatedier 已提交
93
func (monitor *Monitor) checkWorker() {
F
fatedier 已提交
94
	xl := xlog.FromContextSafe(monitor.ctx)
F
fatedier 已提交
95
	for {
F
fatedier 已提交
96 97
		doCtx, cancel := context.WithDeadline(monitor.ctx, time.Now().Add(monitor.timeout))
		err := monitor.doCheck(doCtx)
F
fatedier 已提交
98 99 100

		// check if this monitor has been closed
		select {
F
fatedier 已提交
101
		case <-monitor.ctx.Done():
F
fatedier 已提交
102 103 104 105 106 107
			cancel()
			return
		default:
			cancel()
		}

F
fatedier 已提交
108
		if err == nil {
F
fatedier 已提交
109
			xl.Trace("do one health check success")
F
fatedier 已提交
110
			if !monitor.statusOK && monitor.statusNormalFn != nil {
F
fatedier 已提交
111
				xl.Info("health check status change to success")
F
fatedier 已提交
112 113 114 115
				monitor.statusOK = true
				monitor.statusNormalFn()
			}
		} else {
F
fatedier 已提交
116
			xl.Warn("do one health check failed: %v", err)
F
fatedier 已提交
117 118
			monitor.failedTimes++
			if monitor.statusOK && int(monitor.failedTimes) >= monitor.maxFailedTimes && monitor.statusFailedFn != nil {
F
fatedier 已提交
119
				xl.Warn("health check status change to failed")
F
fatedier 已提交
120 121 122 123 124 125 126 127 128
				monitor.statusOK = false
				monitor.statusFailedFn()
			}
		}

		time.Sleep(monitor.interval)
	}
}

F
fatedier 已提交
129
func (monitor *Monitor) doCheck(ctx context.Context) error {
F
fatedier 已提交
130 131
	switch monitor.checkType {
	case "tcp":
F
fatedier 已提交
132
		return monitor.doTCPCheck(ctx)
F
fatedier 已提交
133
	case "http":
F
fatedier 已提交
134
		return monitor.doHTTPCheck(ctx)
F
fatedier 已提交
135
	default:
F
fatedier 已提交
136
		return ErrHealthCheckType
F
fatedier 已提交
137 138 139
	}
}

F
fatedier 已提交
140
func (monitor *Monitor) doTCPCheck(ctx context.Context) error {
F
fatedier 已提交
141 142 143 144 145
	// if tcp address is not specified, always return nil
	if monitor.addr == "" {
		return nil
	}

F
fatedier 已提交
146 147 148
	var d net.Dialer
	conn, err := d.DialContext(ctx, "tcp", monitor.addr)
	if err != nil {
F
fatedier 已提交
149
		return err
F
fatedier 已提交
150 151
	}
	conn.Close()
F
fatedier 已提交
152
	return nil
F
fatedier 已提交
153 154
}

F
fatedier 已提交
155
func (monitor *Monitor) doHTTPCheck(ctx context.Context) error {
F
fatedier 已提交
156 157
	req, err := http.NewRequest("GET", monitor.url, nil)
	if err != nil {
F
fatedier 已提交
158
		return err
F
fatedier 已提交
159 160 161
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
F
fatedier 已提交
162
		return err
F
fatedier 已提交
163
	}
164 165
	defer resp.Body.Close()
	io.Copy(ioutil.Discard, resp.Body)
F
fatedier 已提交
166 167

	if resp.StatusCode/100 != 2 {
F
fatedier 已提交
168
		return fmt.Errorf("do http health check, StatusCode is [%d] not 2xx", resp.StatusCode)
F
fatedier 已提交
169
	}
F
fatedier 已提交
170
	return nil
171
}