diff --git a/Gopkg.lock b/Gopkg.lock index be00cce3..85f45d9a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,31 +2,48 @@ [[projects]] + digest = "1:b16fbfbcc20645cb419f78325bb2e85ec729b338e996a228124d68931a6f2a37" name = "github.com/BurntSushi/toml" packages = ["."] + pruneopts = "UT" revision = "b26d9c308763d68093482582cea63d69be07a0f0" version = "v0.3.0" [[projects]] branch = "master" + digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d" name = "github.com/beorn7/perks" packages = ["quantile"] + pruneopts = "UT" revision = "3a771d992973f24aa725d07868b467d1ddfceafb" [[projects]] + digest = "1:fed1f537c2f1269fe475a8556c393fe466641682d73ef8fd0491cd3aa1e47bad" name = "github.com/certifi/gocertifi" packages = ["."] + pruneopts = "UT" revision = "deb3ae2ef2610fde3330947281941c562861188b" version = "2018.01.18" [[projects]] branch = "master" + digest = "1:e5003c19d396d8b3cf1324ea0bf49b00f13e9466d0297d1268b641f1c617c3a2" name = "github.com/cloudflare/brotli-go" packages = ["."] + pruneopts = "T" revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28" [[projects]] branch = "master" + digest = "1:6dbb2bbc7e6333e691c4d82fd86485f0695a35902fbb9b2df5f72e22ab0040f3" + name = "github.com/cloudflare/golibs" + packages = ["lrucache"] + pruneopts = "UT" + revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1" + +[[projects]] + branch = "master" + digest = "1:af68c8bdac3ceaf46c67f0c43cc1ee5f10e705d8e04f8a63fa41231d0be6b0fb" name = "github.com/coredns/coredns" packages = [ "core/dnsserver", @@ -52,52 +69,64 @@ "plugin/pkg/uniq", "plugin/pkg/watch", "plugin/test", - "request" + "request", ] + pruneopts = "UT" revision = "992e7928c7c258628d2b13b769acc86781b9faea" [[projects]] branch = "master" + digest = "1:6a503e232df389d94ebb97dfb22d4ae463b6e2f351660613e11d9e42f57ab6df" name = "github.com/coreos/go-oidc" packages = [ "http", "jose", "key", "oauth2", - "oidc" + "oidc", ] + pruneopts = "UT" revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df" [[projects]] + digest = "1:1da3a221f0bc090792d3a2a080ff09008427c0e0f0533a4ed6abd8994421da73" name = "github.com/coreos/go-systemd" packages = ["daemon"] + pruneopts = "UT" revision = "39ca1b05acc7ad1220e09f133283b8859a8b71ab" version = "v17" [[projects]] + digest = "1:6fda0d7f5e52b081e075775b1ecebf1ea0c923e7be33604ed0225ae078e701b5" name = "github.com/coreos/pkg" packages = [ "health", "httputil", - "timeutil" + "timeutil", ] + pruneopts = "UT" revision = "97fdf19511ea361ae1c100dd393cc47f8dcfa1e1" version = "v4" [[projects]] + digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "UT" revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" [[projects]] branch = "master" + digest = "1:c013ffc6e15f9f898078f9d38441c68b228aa7b899659452170250ccb27f5f1e" name = "github.com/elgs/gosqljson" packages = ["."] + pruneopts = "UT" revision = "027aa4915315a0b2825c0f025cea347829b974fa" [[projects]] branch = "master" + digest = "1:15eadee5930d325154e5e1f3c4f4b28ab62f9c0aa949580e8d9edabaf0b64e85" name = "github.com/equinox-io/equinox" packages = [ ".", @@ -105,209 +134,267 @@ "internal/go-update/internal/binarydist", "internal/go-update/internal/osext", "internal/osext", - "proto" + "proto", ] + pruneopts = "UT" revision = "f24972fa72facf59d05c91c848b65eac38815915" [[projects]] branch = "master" + digest = "1:433763f10d88dba9b533a7ea2fe9f5ee11e57e00306eb97a1f6090fd978e8fa1" name = "github.com/facebookgo/grace" packages = ["gracenet"] + pruneopts = "UT" revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e" [[projects]] branch = "master" + digest = "1:50a46ab1d5edbbdd55125b4d37f1bf503d0807c26461f9ad7b358d6006641d09" name = "github.com/flynn/go-shlex" packages = ["."] + pruneopts = "UT" revision = "3f9db97f856818214da2e1057f8ad84803971cff" [[projects]] branch = "master" + digest = "1:d4623fc7bf7e281d9107367cc4a9e76ed3e86b1eec1a4e30630c870bef1fedd0" name = "github.com/getsentry/raven-go" packages = ["."] + pruneopts = "UT" revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa" [[projects]] branch = "master" + digest = "1:3e6afc3ed8a72949aa735c00fddc23427dc9384ccfd51cf0d91a412e668da632" name = "github.com/golang-collections/collections" packages = ["queue"] + pruneopts = "UT" revision = "604e922904d35e97f98a774db7881f049cd8d970" [[projects]] + digest = "1:17fe264ee908afc795734e8c4e63db2accabaf57326dbf21763a7d6b86096260" name = "github.com/golang/protobuf" packages = [ "proto", "ptypes", "ptypes/any", "ptypes/duration", - "ptypes/timestamp" + "ptypes/timestamp", ] + pruneopts = "UT" revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" version = "v1.1.0" [[projects]] + digest = "1:8f8811f9be822914c3a25c6a071e93beb4c805d7b026cbf298bc577bc1cc945b" name = "github.com/google/uuid" packages = ["."] + pruneopts = "UT" revision = "064e2069ce9c359c118179501254f67d7d37ba24" version = "0.2" [[projects]] + digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" name = "github.com/gorilla/context" packages = ["."] + pruneopts = "UT" revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" version = "v1.1.1" [[projects]] + digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f" name = "github.com/gorilla/mux" packages = ["."] + pruneopts = "UT" revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" version = "v1.6.2" [[projects]] + digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e" name = "github.com/gorilla/websocket" packages = ["."] + pruneopts = "UT" revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" version = "v1.2.0" [[projects]] branch = "master" + digest = "1:1a1206efd03a54d336dce7bb8719e74f2f8932f661cb9f57d5813a1d99c083d8" name = "github.com/grpc-ecosystem/grpc-opentracing" packages = ["go/otgrpc"] + pruneopts = "UT" revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746" [[projects]] + digest = "1:75ab90ae3f5d876167e60f493beadfe66f0ed861a710f283fb06c86437a09538" name = "github.com/jonboulle/clockwork" packages = ["."] + pruneopts = "UT" revision = "2eee05ed794112d45db504eb05aa693efd2b8b09" version = "v0.1.0" [[projects]] branch = "master" + digest = "1:37ce7d7d80531b227023331002c0d42b4b4b291a96798c82a049d03a54ba79e4" name = "github.com/lib/pq" packages = [ ".", - "oid" + "oid", ] + pruneopts = "UT" revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8" [[projects]] + digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67" name = "github.com/mattn/go-colorable" packages = ["."] + pruneopts = "UT" revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" version = "v0.0.9" [[projects]] + digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb" name = "github.com/mattn/go-isatty" packages = ["."] + pruneopts = "UT" revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" [[projects]] + digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] + pruneopts = "UT" revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" version = "v1.0.1" [[projects]] branch = "master" + digest = "1:75fa16a231ef40da3e462d651c20b9df20bde0777bdc1ac0982242c79057ee71" name = "github.com/mholt/caddy" packages = [ ".", "caddyfile", - "telemetry" + "telemetry", ] + pruneopts = "UT" revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc" [[projects]] + digest = "1:463e4140189f8194f9121ca1c7fe3b8e9e9a2ab3d949b43c835c21034927dc62" name = "github.com/miekg/dns" packages = ["."] + pruneopts = "UT" revision = "5a2b9fab83ff0f8bfc99684bd5f43a37abe560f1" version = "v1.0.8" [[projects]] branch = "master" + digest = "1:8eb17c2ec4df79193ae65b621cd1c0c4697db3bc317fe6afdc76d7f2746abd05" name = "github.com/mitchellh/go-homedir" packages = ["."] + pruneopts = "UT" revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66" [[projects]] + digest = "1:450b7623b185031f3a456801155c8320209f75d0d4c4e633c6b1e59d44d6e392" name = "github.com/opentracing/opentracing-go" packages = [ ".", "ext", - "log" + "log", ] + pruneopts = "UT" revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" version = "v1.0.2" [[projects]] + digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" name = "github.com/pkg/errors" packages = ["."] + pruneopts = "UT" revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" [[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" name = "github.com/pmezard/go-difflib" packages = ["difflib"] + pruneopts = "UT" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] + digest = "1:c968b29db5d68ec97de404b6d058d5937fa015a141b3b4f7a0d87d5f8226f04c" name = "github.com/prometheus/client_golang" packages = [ "prometheus", - "prometheus/promhttp" + "prometheus/promhttp", ] + pruneopts = "UT" revision = "967789050ba94deca04a5e84cce8ad472ce313c1" version = "v0.9.0-pre1" [[projects]] branch = "master" + digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b" name = "github.com/prometheus/client_model" packages = ["go"] + pruneopts = "UT" revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" [[projects]] branch = "master" + digest = "1:e469cd65badf7694aeb44874518606d93c1d59e7735d3754ad442782437d3cc3" name = "github.com/prometheus/common" packages = [ "expfmt", "internal/bitbucket.org/ww/goautoneg", - "model" + "model", ] + pruneopts = "UT" revision = "7600349dcfe1abd18d72d3a1770870d9800a7801" [[projects]] branch = "master" + digest = "1:20d9bb50dbee172242f9bcd6ec24a917dd7a5bb17421bf16a79c33111dea7db1" name = "github.com/prometheus/procfs" packages = [ ".", "internal/util", "nfs", - "xfs" + "xfs", ] + pruneopts = "UT" revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a" [[projects]] + digest = "1:9a6f766efd8d5752adb7052aebb6e3d85255b31a8dff5e58ab4efa740ba9efa0" name = "github.com/rifflock/lfshook" packages = ["."] + pruneopts = "UT" revision = "bf539943797a1f34c1f502d07de419b5238ae6c6" version = "v2.3" [[projects]] + digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2" name = "github.com/sirupsen/logrus" packages = ["."] + pruneopts = "UT" revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" version = "v1.0.5" [[projects]] + digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83" name = "github.com/stretchr/testify" packages = ["assert"] + pruneopts = "UT" revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" version = "v1.2.2" [[projects]] branch = "master" + digest = "1:1182d12da264c8fe593504f150c3a479297767991fc1c06796cb67e4f61c4366" name = "golang.org/x/crypto" packages = [ "curve25519", @@ -318,12 +405,14 @@ "nacl/secretbox", "poly1305", "salsa20/salsa", - "ssh/terminal" + "ssh/terminal", ] + pruneopts = "UT" revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602" [[projects]] branch = "master" + digest = "1:011ee76ffb757c7c91a3e61cf5d6f367d1fd5dd08e16a4e6ae688e7759f8509a" name = "golang.org/x/net" packages = [ "bpf", @@ -338,18 +427,22 @@ "ipv4", "ipv6", "trace", - "websocket" + "websocket", ] + pruneopts = "UT" revision = "32a936f46389aa10549d60bd7833e54b01685d09" [[projects]] branch = "master" + digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239" name = "golang.org/x/sync" packages = ["errgroup"] + pruneopts = "UT" revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" [[projects]] branch = "master" + digest = "1:2e8e74aa73847379a370c2704ff08baf4a7303ae682766e70c23ded3fd9b3f37" name = "golang.org/x/sys" packages = [ "unix", @@ -357,11 +450,13 @@ "windows/registry", "windows/svc", "windows/svc/eventlog", - "windows/svc/mgr" + "windows/svc/mgr", ] + pruneopts = "UT" revision = "ce36f3865eeb42541ce3f87f32f8462c5687befa" [[projects]] + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" name = "golang.org/x/text" packages = [ "collate", @@ -377,18 +472,22 @@ "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable" + "unicode/rangetable", ] + pruneopts = "UT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] branch = "master" + digest = "1:601e63e7d4577f907118bec825902505291918859d223bce015539e79f1160e3" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] + pruneopts = "UT" revision = "ff3583edef7de132f219f0efc00e097cabcc0ec0" [[projects]] + digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74" name = "google.golang.org/grpc" packages = [ ".", @@ -415,28 +514,34 @@ "stats", "status", "tap", - "transport" + "transport", ] + pruneopts = "UT" revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8" version = "v1.13.0" [[projects]] branch = "altsrc-parse-durations" + digest = "1:0370b1bceda03dbfade3abbde639a43f1113bab711ec760452e5c0dcc0c14787" name = "gopkg.in/urfave/cli.v2" packages = [ ".", - "altsrc" + "altsrc", ] + pruneopts = "UT" revision = "d604b6ffeee878fbf084fd2761466b6649989cee" source = "https://github.com/cbranch/cli" [[projects]] + digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "UT" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" version = "v2.2.1" [[projects]] + digest = "1:8ffc3ddc31414c0a71220957bb723b16510d7fcb5b3880dc0da4cf6d39c31642" name = "zombiezen.com/go/capnproto2" packages = [ ".", @@ -452,8 +557,9 @@ "rpc/internal/refcount", "schemas", "server", - "std/capnp/rpc" + "std/capnp/rpc", ] + pruneopts = "UT" revision = "7cfd211c19c7f5783c695f3654efa46f0df259c3" source = "https://github.com/zombiezen/go-capnproto2" version = "v2.17.1" @@ -461,6 +567,59 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "ee681bef3527e49801c841e313f98b40116eafe8b60be21273956eeb96487486" + input-imports = [ + "github.com/cloudflare/brotli-go", + "github.com/cloudflare/golibs/lrucache", + "github.com/coredns/coredns/core/dnsserver", + "github.com/coredns/coredns/plugin", + "github.com/coredns/coredns/plugin/cache", + "github.com/coredns/coredns/plugin/metrics/vars", + "github.com/coredns/coredns/plugin/pkg/dnstest", + "github.com/coredns/coredns/plugin/pkg/rcode", + "github.com/coredns/coredns/request", + "github.com/coreos/go-oidc/jose", + "github.com/coreos/go-oidc/oidc", + "github.com/coreos/go-systemd/daemon", + "github.com/elgs/gosqljson", + "github.com/equinox-io/equinox", + "github.com/facebookgo/grace/gracenet", + "github.com/getsentry/raven-go", + "github.com/golang-collections/collections/queue", + "github.com/google/uuid", + "github.com/gorilla/mux", + "github.com/gorilla/websocket", + "github.com/lib/pq", + "github.com/mattn/go-colorable", + "github.com/miekg/dns", + "github.com/mitchellh/go-homedir", + "github.com/pkg/errors", + "github.com/prometheus/client_golang/prometheus", + "github.com/prometheus/client_golang/prometheus/promhttp", + "github.com/rifflock/lfshook", + "github.com/sirupsen/logrus", + "github.com/stretchr/testify/assert", + "golang.org/x/crypto/nacl/box", + "golang.org/x/crypto/ssh/terminal", + "golang.org/x/net/context", + "golang.org/x/net/http2", + "golang.org/x/net/http2/hpack", + "golang.org/x/net/idna", + "golang.org/x/net/trace", + "golang.org/x/net/websocket", + "golang.org/x/sync/errgroup", + "golang.org/x/sys/windows", + "golang.org/x/sys/windows/svc", + "golang.org/x/sys/windows/svc/eventlog", + "golang.org/x/sys/windows/svc/mgr", + "gopkg.in/urfave/cli.v2", + "gopkg.in/urfave/cli.v2/altsrc", + "zombiezen.com/go/capnproto2", + "zombiezen.com/go/capnproto2/encoding/text", + "zombiezen.com/go/capnproto2/pogs", + "zombiezen.com/go/capnproto2/rpc", + "zombiezen.com/go/capnproto2/schemas", + "zombiezen.com/go/capnproto2/server", + "zombiezen.com/go/capnproto2/std/capnp/rpc", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 8f88597b..f5352d02 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -73,3 +73,7 @@ [[constraint]] branch = "master" name = "golang.org/x/crypto" + +[[constraint]] + branch = "master" + name = "github.com/cloudflare/golibs" diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 6647a73f..fca4993f 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -230,6 +230,7 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, version st RunFromTerminal: isRunningFromTerminal(), NoChunkedEncoding: c.Bool("no-chunked-encoding"), CompressionQuality: c.Uint64("compression-quality"), + IncidentLookup: origin.NewIncidentLookup(), }, nil } diff --git a/metrics/metrics.go b/metrics/metrics.go index 4b6156ab..7c4170c0 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -1,6 +1,7 @@ package metrics import ( + "context" "net" "net/http" _ "net/http/pprof" @@ -8,7 +9,6 @@ import ( "sync" "time" - "golang.org/x/net/context" "golang.org/x/net/trace" "github.com/prometheus/client_golang/prometheus" diff --git a/origin/backoffhandler.go b/origin/backoffhandler.go index c4b3d80e..97bb9ad8 100644 --- a/origin/backoffhandler.go +++ b/origin/backoffhandler.go @@ -1,9 +1,8 @@ package origin import ( + "context" "time" - - "golang.org/x/net/context" ) // Redeclare time functions so they can be overridden in tests. diff --git a/origin/backoffhandler_test.go b/origin/backoffhandler_test.go index c9968044..6c8e4e9c 100644 --- a/origin/backoffhandler_test.go +++ b/origin/backoffhandler_test.go @@ -1,10 +1,9 @@ package origin import ( + "context" "testing" "time" - - "golang.org/x/net/context" ) func immediateTimeAfter(time.Duration) <-chan time.Time { diff --git a/origin/cloudflare_status_page.go b/origin/cloudflare_status_page.go new file mode 100644 index 00000000..dfa9143a --- /dev/null +++ b/origin/cloudflare_status_page.go @@ -0,0 +1,117 @@ +package origin + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/cloudflare/golibs/lrucache" +) + +// StatusPage.io API docs: +// https://www.cloudflarestatus.com/api/v2/#incidents-unresolved +const ( + activeIncidentsURL = "https://yh6f0r4529hb.statuspage.io/api/v2/incidents/unresolved.json" + argoTunnelKeyword = "argo tunnel" + incidentDetailsPrefix = "https://www.cloudflarestatus.com/incidents/" +) + +// IncidentLookup is an object that checks for active incidents in +// the Cloudflare infrastructure. +type IncidentLookup interface { + ActiveIncidents() []Incident +} + +// NewIncidentLookup returns a new IncidentLookup instance that caches its +// results with a 1-minute TTL. +func NewIncidentLookup() IncidentLookup { + return newCachedIncidentLookup(fetchActiveIncidents) +} + +type IncidentUpdate struct { + Body string +} + +type Incident struct { + Name string + ID string `json:"id"` + Updates []IncidentUpdate `json:"incident_updates"` +} + +type StatusPage struct { + Incidents []Incident +} + +func (i Incident) URL() string { + return incidentDetailsPrefix + i.ID +} + +func parseStatusPage(data []byte) (*StatusPage, error) { + var result StatusPage + err := json.Unmarshal(data, &result) + return &result, err +} + +func isArgoTunnelIncident(i Incident) bool { + if strings.Contains(strings.ToLower(i.Name), argoTunnelKeyword) { + return true + } + for _, u := range i.Updates { + if strings.Contains(strings.ToLower(u.Body), argoTunnelKeyword) { + return true + } + } + return false +} + +func fetchActiveIncidents() (incidents []Incident) { + resp, err := http.Get(activeIncidentsURL) + if err != nil { + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + statusPage, err := parseStatusPage(body) + if err != nil { + return + } + for _, i := range statusPage.Incidents { + if isArgoTunnelIncident(i) { + incidents = append(incidents, i) + } + } + return incidents +} + +type cachedIncidentLookup struct { + cache *lrucache.LRUCache + ttl time.Duration + uncachedLookup func() []Incident +} + +func newCachedIncidentLookup(uncachedLookup func() []Incident) *cachedIncidentLookup { + return &cachedIncidentLookup{ + cache: lrucache.NewLRUCache(1), + ttl: time.Minute, + uncachedLookup: uncachedLookup, + } +} + +// We only need one cache entry. Always use the empty string as its key. +const cacheKey = "" + +func (c *cachedIncidentLookup) ActiveIncidents() []Incident { + if cached, ok := c.cache.GetNotStale(cacheKey); ok { + if incidents, ok := cached.([]Incident); ok { + return incidents + } + } + incidents := c.uncachedLookup() + c.cache.Set(cacheKey, incidents, time.Now().Add(c.ttl)) + return incidents +} diff --git a/origin/cloudflare_status_page_test.go b/origin/cloudflare_status_page_test.go new file mode 100644 index 00000000..9ea38158 --- /dev/null +++ b/origin/cloudflare_status_page_test.go @@ -0,0 +1,386 @@ +package origin + +import ( + "testing" + "time" + + "github.com/cloudflare/golibs/lrucache" + "github.com/stretchr/testify/assert" +) + +func TestParseStatusPage(t *testing.T) { + testCases := []struct { + input []byte + output *StatusPage + fail bool + }{ + { + input: []byte(` + 504 Gateway Time-out +

504 Gateway Time-out

+ `), + output: nil, + fail: true, + }, + { + input: []byte(`{ + "page": { + "id": "yh6f0r4529hb", + "name": "Cloudflare", + "url": "https://www.cloudflarestatus.com", + "time_zone": "Etc/UTC", + "updated_at": "2019-01-10T20:11:38.750Z" + }, + "incidents": [ + { + "name": "Cloudflare API service issues", + "status": "resolved", + "created_at": "2018-09-17T19:29:21.132Z", + "updated_at": "2018-09-18T07:45:41.313Z", + "monitoring_at": "2018-09-17T21:35:06.492Z", + "resolved_at": "2018-09-18T07:45:41.290Z", + "shortlink": "http://stspg.io/7f079791e", + "id": "q746ybtyb6q0", + "page_id": "yh6f0r4529hb", + "incident_updates": [ + { + "status": "resolved", + "body": "Cloudflare has resolved the issue and the service have resumed normal operation.", + "created_at": "2018-09-18T07:45:41.290Z", + "updated_at": "2018-09-18T07:45:41.290Z", + "display_at": "2018-09-18T07:45:41.290Z", + "affected_components": [ + { + "code": "g4tb35rs9yw7", + "name": "Cloudflare customer dashboard and APIs - Cloudflare APIs", + "old_status": "operational", + "new_status": "operational" + } + ], + "deliver_notifications": true, + "tweet_id": null, + "id": "zl5g2pl5zhfs", + "incident_id": "q746ybtyb6q0", + "custom_tweet": null + }, + { + "status": "monitoring", + "body": "Cloudflare has implemented a fix for this issue and is currently monitoring the results.\r\n\r\nWe will update the status once the issue is resolved.", + "created_at": "2018-09-17T21:35:06.492Z", + "updated_at": "2018-09-17T21:35:06.492Z", + "display_at": "2018-09-17T21:35:06.492Z", + "affected_components": [ + { + "code": "g4tb35rs9yw7", + "name": "Cloudflare customer dashboard and APIs - Cloudflare APIs", + "old_status": "degraded_performance", + "new_status": "operational" + } + ], + "deliver_notifications": false, + "tweet_id": null, + "id": "0001sv3chdnx", + "incident_id": "q746ybtyb6q0", + "custom_tweet": null + }, + { + "status": "investigating", + "body": "We are continuing to investigate this issue.", + "created_at": "2018-09-17T19:30:08.049Z", + "updated_at": "2018-09-17T19:30:08.049Z", + "display_at": "2018-09-17T19:30:08.049Z", + "affected_components": [ + { + "code": "g4tb35rs9yw7", + "name": "Cloudflare customer dashboard and APIs - Cloudflare APIs", + "old_status": "operational", + "new_status": "degraded_performance" + } + ], + "deliver_notifications": false, + "tweet_id": null, + "id": "qdr164tfpq7m", + "incident_id": "q746ybtyb6q0", + "custom_tweet": null + }, + { + "status": "investigating", + "body": "Cloudflare is investigating issues with APIs and Page Rule delays for Page Rule updates. Cloudflare Page Rule service delivery is unaffected and is operating normally. Also, these issues do not affect the Cloudflare CDN and therefore, do not impact customer websites.", + "created_at": "2018-09-17T19:29:21.201Z", + "updated_at": "2018-09-17T19:29:21.201Z", + "display_at": "2018-09-17T19:29:21.201Z", + "affected_components": [ + { + "code": "g4tb35rs9yw7", + "name": "Cloudflare customer dashboard and APIs - Cloudflare APIs", + "old_status": "operational", + "new_status": "operational" + } + ], + "deliver_notifications": false, + "tweet_id": null, + "id": "qzl2n0q8tskg", + "incident_id": "q746ybtyb6q0", + "custom_tweet": null + } + ], + "components": [ + { + "status": "operational", + "name": "Cloudflare APIs", + "created_at": "2014-10-09T03:32:07.158Z", + "updated_at": "2019-01-01T22:58:30.846Z", + "position": 2, + "description": null, + "showcase": false, + "id": "g4tb35rs9yw7", + "page_id": "yh6f0r4529hb", + "group_id": "1km35smx8p41", + "group": false, + "only_show_if_degraded": false, + "automation_email": "component+g4tb35rs9yw7@notifications.statuspage.io" + } + ], + "impact": "minor" + }, + { + "name": "Web Analytics Delays", + "status": "resolved", + "created_at": "2018-09-17T18:05:39.907Z", + "updated_at": "2018-09-17T22:53:05.078Z", + "monitoring_at": null, + "resolved_at": "2018-09-17T22:53:05.057Z", + "shortlink": "http://stspg.io/cb208928c", + "id": "wqfk9mzs5qt1", + "page_id": "yh6f0r4529hb", + "incident_updates": [ + { + "status": "resolved", + "body": "Cloudflare has resolved the issue and Web Analytics have resumed normal operation.", + "created_at": "2018-09-17T22:53:05.057Z", + "updated_at": "2018-09-17T22:53:05.057Z", + "display_at": "2018-09-17T22:53:05.057Z", + "affected_components": [ + { + "code": "4c231tkdlpcl", + "name": "Cloudflare customer dashboard and APIs - Analytics", + "old_status": "degraded_performance", + "new_status": "operational" + } + ], + "deliver_notifications": false, + "tweet_id": null, + "id": "93y1w00yqzk4", + "incident_id": "wqfk9mzs5qt1", + "custom_tweet": null + }, + { + "status": "investigating", + "body": "There is a delay in processing Cloudflare Web Analytics. This affects timely delivery of customer data.\n\nThese delays do not impact analytics for DNS and Rate Limiting.", + "created_at": "2018-09-17T18:05:40.033Z", + "updated_at": "2018-09-17T18:05:40.033Z", + "display_at": "2018-09-17T18:05:40.033Z", + "affected_components": [ + { + "code": "4c231tkdlpcl", + "name": "Cloudflare customer dashboard and APIs - Analytics", + "old_status": "operational", + "new_status": "degraded_performance" + } + ], + "deliver_notifications": false, + "tweet_id": null, + "id": "362t6lv0vrpk", + "incident_id": "wqfk9mzs5qt1", + "custom_tweet": null + } + ], + "components": [ + { + "status": "operational", + "name": "Analytics", + "created_at": "2014-11-13T11:54:10.191Z", + "updated_at": "2018-12-31T08:20:52.349Z", + "position": 3, + "description": "Customer data", + "showcase": false, + "id": "4c231tkdlpcl", + "page_id": "yh6f0r4529hb", + "group_id": "1km35smx8p41", + "group": false, + "only_show_if_degraded": false, + "automation_email": "component+4c231tkdlpcl@notifications.statuspage.io" + } + ], + "impact": "minor" + } + ] + }`), + output: &StatusPage{ + Incidents: []Incident{ + Incident{ + Name: "Cloudflare API service issues", + ID: "q746ybtyb6q0", + Updates: []IncidentUpdate{ + IncidentUpdate{ + Body: "Cloudflare has resolved the issue and the service have resumed normal operation.", + }, + IncidentUpdate{ + Body: "Cloudflare has implemented a fix for this issue and is currently monitoring the results.\r\n\r\nWe will update the status once the issue is resolved.", + }, + IncidentUpdate{ + Body: "We are continuing to investigate this issue.", + }, + IncidentUpdate{ + Body: "Cloudflare is investigating issues with APIs and Page Rule delays for Page Rule updates. Cloudflare Page Rule service delivery is unaffected and is operating normally. Also, these issues do not affect the Cloudflare CDN and therefore, do not impact customer websites.", + }, + }, + }, + Incident{ + Name: "Web Analytics Delays", + ID: "wqfk9mzs5qt1", + Updates: []IncidentUpdate{ + IncidentUpdate{ + Body: "Cloudflare has resolved the issue and Web Analytics have resumed normal operation.", + }, + IncidentUpdate{ + Body: "There is a delay in processing Cloudflare Web Analytics. This affects timely delivery of customer data.\n\nThese delays do not impact analytics for DNS and Rate Limiting.", + }, + }, + }, + }, + }, + fail: false, + }, + } + + for _, testCase := range testCases { + output, err := parseStatusPage(testCase.input) + if testCase.fail { + assert.Error(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, testCase.output, output) + } + } +} + +func TestIsArgoTunnelIncident(t *testing.T) { + testCases := []struct { + input Incident + output bool + }{ + { + input: Incident{}, + output: false, + }, + { + input: Incident{Name: "An Argo Tunnel incident"}, + output: true, + }, + { + input: Incident{Name: "an argo tunnel incident"}, + output: true, + }, + { + input: Incident{Name: "an aRgO TuNnEl incident"}, + output: true, + }, + { + input: Incident{Name: "an argotunnel incident"}, + output: false, + }, + { + input: Incident{Name: "irrelevant"}, + output: false, + }, + { + input: Incident{ + Name: "irrelevant", + Updates: []IncidentUpdate{ + IncidentUpdate{Body: "irrelevant"}, + IncidentUpdate{Body: "an Argo Tunnel incident"}, + IncidentUpdate{Body: "irrelevant"}, + }, + }, + output: true, + }, + { + input: Incident{ + Name: "an Argo Tunnel incident", + Updates: []IncidentUpdate{ + IncidentUpdate{Body: "irrelevant"}, + IncidentUpdate{Body: "irrelevant"}, + IncidentUpdate{Body: "irrelevant"}, + }, + }, + output: true, + }, + } + for _, testCase := range testCases { + actual := isArgoTunnelIncident(testCase.input) + assert.Equal(t, testCase.output, actual, "Test case failed: %v", testCase.input) + } +} + +func TestIncidentURL(t *testing.T) { + incident := Incident{ + ID: "s6k0dnn5347b", + } + assert.Equal(t, "https://www.cloudflarestatus.com/incidents/s6k0dnn5347b", incident.URL()) +} + +func TestNewCachedIncidentLookup(t *testing.T) { + c := newCachedIncidentLookup(func() []Incident { return nil }) + assert.Equal(t, time.Minute, c.ttl) + assert.Equal(t, 1, c.cache.Capacity()) +} + +func TestCachedIncidentLookup(t *testing.T) { + expected := []Incident{ + Incident{ + Name: "An incident", + ID: "incidentID", + }, + } + + var shouldCallUncachedLookup bool + c := &cachedIncidentLookup{ + cache: lrucache.NewLRUCache(1), + ttl: 50 * time.Millisecond, + uncachedLookup: func() []Incident { + if !shouldCallUncachedLookup { + t.Fatal("uncachedLookup shouldn't have been called") + } + return expected + }, + } + + shouldCallUncachedLookup = true + assert.Equal(t, expected, c.ActiveIncidents()) + + shouldCallUncachedLookup = false + assert.Equal(t, expected, c.ActiveIncidents()) + assert.Equal(t, expected, c.ActiveIncidents()) + + time.Sleep(50 * time.Millisecond) + shouldCallUncachedLookup = true + assert.Equal(t, expected, c.ActiveIncidents()) +} + +func TestCachedIncidentLookupDoesntPanic(t *testing.T) { + expected := []Incident{ + Incident{ + Name: "An incident", + ID: "incidentID", + }, + } + c := &cachedIncidentLookup{ + cache: lrucache.NewLRUCache(1), + ttl: 50 * time.Millisecond, + uncachedLookup: func() []Incident { return expected }, + } + c.cache.Set(cacheKey, 42, time.Now().Add(30*time.Minute)) + actual := c.ActiveIncidents() + assert.Equal(t, expected, actual) +} diff --git a/origin/supervisor.go b/origin/supervisor.go index 297d5153..6da0e901 100644 --- a/origin/supervisor.go +++ b/origin/supervisor.go @@ -1,11 +1,10 @@ package origin import ( + "context" "fmt" "net" "time" - - "golang.org/x/net/context" ) const ( diff --git a/origin/tunnel.go b/origin/tunnel.go index 28f411bf..184be561 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -2,9 +2,9 @@ package origin import ( "bufio" + "context" "crypto/tls" "fmt" - "github.com/google/uuid" "io" "net" "net/http" @@ -13,7 +13,6 @@ import ( "strings" "time" - "golang.org/x/net/context" "golang.org/x/sync/errgroup" "github.com/cloudflare/cloudflared/h2mux" @@ -23,6 +22,7 @@ import ( "github.com/cloudflare/cloudflared/websocket" raven "github.com/getsentry/raven-go" + "github.com/google/uuid" "github.com/pkg/errors" _ "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" @@ -63,6 +63,7 @@ type TunnelConfig struct { NoChunkedEncoding bool WSGI bool CompressionQuality uint64 + IncidentLookup IncidentLookup } type dialError struct { @@ -265,6 +266,9 @@ func ServeTunnel( logger.WithError(castedErr.cause).Error("Register tunnel error from server side") // Don't send registration error return from server to Sentry. They are // logged on server side + if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 { + logger.Error(activeIncidentsMsg(incidents)) + } return castedErr.cause, !castedErr.permanent case clientRegisterTunnelError: logger.WithError(castedErr.cause).Error("Register tunnel error on client side") @@ -696,3 +700,17 @@ func trialZoneMsg(url string) []string { " " + url, } } + +func activeIncidentsMsg(incidents []Incident) string { + preamble := "There is an active Cloudflare incident that may be related:" + if len(incidents) > 1 { + preamble = "There are active Cloudflare incidents that may be related:" + } + incidentStrings := []string{} + for _, incident := range incidents { + incidentString := fmt.Sprintf("%s (%s)", incident.Name, incident.URL()) + incidentStrings = append(incidentStrings, incidentString) + } + return preamble + " " + strings.Join(incidentStrings, "; ") + +} diff --git a/tunneldns/https_proxy.go b/tunneldns/https_proxy.go index 3f6140b8..c361084c 100644 --- a/tunneldns/https_proxy.go +++ b/tunneldns/https_proxy.go @@ -1,10 +1,11 @@ package tunneldns import ( + "context" + "github.com/coredns/coredns/plugin" "github.com/miekg/dns" "github.com/pkg/errors" - "golang.org/x/net/context" ) // Upstream is a simplified interface for proxy destination diff --git a/tunneldns/https_upstream.go b/tunneldns/https_upstream.go index 03d7520c..27e3d09b 100644 --- a/tunneldns/https_upstream.go +++ b/tunneldns/https_upstream.go @@ -2,6 +2,7 @@ package tunneldns import ( "bytes" + "context" "crypto/tls" "fmt" "io/ioutil" @@ -12,7 +13,6 @@ import ( "github.com/miekg/dns" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "golang.org/x/net/context" "golang.org/x/net/http2" ) diff --git a/tunneldns/metrics.go b/tunneldns/metrics.go index 81ea7bce..33f9eeae 100644 --- a/tunneldns/metrics.go +++ b/tunneldns/metrics.go @@ -1,6 +1,8 @@ package tunneldns import ( + "context" + "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/metrics/vars" "github.com/coredns/coredns/plugin/pkg/dnstest" @@ -8,7 +10,6 @@ import ( "github.com/coredns/coredns/request" "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" - "golang.org/x/net/context" ) // MetricsPlugin is an adapter for CoreDNS and built-in metrics diff --git a/tunnelrpc/log.go b/tunnelrpc/log.go index d69f167a..848012b8 100644 --- a/tunnelrpc/log.go +++ b/tunnelrpc/log.go @@ -1,8 +1,9 @@ package tunnelrpc import ( + "context" + log "github.com/sirupsen/logrus" - "golang.org/x/net/context" "golang.org/x/net/trace" "zombiezen.com/go/capnproto2/rpc" ) diff --git a/tunnelrpc/logtransport.go b/tunnelrpc/logtransport.go index 6893ba03..80fdc6a1 100644 --- a/tunnelrpc/logtransport.go +++ b/tunnelrpc/logtransport.go @@ -3,9 +3,9 @@ package tunnelrpc import ( "bytes" + "context" log "github.com/sirupsen/logrus" - "golang.org/x/net/context" "zombiezen.com/go/capnproto2/encoding/text" "zombiezen.com/go/capnproto2/rpc" rpccapnp "zombiezen.com/go/capnproto2/std/capnp/rpc" diff --git a/vendor/github.com/cloudflare/golibs/LICENSE-BSD-CloudFlare b/vendor/github.com/cloudflare/golibs/LICENSE-BSD-CloudFlare new file mode 100644 index 00000000..20d90888 --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/LICENSE-BSD-CloudFlare @@ -0,0 +1,27 @@ +Copyright (c) 2013 CloudFlare, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the CloudFlare, Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/cloudflare/golibs/lrucache/.gitignore b/vendor/github.com/cloudflare/golibs/lrucache/.gitignore new file mode 100644 index 00000000..e10fb619 --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/.gitignore @@ -0,0 +1,3 @@ +cover.out~ +benchmark/benchmark + diff --git a/vendor/github.com/cloudflare/golibs/lrucache/Makefile b/vendor/github.com/cloudflare/golibs/lrucache/Makefile new file mode 100644 index 00000000..132f8094 --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/Makefile @@ -0,0 +1,41 @@ +# Copyright (c) 2013 CloudFlare, Inc. + +RACE+=--race + +PKGNAME=github.com/cloudflare/golibs/lrucache +SKIPCOVER=list.go|list_extension.go|priorityqueue.go + +.PHONY: all test bench cover clean + +all: + @echo "Targets:" + @echo " test: run tests with race detector" + @echo " cover: print test coverage" + @echo " bench: run basic benchmarks" + +test: + @go test $(RACE) -bench=. -v $(PKGNAME) + +COVEROUT=cover.out +cover: + @go test -coverprofile=$(COVEROUT) -v $(PKGNAME) + @cat $(COVEROUT) | egrep -v '$(SKIPCOVER)' > $(COVEROUT)~ + @go tool cover -func=$(COVEROUT)~|sed 's|^.*/\([^/]*/[^/]*/[^/]*\)$$|\1|g' + +bench: + @echo "[*] Scalability of cache/lrucache" + @echo "[ ] Operations in shared cache using one core" + @GOMAXPROCS=1 go test -run=- -bench='.*LRUCache.*' $(PKGNAME) \ + | egrep -v "^PASS|^ok" + + @echo "[*] Scalability of cache/multilru" + @echo "[ ] Operations in four caches using four cores " + @GOMAXPROCS=4 go test -run=- -bench='.*MultiLRU.*' $(PKGNAME) \ + | egrep -v "^PASS|^ok" + + + @(cd benchmark; go build $(PKGNAME)/benchmark) + @./benchmark/benchmark + +clean: + rm -rf $(COVEROUT) $(COVEROUT)~ benchmark/benchmark diff --git a/vendor/github.com/cloudflare/golibs/lrucache/README.md b/vendor/github.com/cloudflare/golibs/lrucache/README.md new file mode 100644 index 00000000..7e7a5c7a --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/README.md @@ -0,0 +1,40 @@ +LRU Cache +--------- + +A `golang` implementation of last recently used cache data structure. + +To install: + + go get github.com/cloudflare/golibs/lrucache + +To test: + + cd $GOPATH/src/github.com/cloudflare/golibs/lrucache + make test + +For coverage: + + make cover + +Basic benchmarks: + + $ make bench # As tested on my two core i5 + [*] Scalability of cache/lrucache + [ ] Operations in shared cache using one core + BenchmarkConcurrentGetLRUCache 5000000 450 ns/op + BenchmarkConcurrentSetLRUCache 2000000 821 ns/op + BenchmarkConcurrentSetNXLRUCache 5000000 664 ns/op + + [*] Scalability of cache/multilru + [ ] Operations in four caches using four cores + BenchmarkConcurrentGetMultiLRU-4 5000000 475 ns/op + BenchmarkConcurrentSetMultiLRU-4 2000000 809 ns/op + BenchmarkConcurrentSetNXMultiLRU-4 5000000 643 ns/op + + [*] Capacity=4096 Keys=30000 KeySpace=15625 + vitess LRUCache MultiLRUCache-4 + create 1.709us 1.626374ms 343.54us + Get (miss) 144.266083ms 132.470397ms 177.277193ms + SetNX #1 338.637977ms 380.733302ms 411.709204ms + Get (hit) 195.896066ms 173.252112ms 234.109494ms + SetNX #2 349.785951ms 367.255624ms 419.129127ms diff --git a/vendor/github.com/cloudflare/golibs/lrucache/cache.go b/vendor/github.com/cloudflare/golibs/lrucache/cache.go new file mode 100644 index 00000000..606574e5 --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/cache.go @@ -0,0 +1,69 @@ +// Copyright (c) 2013 CloudFlare, Inc. + +// Package lrucache implements a last recently used cache data structure. +// +// This code tries to avoid dynamic memory allocations - all required +// memory is allocated on creation. Access to the data structure is +// O(1). Modification O(log(n)) if expiry is used, O(1) +// otherwise. +// +// This package exports three things: +// LRUCache: is the main implementation. It supports multithreading by +// using guarding mutex lock. +// +// MultiLRUCache: is a sharded implementation. It supports the same +// API as LRUCache and uses it internally, but is not limited to +// a single CPU as every shard is separately locked. Use this +// data structure instead of LRUCache if you have have lock +// contention issues. +// +// Cache interface: Both implementations fulfill it. +package lrucache + +import ( + "time" +) + +// Cache interface is fulfilled by the LRUCache and MultiLRUCache +// implementations. +type Cache interface { + // Methods not needing to know current time. + // + // Get a key from the cache, possibly stale. Update its LRU + // score. + Get(key string) (value interface{}, ok bool) + // Get a key from the cache, possibly stale. Don't modify its LRU score. O(1) + GetQuiet(key string) (value interface{}, ok bool) + // Get and remove a key from the cache. + Del(key string) (value interface{}, ok bool) + // Evict all items from the cache. + Clear() int + // Number of entries used in the LRU + Len() int + // Get the total capacity of the LRU + Capacity() int + + // Methods use time.Now() when neccessary to determine expiry. + // + // Add an item to the cache overwriting existing one if it + // exists. + Set(key string, value interface{}, expire time.Time) + // Get a key from the cache, make sure it's not stale. Update + // its LRU score. + GetNotStale(key string) (value interface{}, ok bool) + // Evict all the expired items. + Expire() int + + // Methods allowing to explicitly specify time used to + // determine if items are expired. + // + // Add an item to the cache overwriting existing one if it + // exists. Allows specifing current time required to expire an + // item when no more slots are used. + SetNow(key string, value interface{}, expire time.Time, now time.Time) + // Get a key from the cache, make sure it's not stale. Update + // its LRU score. + GetNotStaleNow(key string, now time.Time) (value interface{}, ok bool) + // Evict items that expire before Now. + ExpireNow(now time.Time) int +} diff --git a/vendor/github.com/cloudflare/golibs/lrucache/list.go b/vendor/github.com/cloudflare/golibs/lrucache/list.go new file mode 100644 index 00000000..0c7bb85b --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/list.go @@ -0,0 +1,238 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* This file is a slightly modified file from the go package sources +and is released on the following license: + +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// Package list implements a doubly linked list. +// +// To iterate over a list (where l is a *List): +// for e := l.Front(); e != nil; e = e.Next() { +// // do something with e.Value +// } +// + +package lrucache + +// Element is an element of a linked list. +type element struct { + // Next and previous pointers in the doubly-linked list of elements. + // To simplify the implementation, internally a list l is implemented + // as a ring, such that &l.root is both the next element of the last + // list element (l.Back()) and the previous element of the first list + // element (l.Front()). + next, prev *element + + // The list to which this element belongs. + list *list + + // The value stored with this element. + Value interface{} +} + +// Next returns the next list element or nil. +func (e *element) Next() *element { + if p := e.next; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// Prev returns the previous list element or nil. +func (e *element) Prev() *element { + if p := e.prev; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// List represents a doubly linked list. +// The zero value for List is an empty list ready to use. +type list struct { + root element // sentinel list element, only &root, root.prev, and root.next are used + len int // current list length excluding (this) sentinel element +} + +// Init initializes or clears list l. +func (l *list) Init() *list { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + return l +} + +// New returns an initialized list. +// func New() *list { return new(list).Init() } + +// Len returns the number of elements of list l. +// The complexity is O(1). +func (l *list) Len() int { return l.len } + +// Front returns the first element of list l or nil +func (l *list) Front() *element { + if l.len == 0 { + return nil + } + return l.root.next +} + +// Back returns the last element of list l or nil. +func (l *list) Back() *element { + if l.len == 0 { + return nil + } + return l.root.prev +} + +// insert inserts e after at, increments l.len, and returns e. +func (l *list) insert(e, at *element) *element { + n := at.next + at.next = e + e.prev = at + e.next = n + n.prev = e + e.list = l + l.len++ + return e +} + +// insertValue is a convenience wrapper for insert(&Element{Value: v}, at). +func (l *list) insertValue(v interface{}, at *element) *element { + return l.insert(&element{Value: v}, at) +} + +// remove removes e from its list, decrements l.len, and returns e. +func (l *list) remove(e *element) *element { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil // avoid memory leaks + e.prev = nil // avoid memory leaks + e.list = nil + l.len-- + return e +} + +// Remove removes e from l if e is an element of list l. +// It returns the element value e.Value. +func (l *list) Remove(e *element) interface{} { + if e.list == l { + // if e.list == l, l must have been initialized when e was inserted + // in l or l == nil (e is a zero Element) and l.remove will crash + l.remove(e) + } + return e.Value +} + +// PushFront inserts a new element e with value v at the front of list l and returns e. +func (l *list) PushFront(v interface{}) *element { + return l.insertValue(v, &l.root) +} + +// PushBack inserts a new element e with value v at the back of list l and returns e. +func (l *list) PushBack(v interface{}) *element { + return l.insertValue(v, l.root.prev) +} + +// InsertBefore inserts a new element e with value v immediately before mark and returns e. +// If mark is not an element of l, the list is not modified. +func (l *list) InsertBefore(v interface{}, mark *element) *element { + if mark.list != l { + return nil + } + // see comment in List.Remove about initialization of l + return l.insertValue(v, mark.prev) +} + +// InsertAfter inserts a new element e with value v immediately after mark and returns e. +// If mark is not an element of l, the list is not modified. +func (l *list) InsertAfter(v interface{}, mark *element) *element { + if mark.list != l { + return nil + } + // see comment in List.Remove about initialization of l + return l.insertValue(v, mark) +} + +// MoveToFront moves element e to the front of list l. +// If e is not an element of l, the list is not modified. +func (l *list) MoveToFront(e *element) { + if e.list != l || l.root.next == e { + return + } + // see comment in List.Remove about initialization of l + l.insert(l.remove(e), &l.root) +} + +// MoveToBack moves element e to the back of list l. +// If e is not an element of l, the list is not modified. +func (l *list) MoveToBack(e *element) { + if e.list != l || l.root.prev == e { + return + } + // see comment in List.Remove about initialization of l + l.insert(l.remove(e), l.root.prev) +} + +// MoveBefore moves element e to its new position before mark. +// If e is not an element of l, or e == mark, the list is not modified. +func (l *list) MoveBefore(e, mark *element) { + if e.list != l || e == mark { + return + } + l.insert(l.remove(e), mark.prev) +} + +// MoveAfter moves element e to its new position after mark. +// If e is not an element of l, or e == mark, the list is not modified. +func (l *list) MoveAfter(e, mark *element) { + if e.list != l || e == mark { + return + } + l.insert(l.remove(e), mark) +} + +// PushBackList inserts a copy of an other list at the back of list l. +// The lists l and other may be the same. +func (l *list) PushBackList(other *list) { + for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() { + l.insertValue(e.Value, l.root.prev) + } +} + +// PushFrontList inserts a copy of an other list at the front of list l. +// The lists l and other may be the same. +func (l *list) PushFrontList(other *list) { + for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { + l.insertValue(e.Value, &l.root) + } +} diff --git a/vendor/github.com/cloudflare/golibs/lrucache/list_extension.go b/vendor/github.com/cloudflare/golibs/lrucache/list_extension.go new file mode 100644 index 00000000..baac809a --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/list_extension.go @@ -0,0 +1,25 @@ +// Copyright (c) 2013 CloudFlare, Inc. + +// Extensions to "container/list" that allowing reuse of Elements. + +package lrucache + +func (l *list) PushElementFront(e *element) *element { + return l.insert(e, &l.root) +} + +func (l *list) PushElementBack(e *element) *element { + return l.insert(e, l.root.prev) +} + +func (l *list) PopElementFront() *element { + el := l.Front() + l.Remove(el) + return el +} + +func (l *list) PopFront() interface{} { + el := l.Front() + l.Remove(el) + return el.Value +} diff --git a/vendor/github.com/cloudflare/golibs/lrucache/lrucache.go b/vendor/github.com/cloudflare/golibs/lrucache/lrucache.go new file mode 100644 index 00000000..86dcca20 --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/lrucache.go @@ -0,0 +1,316 @@ +// Copyright (c) 2013 CloudFlare, Inc. + +package lrucache + +import ( + "container/heap" + "sync" + "time" +) + +// Every element in the cache is linked to three data structures: +// Table map, PriorityQueue heap ordered by expiry and a LruList list +// ordered by decreasing popularity. +type entry struct { + element element // list element. value is a pointer to this entry + key string // key is a key! + value interface{} // + expire time.Time // time when the item is expired. it's okay to be stale. + index int // index for priority queue needs. -1 if entry is free +} + +// LRUCache data structure. Never dereference it or copy it by +// value. Always use it through a pointer. +type LRUCache struct { + lock sync.Mutex + table map[string]*entry // all entries in table must be in lruList + priorityQueue priorityQueue // some elements from table may be in priorityQueue + lruList list // every entry is either used and resides in lruList + freeList list // or free and is linked to freeList + + ExpireGracePeriod time.Duration // time after an expired entry is purged from cache (unless pushed out of LRU) +} + +// Initialize the LRU cache instance. O(capacity) +func (b *LRUCache) Init(capacity uint) { + b.table = make(map[string]*entry, capacity) + b.priorityQueue = make([]*entry, 0, capacity) + b.lruList.Init() + b.freeList.Init() + heap.Init(&b.priorityQueue) + + // Reserve all the entries in one giant continous block of memory + arrayOfEntries := make([]entry, capacity) + for i := uint(0); i < capacity; i++ { + e := &arrayOfEntries[i] + e.element.Value = e + e.index = -1 + b.freeList.PushElementBack(&e.element) + } +} + +// Create new LRU cache instance. Allocate all the needed memory. O(capacity) +func NewLRUCache(capacity uint) *LRUCache { + b := &LRUCache{} + b.Init(capacity) + return b +} + +// Give me the entry with lowest expiry field if it's before now. +func (b *LRUCache) expiredEntry(now time.Time) *entry { + if len(b.priorityQueue) == 0 { + return nil + } + + if now.IsZero() { + // Fill it only when actually used. + now = time.Now() + } + + if e := b.priorityQueue[0]; e.expire.Before(now) { + return e + } + return nil +} + +// Give me the least used entry. +func (b *LRUCache) leastUsedEntry() *entry { + return b.lruList.Back().Value.(*entry) +} + +func (b *LRUCache) freeSomeEntry(now time.Time) (e *entry, used bool) { + if b.freeList.Len() > 0 { + return b.freeList.Front().Value.(*entry), false + } + + e = b.expiredEntry(now) + if e != nil { + return e, true + } + + if b.lruList.Len() == 0 { + return nil, false + } + + return b.leastUsedEntry(), true +} + +// Move entry from used/lru list to a free list. Clear the entry as well. +func (b *LRUCache) removeEntry(e *entry) { + if e.element.list != &b.lruList { + panic("list lruList") + } + + if e.index != -1 { + heap.Remove(&b.priorityQueue, e.index) + } + b.lruList.Remove(&e.element) + b.freeList.PushElementFront(&e.element) + delete(b.table, e.key) + e.key = "" + e.value = nil +} + +func (b *LRUCache) insertEntry(e *entry) { + if e.element.list != &b.freeList { + panic("list freeList") + } + + if !e.expire.IsZero() { + heap.Push(&b.priorityQueue, e) + } + b.freeList.Remove(&e.element) + b.lruList.PushElementFront(&e.element) + b.table[e.key] = e +} + +func (b *LRUCache) touchEntry(e *entry) { + b.lruList.MoveToFront(&e.element) +} + +// Add an item to the cache overwriting existing one if it +// exists. Allows specifing current time required to expire an item +// when no more slots are used. O(log(n)) if expiry is set, O(1) when +// clear. +func (b *LRUCache) SetNow(key string, value interface{}, expire time.Time, now time.Time) { + b.lock.Lock() + defer b.lock.Unlock() + + var used bool + + e := b.table[key] + if e != nil { + used = true + } else { + e, used = b.freeSomeEntry(now) + if e == nil { + return + } + } + if used { + b.removeEntry(e) + } + + e.key = key + e.value = value + e.expire = expire + b.insertEntry(e) +} + +// Add an item to the cache overwriting existing one if it +// exists. O(log(n)) if expiry is set, O(1) when clear. +func (b *LRUCache) Set(key string, value interface{}, expire time.Time) { + b.SetNow(key, value, expire, time.Time{}) +} + +// Get a key from the cache, possibly stale. Update its LRU score. O(1) +func (b *LRUCache) Get(key string) (v interface{}, ok bool) { + b.lock.Lock() + defer b.lock.Unlock() + + e := b.table[key] + if e == nil { + return nil, false + } + + b.touchEntry(e) + return e.value, true +} + +// Get a key from the cache, possibly stale. Don't modify its LRU score. O(1) +func (b *LRUCache) GetQuiet(key string) (v interface{}, ok bool) { + b.lock.Lock() + defer b.lock.Unlock() + + e := b.table[key] + if e == nil { + return nil, false + } + + return e.value, true +} + +// Get a key from the cache, make sure it's not stale. Update its +// LRU score. O(log(n)) if the item is expired. +func (b *LRUCache) GetNotStale(key string) (value interface{}, ok bool) { + return b.GetNotStaleNow(key, time.Now()) +} + +// Get a key from the cache, make sure it's not stale. Update its +// LRU score. O(log(n)) if the item is expired. +func (b *LRUCache) GetNotStaleNow(key string, now time.Time) (value interface{}, ok bool) { + b.lock.Lock() + defer b.lock.Unlock() + + e := b.table[key] + if e == nil { + return nil, false + } + + if e.expire.Before(now) { + // Remove entries expired for more than a graceful period + if b.ExpireGracePeriod == 0 || e.expire.Sub(now) > b.ExpireGracePeriod { + b.removeEntry(e) + } + return nil, false + } + + b.touchEntry(e) + return e.value, true +} + +// Get a key from the cache, possibly stale. Update its LRU +// score. O(1) always. +func (b *LRUCache) GetStale(key string) (value interface{}, ok, expired bool) { + return b.GetStaleNow(key, time.Now()) +} + +// Get a key from the cache, possibly stale. Update its LRU +// score. O(1) always. +func (b *LRUCache) GetStaleNow(key string, now time.Time) (value interface{}, ok, expired bool) { + b.lock.Lock() + defer b.lock.Unlock() + + e := b.table[key] + if e == nil { + return nil, false, false + } + + b.touchEntry(e) + return e.value, true, e.expire.Before(now) +} + +// Get and remove a key from the cache. O(log(n)) if the item is using expiry, O(1) otherwise. +func (b *LRUCache) Del(key string) (v interface{}, ok bool) { + b.lock.Lock() + defer b.lock.Unlock() + + e := b.table[key] + if e == nil { + return nil, false + } + + value := e.value + b.removeEntry(e) + return value, true +} + +// Evict all items from the cache. O(n*log(n)) +func (b *LRUCache) Clear() int { + b.lock.Lock() + defer b.lock.Unlock() + + // First, remove entries that have expiry set + l := len(b.priorityQueue) + for i := 0; i < l; i++ { + // This could be reduced to O(n). + b.removeEntry(b.priorityQueue[0]) + } + + // Second, remove all remaining entries + r := b.lruList.Len() + for i := 0; i < r; i++ { + b.removeEntry(b.leastUsedEntry()) + } + return l + r +} + +// Evict all the expired items. O(n*log(n)) +func (b *LRUCache) Expire() int { + return b.ExpireNow(time.Now()) +} + +// Evict items that expire before `now`. O(n*log(n)) +func (b *LRUCache) ExpireNow(now time.Time) int { + b.lock.Lock() + defer b.lock.Unlock() + + i := 0 + for { + e := b.expiredEntry(now) + if e == nil { + break + } + b.removeEntry(e) + i += 1 + } + return i +} + +// Number of entries used in the LRU +func (b *LRUCache) Len() int { + // yes. this stupid thing requires locking + b.lock.Lock() + defer b.lock.Unlock() + + return b.lruList.Len() +} + +// Get the total capacity of the LRU +func (b *LRUCache) Capacity() int { + // yes. this stupid thing requires locking + b.lock.Lock() + defer b.lock.Unlock() + + return b.lruList.Len() + b.freeList.Len() +} diff --git a/vendor/github.com/cloudflare/golibs/lrucache/multilru.go b/vendor/github.com/cloudflare/golibs/lrucache/multilru.go new file mode 100644 index 00000000..fc8d7de0 --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/multilru.go @@ -0,0 +1,118 @@ +// Copyright (c) 2013 CloudFlare, Inc. + +package lrucache + +import ( + "hash/crc32" + "time" +) + +// MultiLRUCache data structure. Never dereference it or copy it by +// value. Always use it through a pointer. +type MultiLRUCache struct { + buckets uint + cache []*LRUCache +} + +// Using this constructor is almost always wrong. Use NewMultiLRUCache instead. +func (m *MultiLRUCache) Init(buckets, bucket_capacity uint) { + m.buckets = buckets + m.cache = make([]*LRUCache, buckets) + for i := uint(0); i < buckets; i++ { + m.cache[i] = NewLRUCache(bucket_capacity) + } +} + +// Set the stale expiry grace period for each cache in the multicache instance. +func (m *MultiLRUCache) SetExpireGracePeriod(p time.Duration) { + for _, c := range m.cache { + c.ExpireGracePeriod = p + } +} + +func NewMultiLRUCache(buckets, bucket_capacity uint) *MultiLRUCache { + m := &MultiLRUCache{} + m.Init(buckets, bucket_capacity) + return m +} + +func (m *MultiLRUCache) bucketNo(key string) uint { + // Arbitrary choice. Any fast hash will do. + return uint(crc32.ChecksumIEEE([]byte(key))) % m.buckets +} + +func (m *MultiLRUCache) Set(key string, value interface{}, expire time.Time) { + m.cache[m.bucketNo(key)].Set(key, value, expire) +} + +func (m *MultiLRUCache) SetNow(key string, value interface{}, expire time.Time, now time.Time) { + m.cache[m.bucketNo(key)].SetNow(key, value, expire, now) +} + +func (m *MultiLRUCache) Get(key string) (value interface{}, ok bool) { + return m.cache[m.bucketNo(key)].Get(key) +} + +func (m *MultiLRUCache) GetQuiet(key string) (value interface{}, ok bool) { + return m.cache[m.bucketNo(key)].Get(key) +} + +func (m *MultiLRUCache) GetNotStale(key string) (value interface{}, ok bool) { + return m.cache[m.bucketNo(key)].GetNotStale(key) +} + +func (m *MultiLRUCache) GetNotStaleNow(key string, now time.Time) (value interface{}, ok bool) { + return m.cache[m.bucketNo(key)].GetNotStaleNow(key, now) +} + +func (m *MultiLRUCache) GetStale(key string) (value interface{}, ok, expired bool) { + return m.cache[m.bucketNo(key)].GetStale(key) +} + +func (m *MultiLRUCache) GetStaleNow(key string, now time.Time) (value interface{}, ok, expired bool) { + return m.cache[m.bucketNo(key)].GetStaleNow(key, now) +} + +func (m *MultiLRUCache) Del(key string) (value interface{}, ok bool) { + return m.cache[m.bucketNo(key)].Del(key) +} + +func (m *MultiLRUCache) Clear() int { + var s int + for _, c := range m.cache { + s += c.Clear() + } + return s +} + +func (m *MultiLRUCache) Len() int { + var s int + for _, c := range m.cache { + s += c.Len() + } + return s +} + +func (m *MultiLRUCache) Capacity() int { + var s int + for _, c := range m.cache { + s += c.Capacity() + } + return s +} + +func (m *MultiLRUCache) Expire() int { + var s int + for _, c := range m.cache { + s += c.Expire() + } + return s +} + +func (m *MultiLRUCache) ExpireNow(now time.Time) int { + var s int + for _, c := range m.cache { + s += c.ExpireNow(now) + } + return s +} diff --git a/vendor/github.com/cloudflare/golibs/lrucache/priorityqueue.go b/vendor/github.com/cloudflare/golibs/lrucache/priorityqueue.go new file mode 100644 index 00000000..15e37167 --- /dev/null +++ b/vendor/github.com/cloudflare/golibs/lrucache/priorityqueue.go @@ -0,0 +1,37 @@ +// Copyright (c) 2013 CloudFlare, Inc. + +// This code is based on golang example from "container/heap" package. + +package lrucache + +type priorityQueue []*entry + +func (pq priorityQueue) Len() int { + return len(pq) +} + +func (pq priorityQueue) Less(i, j int) bool { + return pq[i].expire.Before(pq[j].expire) +} + +func (pq priorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] + pq[i].index = i + pq[j].index = j +} + +func (pq *priorityQueue) Push(e interface{}) { + n := len(*pq) + item := e.(*entry) + item.index = n + *pq = append(*pq, item) +} + +func (pq *priorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + item.index = -1 + *pq = old[0 : n-1] + return item +}