Add *ready* plugin (#2616)
Add a ready plugin that allows plugin to signal when they are ready. Once a plugin is ready it is not queried again. This uses same mechanism as the health plugin: each plugin needs to implement an interface. Implement readines for the *erratic* plugin to aid in testing. Add README.md and tests moduled after the health plugin; which will be relegated to just providing process health. In similar vein to health this is a process wide setting. With this Corefile: ~~~ . { erratic whoami ready } bla { erratic whoami } ~~~ ready will lead to: ~~~ sh % curl localhost:8181/ready % dig @localhost -p 1053 mx example.org % curl localhost:8181/ready OK% ~~~ Meanwhile CoreDNS logs: ~~~ .:1053 bla.:1053 2019-02-26T20:59:07.137Z [INFO] CoreDNS-1.3.1 2019-02-26T20:59:07.137Z [INFO] linux/amd64, go1.11.4, CoreDNS-1.3.1 linux/amd64, go1.11.4, 2019-02-26T20:59:11.415Z [INFO] plugin/ready: Still waiting on: "erratic" 2019-02-26T20:59:13.510Z [INFO] plugin/ready: Still waiting on: "erratic" ~~~ *ready* can be used in multiple server blocks and will do the right thing; query all those plugins from all server blocks for readiness. This does a similar thing to the prometheus plugin. Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
parent
2b7e84a076
commit
db0b16b615
13 changed files with 409 additions and 2 deletions
|
@ -18,6 +18,7 @@ var Directives = []string{
|
|||
"bind",
|
||||
"debug",
|
||||
"trace",
|
||||
"ready",
|
||||
"health",
|
||||
"pprof",
|
||||
"prometheus",
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
_ "github.com/coredns/coredns/plugin/metrics"
|
||||
_ "github.com/coredns/coredns/plugin/nsid"
|
||||
_ "github.com/coredns/coredns/plugin/pprof"
|
||||
_ "github.com/coredns/coredns/plugin/ready"
|
||||
_ "github.com/coredns/coredns/plugin/reload"
|
||||
_ "github.com/coredns/coredns/plugin/rewrite"
|
||||
_ "github.com/coredns/coredns/plugin/root"
|
||||
|
|
|
@ -27,6 +27,7 @@ root:root
|
|||
bind:bind
|
||||
debug:debug
|
||||
trace:trace
|
||||
ready:ready
|
||||
health:health
|
||||
pprof:pprof
|
||||
prometheus:metrics
|
||||
|
|
|
@ -57,8 +57,13 @@ server.
|
|||
|
||||
When exporting metrics the *Namespace* should be `plugin.Namespace` (="coredns"), and the
|
||||
*Subsystem* should be the name of the plugin. The README.md for the plugin should then also contain
|
||||
a *Metrics* section detailing the metrics. If the plugin supports dynamic health reporting it
|
||||
should also have *Health* section detailing on some of its inner workings.
|
||||
a *Metrics* section detailing the metrics.
|
||||
|
||||
If the plugin supports dynamic health reporting it should also have *Health* section detailing on
|
||||
some of its inner workings.
|
||||
|
||||
If the plugins supports signalling readiness it should have a *Ready* section detailing how it
|
||||
works.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
@ -37,6 +37,10 @@ In case of a zone transfer and truncate the final SOA record *isn't* added to th
|
|||
|
||||
This plugin implements dynamic health checking. For every dropped query it turns unhealthy.
|
||||
|
||||
## Ready
|
||||
|
||||
This plugin reports readiness to the ready plugin.
|
||||
|
||||
## Examples
|
||||
|
||||
~~~ corefile
|
||||
|
|
13
plugin/erratic/ready.go
Normal file
13
plugin/erratic/ready.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package erratic
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
// Ready returns true if the number of received queries is in the range [3, 5). All other values return false.
|
||||
// To aid in testing we want to this flip between ready and not ready.
|
||||
func (e *Erratic) Ready() bool {
|
||||
q := atomic.LoadUint64(&e.q)
|
||||
if q >= 3 && q < 5 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
56
plugin/ready/README.md
Normal file
56
plugin/ready/README.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
# ready
|
||||
|
||||
## Name
|
||||
|
||||
*ready* - enables a readiness check HTTP endpoint.
|
||||
|
||||
## Description
|
||||
|
||||
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
|
||||
body containing the list of plugins that are not ready. Once a plugin has signaled it is ready it
|
||||
will not be queried again.
|
||||
|
||||
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.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
ready [ADDRESS]
|
||||
~~~
|
||||
|
||||
*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
|
||||
returns a 503 otherwise.
|
||||
|
||||
## Plugins
|
||||
|
||||
Any plugin wanting to signal readiness will need to implement the `ready.Readiness` interface by
|
||||
implementing a method `Ready() bool` that returns true when the plugin is ready and false otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
Let *ready* report readiness for both the `.` and `example.org` servers (assuming the *whois*
|
||||
plugin also exports readiness):
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
ready
|
||||
erratic
|
||||
}
|
||||
|
||||
example.org {
|
||||
ready
|
||||
whoami
|
||||
}
|
||||
|
||||
~~~
|
||||
|
||||
Run *ready* on a different port.
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
ready localhost:8091
|
||||
}
|
||||
~~~
|
48
plugin/ready/list.go
Normal file
48
plugin/ready/list.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package ready
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// list is structure that holds the plugins that signals readiness for this server block.
|
||||
type list struct {
|
||||
sync.RWMutex
|
||||
rs []Readiness
|
||||
names []string
|
||||
}
|
||||
|
||||
// Append adds a new readiness to l.
|
||||
func (l *list) Append(r Readiness, name string) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.rs = append(l.rs, r)
|
||||
l.names = append(l.names, name)
|
||||
}
|
||||
|
||||
// Ready return true when all plugins ready, if the returned value is false the string
|
||||
// contains a comma separated list of plugins that are not ready.
|
||||
func (l *list) Ready() (bool, string) {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
ok := true
|
||||
s := []string{}
|
||||
for i, r := range l.rs {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
if !r.Ready() {
|
||||
ok = false
|
||||
s = append(s, l.names[i])
|
||||
} else {
|
||||
// if ok, this plugin is ready and will not be queried anymore.
|
||||
l.rs[i] = nil
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
return true, ""
|
||||
}
|
||||
sort.Strings(s)
|
||||
return false, strings.Join(s, ",")
|
||||
}
|
7
plugin/ready/readiness.go
Normal file
7
plugin/ready/readiness.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package ready
|
||||
|
||||
// The Readiness interface needs to be implemented by each plugin willing to provide a readiness check.
|
||||
type Readiness interface {
|
||||
// Ready is called by ready to see whether the plugin is ready.
|
||||
Ready() bool
|
||||
}
|
81
plugin/ready/ready.go
Normal file
81
plugin/ready/ready.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// Package ready is used to signal readiness of the CoreDNS process. Once all
|
||||
// plugins have called in the plugin will signal readiness by returning a 200
|
||||
// OK on the HTTP handler (on port 8181). If not ready yet, the handler will
|
||||
// return a 503.
|
||||
package ready
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/uniq"
|
||||
)
|
||||
|
||||
var (
|
||||
log = clog.NewWithPlugin("ready")
|
||||
plugins = &list{}
|
||||
uniqAddr = uniq.New()
|
||||
)
|
||||
|
||||
type ready struct {
|
||||
Addr string
|
||||
|
||||
sync.RWMutex
|
||||
ln net.Listener
|
||||
done bool
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func (rd *ready) onStartup() error {
|
||||
if rd.Addr == "" {
|
||||
rd.Addr = defAddr
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", rd.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rd.Lock()
|
||||
rd.ln = ln
|
||||
rd.mux = http.NewServeMux()
|
||||
rd.done = true
|
||||
rd.Unlock()
|
||||
|
||||
rd.mux.HandleFunc("/ready", func(w http.ResponseWriter, _ *http.Request) {
|
||||
ok, todo := plugins.Ready()
|
||||
if ok {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, "OK")
|
||||
return
|
||||
}
|
||||
log.Infof("Still waiting on: %q", todo)
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
io.WriteString(w, todo)
|
||||
})
|
||||
|
||||
go func() { http.Serve(rd.ln, rd.mux) }()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rd *ready) onRestart() error { return rd.onFinalShutdown() }
|
||||
|
||||
func (rd *ready) onFinalShutdown() error {
|
||||
rd.Lock()
|
||||
defer rd.Unlock()
|
||||
if !rd.done {
|
||||
return nil
|
||||
}
|
||||
|
||||
uniqAddr.Unset(rd.Addr)
|
||||
|
||||
rd.ln.Close()
|
||||
rd.done = false
|
||||
return nil
|
||||
}
|
||||
|
||||
const defAddr = ":8181"
|
81
plugin/ready/ready_test.go
Normal file
81
plugin/ready/ready_test.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package ready
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/erratic"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func init() { clog.Discard() }
|
||||
|
||||
func TestReady(t *testing.T) {
|
||||
rd := &ready{Addr: ":0"}
|
||||
e := &erratic.Erratic{}
|
||||
plugins.Append(e, "erratic")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := rd.onStartup(); err != nil {
|
||||
t.Fatalf("Unable to startup the readiness server: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
defer rd.onFinalShutdown()
|
||||
|
||||
address := fmt.Sprintf("http://%s/ready", rd.ln.Addr().String())
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
response, err := http.Get(address)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to query %s: %v", address, err)
|
||||
}
|
||||
if response.StatusCode != 503 {
|
||||
t.Errorf("Invalid status code: expecting %d, got %d", 503, response.StatusCode)
|
||||
}
|
||||
response.Body.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
// 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 != 200 {
|
||||
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 not 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 != 200 {
|
||||
t.Errorf("Invalid status code: expecting %d, got %d", 200, response.StatusCode)
|
||||
}
|
||||
response.Body.Close()
|
||||
}
|
75
plugin/ready/setup.go
Normal file
75
plugin/ready/setup.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package ready
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("ready", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
addr, err := parse(c)
|
||||
if err != nil {
|
||||
return plugin.Error("ready", err)
|
||||
}
|
||||
|
||||
rd := &ready{Addr: addr}
|
||||
|
||||
uniqAddr.Set(addr, rd.onStartup, rd)
|
||||
|
||||
c.OncePerServerBlock(func() error {
|
||||
c.OnStartup(func() error {
|
||||
return uniqAddr.ForEach()
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
// Each plugin in this server block will (if they support it) report readiness.
|
||||
plugs := dnsserver.GetConfig(c).Handlers()
|
||||
for _, p := range plugs {
|
||||
if r, ok := p.(Readiness); ok {
|
||||
plugins.Append(r, p.Name())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.OnRestart(rd.onRestart)
|
||||
c.OnFinalShutdown(rd.onFinalShutdown)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) (string, error) {
|
||||
addr := ""
|
||||
i := 0
|
||||
for c.Next() {
|
||||
if i > 0 {
|
||||
return "", plugin.ErrOnce
|
||||
}
|
||||
i++
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
addr = args[0]
|
||||
if _, _, e := net.SplitHostPort(addr); e != nil {
|
||||
return "", e
|
||||
}
|
||||
default:
|
||||
return "", c.ArgErr()
|
||||
}
|
||||
}
|
||||
return addr, nil
|
||||
}
|
34
plugin/ready/setup_test.go
Normal file
34
plugin/ready/setup_test.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package ready
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetupReady(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`ready`, false},
|
||||
{`ready localhost:1234`, false},
|
||||
{`ready localhost:1234 b`, true},
|
||||
{`ready bla`, true},
|
||||
{`ready bla bla`, true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := parse(caddy.NewTestController("dns", test.input))
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but found none for input %s", i, test.input)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !test.shouldErr {
|
||||
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue