mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 08:14:18 -04:00
Do not interrupt querying readiness probes for plugins (#6975)
* Do not interrupt querying readiness probes for plugins Signed-off-by: Gleb Kogtev <gleb.kogtev@gmail.com> * Add monitor param for ready plugin Signed-off-by: Gleb Kogtev <gleb.kogtev@gmail.com> * Update ready docs Signed-off-by: Gleb Kogtev <gleb.kogtev@gmail.com> * Update ready docs Signed-off-by: Gleb Kogtev <gleb.kogtev@gmail.com> --------- Signed-off-by: Gleb Kogtev <gleb.kogtev@gmail.com>
This commit is contained in:
@@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
By enabling *ready* an HTTP endpoint on port 8181 will return 200 OK, when all plugins that are able
|
By enabling *ready* an HTTP endpoint on port 8181 will return 200 OK, when all plugins that are able
|
||||||
to signal readiness have done so. If some are not ready yet the endpoint will return a 503 with the
|
to signal readiness have done so. If some are not ready yet the endpoint will return a 503 with the
|
||||||
body containing the list of plugins that are not ready. Once a plugin has signaled it is ready it
|
body containing the list of plugins that are not ready.
|
||||||
will not be queried again.
|
|
||||||
|
|
||||||
Each Server Block that enables the *ready* plugin will have the plugins *in that server block*
|
Each Server Block that enables the *ready* plugin will have the plugins *in that server block*
|
||||||
report readiness into the /ready endpoint that runs on the same port. This also means that the
|
report readiness into the /ready endpoint that runs on the same port. This also means that the
|
||||||
@@ -19,12 +18,20 @@ their readiness reported as the union of their respective readinesses.
|
|||||||
## Syntax
|
## Syntax
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
ready [ADDRESS]
|
ready [ADDRESS] {
|
||||||
|
monitor until-ready|continuously
|
||||||
|
}
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
*ready* optionally takes an address; the default is `:8181`. The path is fixed to `/ready`. The
|
*ready* optionally takes an address; the default is `:8181`. The path is fixed to `/ready`. The
|
||||||
readiness endpoint returns a 200 response code and the word "OK" when this server is ready. It
|
readiness endpoint returns a 200 response code and the word "OK" when this server is ready. It
|
||||||
returns a 503 otherwise *and* the list of plugins that are not ready.
|
returns a 503 otherwise *and* the list of plugins that are not ready.
|
||||||
|
By default, once a plugin has signaled it is ready it will not be queried again.
|
||||||
|
|
||||||
|
The *ready* directive can include an optional `monitor` parameter, defaulting to `until-ready`. The following values are supported:
|
||||||
|
|
||||||
|
* `until-ready` - once a plugin signals it is ready, it will not be checked again. This mode assumes stability after the initial readiness confirmation.
|
||||||
|
* `continuously` - in this mode, plugins are continuously monitored for readiness. This means a plugin may transition between ready and not ready states, providing real-time status updates.
|
||||||
|
|
||||||
## Plugins
|
## Plugins
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ type list struct {
|
|||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
rs []Readiness
|
rs []Readiness
|
||||||
names []string
|
names []string
|
||||||
|
|
||||||
|
// keepReadiness indicates whether the readiness status of plugins should be retained
|
||||||
|
// after they have been confirmed as ready. When set to false, the plugin readiness
|
||||||
|
// status will be reset to nil to conserve resources, assuming ready plugins don't
|
||||||
|
// need continuous monitoring.
|
||||||
|
keepReadiness bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset resets l
|
// Reset resets l
|
||||||
@@ -40,13 +46,14 @@ func (l *list) Ready() (bool, string) {
|
|||||||
if r == nil {
|
if r == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !r.Ready() {
|
if r.Ready() {
|
||||||
ok = false
|
if !l.keepReadiness {
|
||||||
s = append(s, l.names[i])
|
l.rs[i] = nil
|
||||||
} else {
|
}
|
||||||
// if ok, this plugin is ready and will not be queried anymore.
|
continue
|
||||||
l.rs[i] = nil
|
|
||||||
}
|
}
|
||||||
|
ok = false
|
||||||
|
s = append(s, l.names[i])
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
return true, ""
|
return true, ""
|
||||||
|
|||||||
@@ -50,15 +50,15 @@ func (rd *ready) onStartup() error {
|
|||||||
io.WriteString(w, "Shutting down")
|
io.WriteString(w, "Shutting down")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ok, todo := plugins.Ready()
|
ready, notReadyPlugins := plugins.Ready()
|
||||||
if ok {
|
if ready {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
io.WriteString(w, http.StatusText(http.StatusOK))
|
io.WriteString(w, http.StatusText(http.StatusOK))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infof("Still waiting on: %q", todo)
|
log.Infof("Plugins not ready: %q", notReadyPlugins)
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
io.WriteString(w, todo)
|
io.WriteString(w, notReadyPlugins)
|
||||||
})
|
})
|
||||||
|
|
||||||
go func() { http.Serve(rd.ln, rd.mux) }()
|
go func() { http.Serve(rd.ln, rd.mux) }()
|
||||||
|
|||||||
@@ -67,3 +67,57 @@ func TestReady(t *testing.T) {
|
|||||||
}
|
}
|
||||||
response.Body.Close()
|
response.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReady_Continuously(t *testing.T) {
|
||||||
|
rd := &ready{Addr: ":0"}
|
||||||
|
e := &erratic.Erratic{}
|
||||||
|
plugins.Append(e, "erratic")
|
||||||
|
plugins.keepReadiness = true
|
||||||
|
|
||||||
|
if err := rd.onStartup(); err != nil {
|
||||||
|
t.Fatalf("Unable to startup the readiness server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rd.onFinalShutdown()
|
||||||
|
|
||||||
|
address := fmt.Sprintf("http://%s/ready", rd.ln.Addr().String())
|
||||||
|
|
||||||
|
response, err := http.Get(address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to query %s: %v", address, err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusServiceUnavailable {
|
||||||
|
t.Errorf("Invalid status code: expecting %d, got %d", 503, response.StatusCode)
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
|
||||||
|
// make it ready by giving erratic 3 queries.
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("example.org.", dns.TypeA)
|
||||||
|
e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m)
|
||||||
|
e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m)
|
||||||
|
e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m)
|
||||||
|
|
||||||
|
response, err = http.Get(address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to query %s: %v", address, err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Invalid status code: expecting %d, got %d", 200, response.StatusCode)
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
|
||||||
|
// make erratic not-ready by giving it more queries, this should change the process readiness
|
||||||
|
e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m)
|
||||||
|
e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m)
|
||||||
|
e.ServeDNS(context.TODO(), &test.ResponseWriter{}, m)
|
||||||
|
|
||||||
|
response, err = http.Get(address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to query %s: %v", address, err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusServiceUnavailable {
|
||||||
|
t.Errorf("Invalid status code: expecting %d, got %d", 503, response.StatusCode)
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,17 @@ import (
|
|||||||
func init() { plugin.Register("ready", setup) }
|
func init() { plugin.Register("ready", setup) }
|
||||||
|
|
||||||
func setup(c *caddy.Controller) error {
|
func setup(c *caddy.Controller) error {
|
||||||
addr, err := parse(c)
|
addr, monType, err := parse(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plugin.Error("ready", err)
|
return plugin.Error("ready", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if monType == monitorTypeContinuously {
|
||||||
|
plugins.keepReadiness = true
|
||||||
|
} else {
|
||||||
|
plugins.keepReadiness = false
|
||||||
|
}
|
||||||
|
|
||||||
rd := &ready{Addr: addr}
|
rd := &ready{Addr: addr}
|
||||||
|
|
||||||
uniqAddr.Set(addr, rd.onStartup)
|
uniqAddr.Set(addr, rd.onStartup)
|
||||||
@@ -48,12 +55,25 @@ func setup(c *caddy.Controller) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse(c *caddy.Controller) (string, error) {
|
// monitorType represents the type of monitoring behavior for the readiness plugin.
|
||||||
|
type monitorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// monitorTypeUntilReady indicates the monitoring should continue until the system is ready.
|
||||||
|
monitorTypeUntilReady monitorType = "until-ready"
|
||||||
|
|
||||||
|
// monitorTypeContinuously indicates the monitoring should continue indefinitely.
|
||||||
|
monitorTypeContinuously monitorType = "continuously"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parse(c *caddy.Controller) (string, monitorType, error) {
|
||||||
addr := ":8181"
|
addr := ":8181"
|
||||||
|
monType := monitorTypeUntilReady
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for c.Next() {
|
for c.Next() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
return "", plugin.ErrOnce
|
return "", "", plugin.ErrOnce
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
args := c.RemainingArgs()
|
args := c.RemainingArgs()
|
||||||
@@ -63,11 +83,38 @@ func parse(c *caddy.Controller) (string, error) {
|
|||||||
case 1:
|
case 1:
|
||||||
addr = args[0]
|
addr = args[0]
|
||||||
if _, _, e := net.SplitHostPort(addr); e != nil {
|
if _, _, e := net.SplitHostPort(addr); e != nil {
|
||||||
return "", e
|
return "", "", e
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return "", c.ArgErr()
|
return "", "", c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
for c.NextBlock() {
|
||||||
|
switch c.Val() {
|
||||||
|
case "monitor":
|
||||||
|
args := c.RemainingArgs()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return "", "", c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
monType, err = parseMonitorType(c, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return addr, nil
|
return addr, monType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMonitorType(c *caddy.Controller, arg string) (monitorType, error) {
|
||||||
|
switch arg {
|
||||||
|
case "until-ready":
|
||||||
|
return monitorTypeUntilReady, nil
|
||||||
|
case "continuously":
|
||||||
|
return monitorTypeContinuously, nil
|
||||||
|
default:
|
||||||
|
return "", c.Errf("monitor type '%s' not supported", arg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,82 @@ import (
|
|||||||
|
|
||||||
func TestSetupReady(t *testing.T) {
|
func TestSetupReady(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
|
|
||||||
|
expectedAddr string
|
||||||
|
expectedMonitorType monitorType
|
||||||
|
|
||||||
shouldErr bool
|
shouldErr bool
|
||||||
}{
|
}{
|
||||||
{`ready`, false},
|
{
|
||||||
{`ready localhost:1234`, false},
|
input: `ready`,
|
||||||
{`ready localhost:1234 b`, true},
|
expectedAddr: ":8181",
|
||||||
{`ready bla`, true},
|
expectedMonitorType: monitorTypeUntilReady,
|
||||||
{`ready bla bla`, true},
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `ready localhost:1234`,
|
||||||
|
expectedAddr: "localhost:1234",
|
||||||
|
expectedMonitorType: monitorTypeUntilReady,
|
||||||
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
ready {
|
||||||
|
monitor until-ready
|
||||||
|
}`,
|
||||||
|
expectedAddr: ":8181",
|
||||||
|
expectedMonitorType: monitorTypeUntilReady,
|
||||||
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
ready {
|
||||||
|
monitor continuously
|
||||||
|
}`,
|
||||||
|
expectedAddr: ":8181",
|
||||||
|
expectedMonitorType: monitorTypeContinuously,
|
||||||
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
ready localhost:1234 {
|
||||||
|
monitor continuously
|
||||||
|
}`,
|
||||||
|
expectedAddr: "localhost:1234",
|
||||||
|
expectedMonitorType: monitorTypeContinuously,
|
||||||
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
ready localhost:1234 {
|
||||||
|
monitor 404
|
||||||
|
}`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `ready localhost:1234 b`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `ready bla`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `ready bla bla`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
_, err := parse(caddy.NewTestController("dns", test.input))
|
actualAddress, actualMonitorType, err := parse(caddy.NewTestController("dns", test.input))
|
||||||
|
|
||||||
|
if actualAddress != test.expectedAddr {
|
||||||
|
t.Errorf("Test %d: Expected address %s but found %s for input %s", i, test.expectedAddr, actualAddress, test.input)
|
||||||
|
}
|
||||||
|
if actualMonitorType != test.expectedMonitorType {
|
||||||
|
t.Errorf("Test %d: Expected monitor type %s but found %s for input %s", i, test.expectedMonitorType, actualMonitorType, test.input)
|
||||||
|
}
|
||||||
|
|
||||||
if test.shouldErr && err == nil {
|
if test.shouldErr && err == nil {
|
||||||
t.Errorf("Test %d: Expected error but found none for input %s", i, test.input)
|
t.Errorf("Test %d: Expected error but found none for input %s", i, test.input)
|
||||||
|
|||||||
Reference in New Issue
Block a user