aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/dnsserver/zdirectives.go1
-rw-r--r--core/plugin/zplugin.go1
-rw-r--r--plugin.cfg1
-rw-r--r--plugin/cancel/OWNERS4
-rw-r--r--plugin/cancel/README.md45
-rw-r--r--plugin/cancel/cancel.go71
-rw-r--r--plugin/cancel/cancel_test.go52
-rw-r--r--plugin/cancel/setup_test.go29
-rw-r--r--plugin/done.go14
9 files changed, 218 insertions, 0 deletions
diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go
index 89cb43e67..37326beac 100644
--- a/core/dnsserver/zdirectives.go
+++ b/core/dnsserver/zdirectives.go
@@ -11,6 +11,7 @@ package dnsserver
// care what plugin above them are doing.
var Directives = []string{
"metadata",
+ "cancel",
"tls",
"reload",
"nsid",
diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go
index 4536060c0..af0ffcb7e 100644
--- a/core/plugin/zplugin.go
+++ b/core/plugin/zplugin.go
@@ -8,6 +8,7 @@ import (
_ "github.com/coredns/coredns/plugin/autopath"
_ "github.com/coredns/coredns/plugin/bind"
_ "github.com/coredns/coredns/plugin/cache"
+ _ "github.com/coredns/coredns/plugin/cancel"
_ "github.com/coredns/coredns/plugin/chaos"
_ "github.com/coredns/coredns/plugin/debug"
_ "github.com/coredns/coredns/plugin/deprecated"
diff --git a/plugin.cfg b/plugin.cfg
index e31c9fdcf..1cbd3ed20 100644
--- a/plugin.cfg
+++ b/plugin.cfg
@@ -20,6 +20,7 @@
# log:log
metadata:metadata
+cancel:cancel
tls:tls
reload:reload
nsid:nsid
diff --git a/plugin/cancel/OWNERS b/plugin/cancel/OWNERS
new file mode 100644
index 000000000..eee46f686
--- /dev/null
+++ b/plugin/cancel/OWNERS
@@ -0,0 +1,4 @@
+reviewers:
+ - miekg
+approvers:
+ - miekg
diff --git a/plugin/cancel/README.md b/plugin/cancel/README.md
new file mode 100644
index 000000000..1615f6409
--- /dev/null
+++ b/plugin/cancel/README.md
@@ -0,0 +1,45 @@
+# cancel
+
+## Name
+
+*cancel* - a plugin that cancels a request's context after 5001 milliseconds.
+
+## Description
+
+The *cancel* plugin creates a canceling context for each request. It adds a timeout that gets
+triggered after 5001 milliseconds.
+
+The 5001 number is chosen because the default timeout for DNS clients is 5 seconds, after that they
+give up.
+
+A plugin interested in the cancellation status should call `plugin.Done()` on the context. If the
+context was canceled due to a timeout the plugin should not write anything back to the client and
+return a value indicating CoreDNS should not either; a zero return value should suffice for that.
+
+~~~ txt
+cancel [TIMEOUT]
+~~~
+
+* **TIMEOUT** allows setting a custom timeout. The default timeout is 5001 milliseconds (`5001 ms`)
+
+## Examples
+
+~~~ corefile
+. {
+ cancel
+ whoami
+}
+~~~
+
+Or with a custom timeout:
+
+~~~ corefile
+. {
+ cancel 1s
+ whoami
+}
+~~~
+
+## Also See
+
+The Go documentation for the context package.
diff --git a/plugin/cancel/cancel.go b/plugin/cancel/cancel.go
new file mode 100644
index 000000000..68df153d9
--- /dev/null
+++ b/plugin/cancel/cancel.go
@@ -0,0 +1,71 @@
+// Package cancel implements a plugin adds a canceling context to each request.
+package cancel
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/mholt/caddy"
+ "github.com/miekg/dns"
+)
+
+func init() {
+ caddy.RegisterPlugin("cancel", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ ca := Cancel{timeout: 5001 * time.Millisecond}
+
+ for c.Next() {
+ args := c.RemainingArgs()
+ switch len(args) {
+ case 0:
+ break
+ case 1:
+ dur, err := time.ParseDuration(args[0])
+ if err != nil {
+ return plugin.Error("cancel", fmt.Errorf("invalid duration: %q", args[0]))
+ }
+ if dur <= 0 {
+ return plugin.Error("cancel", fmt.Errorf("invalid negative duration: %q", args[0]))
+ }
+ ca.timeout = dur
+ default:
+ return plugin.Error("cancel", c.ArgErr())
+ }
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ ca.Next = next
+ return ca
+ })
+
+ return nil
+}
+
+// Cancel is a plugin that adds a canceling context to each request's context.
+type Cancel struct {
+ timeout time.Duration
+ Next plugin.Handler
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (c Cancel) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ ctx, cancel := context.WithTimeout(ctx, c.timeout)
+
+ code, err := plugin.NextOrFailure(c.Name(), c.Next, ctx, w, r)
+
+ cancel()
+
+ return code, err
+}
+
+// Name implements the Handler interface.
+func (c Cancel) Name() string { return "cancel" }
diff --git a/plugin/cancel/cancel_test.go b/plugin/cancel/cancel_test.go
new file mode 100644
index 000000000..ceba9f5d2
--- /dev/null
+++ b/plugin/cancel/cancel_test.go
@@ -0,0 +1,52 @@
+package cancel
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnstest"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+)
+
+type sleepPlugin struct{}
+
+func (s sleepPlugin) Name() string { return "sleep" }
+
+func (s sleepPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ i := 0
+ m := new(dns.Msg)
+ m.SetReply(r)
+ for {
+ if plugin.Done(ctx) {
+ m.Rcode = dns.RcodeBadTime // use BadTime to return something time related
+ w.WriteMsg(m)
+ return 0, nil
+ }
+ time.Sleep(20 * time.Millisecond)
+ i++
+ if i > 2 {
+ m.Rcode = dns.RcodeServerFailure
+ w.WriteMsg(m)
+ return 0, nil
+ }
+ }
+ return 0, nil
+}
+
+func TestCancel(t *testing.T) {
+ ca := Cancel{Next: sleepPlugin{}, timeout: 20 * time.Millisecond}
+ ctx := context.Background()
+
+ w := dnstest.NewRecorder(&test.ResponseWriter{})
+ m := new(dns.Msg)
+ m.SetQuestion("aaa.example.com.", dns.TypeTXT)
+
+ ca.ServeDNS(ctx, w, m)
+ if w.Rcode != dns.RcodeBadTime {
+ t.Error("Expected ServeDNS to be canceled by context")
+ }
+}
diff --git a/plugin/cancel/setup_test.go b/plugin/cancel/setup_test.go
new file mode 100644
index 000000000..64ae486a2
--- /dev/null
+++ b/plugin/cancel/setup_test.go
@@ -0,0 +1,29 @@
+package cancel
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+)
+
+func TestSetup(t *testing.T) {
+ c := caddy.NewTestController("dns", `cancel`)
+ if err := setup(c); err != nil {
+ t.Errorf("Test 1, expected no errors, but got: %q", err)
+ }
+
+ c = caddy.NewTestController("dns", `cancel 5s`)
+ if err := setup(c); err != nil {
+ t.Errorf("Test 2, expected no errors, but got: %q", err)
+ }
+
+ c = caddy.NewTestController("dns", `cancel 5`)
+ if err := setup(c); err == nil {
+ t.Errorf("Test 3, expected errors, but got none")
+ }
+
+ c = caddy.NewTestController("dns", `cancel -1s`)
+ if err := setup(c); err == nil {
+ t.Errorf("Test 4, expected errors, but got none")
+ }
+}
diff --git a/plugin/done.go b/plugin/done.go
new file mode 100644
index 000000000..3f53273da
--- /dev/null
+++ b/plugin/done.go
@@ -0,0 +1,14 @@
+package plugin
+
+import "context"
+
+// Done is a non-blocking function that returns true if the context has been canceled.
+func Done(ctx context.Context) bool {
+ select {
+ case <-ctx.Done():
+ return true
+ default:
+ return false
+ }
+ return false
+}