2016-11-26 19:48:49 +01:00
package healthcheck
import (
2016-11-30 22:49:57 +01:00
"context"
2017-03-24 09:36:33 +01:00
"fmt"
2017-05-10 14:28:57 -04:00
"net"
2016-11-26 19:48:49 +01:00
"net/http"
"net/url"
2017-05-10 14:28:57 -04:00
"strconv"
2016-11-26 19:48:49 +01:00
"sync"
"time"
2017-01-31 22:55:02 +01:00
"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
"github.com/vulcand/oxy/roundrobin"
2016-11-26 19:48:49 +01:00
)
var singleton * HealthCheck
var once sync . Once
2017-04-20 12:51:06 -04:00
// GetHealthCheck returns the health check which is guaranteed to be a singleton.
2016-11-26 19:48:49 +01:00
func GetHealthCheck ( ) * HealthCheck {
once . Do ( func ( ) {
singleton = newHealthCheck ( )
} )
return singleton
}
2017-03-15 19:16:06 +01:00
// Options are the public health check options.
type Options struct {
2017-11-21 05:06:03 -05:00
Path string
Port int
Transport http . RoundTripper
Interval time . Duration
LB LoadBalancer
2017-03-15 19:16:06 +01:00
}
2017-03-24 09:36:33 +01:00
func ( opt Options ) String ( ) string {
2017-09-18 15:50:03 +02:00
return fmt . Sprintf ( "[Path: %s Port: %d Interval: %s]" , opt . Path , opt . Port , opt . Interval )
2017-03-24 09:36:33 +01:00
}
2016-11-26 19:48:49 +01:00
// BackendHealthCheck HealthCheck configuration for a backend
type BackendHealthCheck struct {
2017-03-15 19:16:06 +01:00
Options
disabledURLs [ ] * url . URL
2017-03-09 16:27:31 +01:00
requestTimeout time . Duration
2016-11-26 19:48:49 +01:00
}
//HealthCheck struct
type HealthCheck struct {
Backends map [ string ] * BackendHealthCheck
2016-11-30 22:49:57 +01:00
cancel context . CancelFunc
2016-11-26 19:48:49 +01:00
}
2017-03-15 19:16:06 +01:00
// LoadBalancer includes functionality for load-balancing management.
type LoadBalancer interface {
2016-11-26 19:48:49 +01:00
RemoveServer ( u * url . URL ) error
UpsertServer ( u * url . URL , options ... roundrobin . ServerOption ) error
Servers ( ) [ ] * url . URL
}
func newHealthCheck ( ) * HealthCheck {
2017-03-09 16:27:31 +01:00
return & HealthCheck {
Backends : make ( map [ string ] * BackendHealthCheck ) ,
}
2016-11-26 19:48:49 +01:00
}
// NewBackendHealthCheck Instantiate a new BackendHealthCheck
2017-03-15 19:16:06 +01:00
func NewBackendHealthCheck ( options Options ) * BackendHealthCheck {
2017-03-09 16:27:31 +01:00
return & BackendHealthCheck {
2017-03-15 19:16:06 +01:00
Options : options ,
2017-03-09 16:27:31 +01:00
requestTimeout : 5 * time . Second ,
}
2016-11-26 19:48:49 +01:00
}
2016-11-29 19:30:51 +01:00
//SetBackendsConfiguration set backends configuration
2017-02-06 09:31:20 +01:00
func ( hc * HealthCheck ) SetBackendsConfiguration ( parentCtx context . Context , backends map [ string ] * BackendHealthCheck ) {
2016-11-26 19:48:49 +01:00
hc . Backends = backends
2016-11-30 22:49:57 +01:00
if hc . cancel != nil {
hc . cancel ( )
}
2017-01-31 22:55:02 +01:00
ctx , cancel := context . WithCancel ( parentCtx )
2016-11-30 22:49:57 +01:00
hc . cancel = cancel
2016-11-26 19:48:49 +01:00
2016-11-30 22:49:57 +01:00
for backendID , backend := range hc . Backends {
2017-01-31 22:55:02 +01:00
currentBackendID := backendID
2017-03-09 16:27:31 +01:00
currentBackend := backend
2017-01-31 22:55:02 +01:00
safe . Go ( func ( ) {
2017-03-09 16:27:31 +01:00
hc . execute ( ctx , currentBackendID , currentBackend )
2017-01-31 22:55:02 +01:00
} )
2016-11-30 22:49:57 +01:00
}
2016-11-26 19:48:49 +01:00
}
2017-03-09 16:27:31 +01:00
func ( hc * HealthCheck ) execute ( ctx context . Context , backendID string , backend * BackendHealthCheck ) {
log . Debugf ( "Initial healthcheck for currentBackend %s " , backendID )
checkBackend ( backend )
ticker := time . NewTicker ( backend . Interval )
defer ticker . Stop ( )
2017-04-11 17:10:46 +02:00
for {
select {
case <- ctx . Done ( ) :
2017-05-26 17:03:14 +02:00
log . Debug ( "Stopping all current Healthcheck goroutines" )
2017-04-11 17:10:46 +02:00
return
case <- ticker . C :
2017-03-09 16:27:31 +01:00
log . Debugf ( "Refreshing healthcheck for currentBackend %s " , backendID )
checkBackend ( backend )
}
}
}
func checkBackend ( currentBackend * BackendHealthCheck ) {
2017-03-15 19:16:06 +01:00
enabledURLs := currentBackend . LB . Servers ( )
2017-04-11 17:10:46 +02:00
var newDisabledURLs [ ] * url . URL
2017-03-15 19:16:06 +01:00
for _ , url := range currentBackend . disabledURLs {
2017-03-09 16:27:31 +01:00
if checkHealth ( url , currentBackend ) {
2017-04-11 17:10:46 +02:00
log . Debugf ( "HealthCheck is up [%s]: Upsert in server list" , url . String ( ) )
2017-03-15 19:16:06 +01:00
currentBackend . LB . UpsertServer ( url , roundrobin . Weight ( 1 ) )
2017-04-11 17:10:46 +02:00
} else {
2017-03-09 16:27:31 +01:00
log . Warnf ( "HealthCheck is still failing [%s]" , url . String ( ) )
2017-04-11 17:10:46 +02:00
newDisabledURLs = append ( newDisabledURLs , url )
}
}
2017-03-15 19:16:06 +01:00
currentBackend . disabledURLs = newDisabledURLs
2017-03-09 16:27:31 +01:00
2017-04-11 17:10:46 +02:00
for _ , url := range enabledURLs {
2017-03-09 16:27:31 +01:00
if ! checkHealth ( url , currentBackend ) {
log . Warnf ( "HealthCheck has failed [%s]: Remove from server list" , url . String ( ) )
2017-03-15 19:16:06 +01:00
currentBackend . LB . RemoveServer ( url )
currentBackend . disabledURLs = append ( currentBackend . disabledURLs , url )
2017-04-11 17:10:46 +02:00
}
}
2017-03-09 16:27:31 +01:00
}
2017-05-10 14:28:57 -04:00
func ( backend * BackendHealthCheck ) newRequest ( serverURL * url . URL ) ( * http . Request , error ) {
2017-09-18 15:50:03 +02:00
if backend . Port == 0 {
2017-11-20 09:40:03 +01:00
return http . NewRequest ( http . MethodGet , serverURL . String ( ) + backend . Path , nil )
2017-05-10 14:28:57 -04:00
}
// copy the url and add the port to the host
u := & url . URL { }
* u = * serverURL
2017-09-18 15:50:03 +02:00
u . Host = net . JoinHostPort ( u . Hostname ( ) , strconv . Itoa ( backend . Port ) )
2017-05-10 14:28:57 -04:00
u . Path = u . Path + backend . Path
2017-11-20 09:40:03 +01:00
return http . NewRequest ( http . MethodGet , u . String ( ) , nil )
2017-05-10 14:28:57 -04:00
}
2017-03-09 16:27:31 +01:00
func checkHealth ( serverURL * url . URL , backend * BackendHealthCheck ) bool {
2016-11-30 22:48:09 +01:00
client := http . Client {
2017-11-21 05:06:03 -05:00
Timeout : backend . requestTimeout ,
Transport : backend . Options . Transport ,
2016-11-30 22:48:09 +01:00
}
2017-05-10 14:28:57 -04:00
req , err := backend . newRequest ( serverURL )
if err != nil {
log . Errorf ( "Failed to create HTTP request [%s] for healthcheck: %s" , serverURL , err )
return false
}
resp , err := client . Do ( req )
2017-03-09 16:27:31 +01:00
if err == nil {
defer resp . Body . Close ( )
2016-11-26 19:48:49 +01:00
}
2017-11-20 09:40:03 +01:00
return err == nil && resp . StatusCode == http . StatusOK
2016-11-26 19:48:49 +01:00
}