Skip to content

Commit

Permalink
Making Router pluggable.
Browse files Browse the repository at this point in the history
    I filed an ISSUE earlier to discuss this here: vulcand#218

    The crux of this change is:
    1. To make Router an interface behind which implementations can be
    changed out.

    2. To allow the Registry to specify an implementation.

    3. To fall back to the default implementation (route.Mux)
    when one is not provided by the registry.

    This change also depends on this pending PR in the mailgun/route library
    vulcand/route#10

    WHY?

    As we try to add new features to mailgun/route, we continue to have to work off custom branches
    and re-vendor the files, and rebuild vulcand for each experiment. For example we have
    a forked route library which contains massive amounts of logging.

    Having re-vendored files is always confusing because we don't know what branch/fork it came
    from. We'd much rather have a way to host 10 forks and simply plug in an implementation
    at runtime based on command-line flags.

    This allows us to rapidly innovate, and I'm sure gives vulcand users the ability to
    use richer or simpler languages.

    For instance, one experiment I want to run is use of Javascript-based expressions
    which would allow the use of VERY rich matching rules at the cost of
    VERY high compute load per rule-match. It's just an idea, but trying it out
    can be made easier by making it pluggable.

    As it happens this change doesn't affect how Vulcan works today, and the defaults
    do not change.

    Some example routing rules are:
        r.Referrer.indexOf("foo.com") > 0 //When referrer is foo.com
        r.Header.Get("Cookie")["foo"] == "bar" //Check value of cookie without using a complex regex

    TESTING:
       There are sufficient UTs here, and in addition we have written sufficient UTs
       in a couple of routing libraries we wrote as an experiment to see how far we could go.

       Polyverse Corporation (the company I work for) has been using the forked copy with these changes
       for over three weeks at this point. We're running a pluggable library in production across a large
       cluster reliably.

    HOW TO:

       After this change, this is how we inject a custom router into our own installation:

func GetRegistry(selfaddress string) (*plugin.Registry, error) {
	r := plugin.NewRegistry()

	specs := []*plugin.MiddlewareSpec{

		connlimit.GetSpec(),

		ratelimit.GetSpec(),

		rewrite.GetSpec(),

		cbreaker.GetSpec(),

		trace.GetSpec(),

		ttlresetmiddleware.GetSpec(),

		unroutedrequesthandler.GetSpec(),

		polyverseerrormiddleware.GetSpec(),

		connectionmanagementmiddleware.GetSpec(),
	}

	r.AddNotFoundMiddleware(notFoundMiddleware)

	//Use combination-router to enable javascript evaluation as a fallback mechanism
	//when mailgun/route is insufficient.

	routers := combineroute.Routers{
		combineroute.RouterEntry{
			Name:   "vulcanrouter",
			Router: route.NewMux(),
		},
		combineroute.RouterEntry{
			Name:   "javascriptrouter",
			Router: jsroute.NewMux(),
		},
	}
	commonrouter := combineroute.NewMux(routers)

	r.SetRouter(commonrouter)

	for _, spec := range specs {
		if err := r.AddSpec(spec); err != nil {
			return nil, err
		}
	}
	return r, nil
}

READY TO USE ROUTERS:
       Here are a couple of libraries that demonstrate the possibilities of allowing different
       language evaluators to be used for routing rules.

       https://github.com/polyverse-security/js-route
       https://github.com/polyverse-security/combine-route
  • Loading branch information
Archis Gore committed Oct 16, 2015
1 parent 791285c commit d598c38
Show file tree
Hide file tree
Showing 20 changed files with 149 additions and 41 deletions.
2 changes: 1 addition & 1 deletion Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Godeps/_workspace/src/github.com/mailgun/route/matcher.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions Godeps/_workspace/src/github.com/mailgun/route/matcher_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 22 additions & 5 deletions Godeps/_workspace/src/github.com/mailgun/route/mux.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/mailgun/vulcand/anomaly"
"github.com/mailgun/vulcand/engine"
"github.com/mailgun/vulcand/plugin"
"github.com/mailgun/vulcand/router"
)

type ProxyController struct {
Expand Down Expand Up @@ -271,7 +272,7 @@ func (c *ProxyController) getBackend(w http.ResponseWriter, r *http.Request, par
}

func (c *ProxyController) upsertFrontend(w http.ResponseWriter, r *http.Request, params map[string]string, body []byte) (interface{}, error) {
frontend, ttl, err := parseFrontendPack(body)
frontend, ttl, err := parseFrontendPack(c.ng.GetRegistry().GetRouter(), body)
if err != nil {
return nil, formatError(err)
}
Expand Down Expand Up @@ -471,15 +472,15 @@ func parseBackendPack(v []byte) (*engine.Backend, error) {
return engine.BackendFromJSON(bp.Backend)
}

func parseFrontendPack(v []byte) (*engine.Frontend, time.Duration, error) {
func parseFrontendPack(router router.Router, v []byte) (*engine.Frontend, time.Duration, error) {
var fp frontendReadPack
if err := json.Unmarshal(v, &fp); err != nil {
return nil, 0, err
}
if len(fp.Frontend) == 0 {
return nil, 0, &scroll.MissingFieldError{Field: "Frontend"}
}
f, err := engine.FrontendFromJSON(fp.Frontend)
f, err := engine.FrontendFromJSON(router, fp.Frontend)
if err != nil {
return nil, 0, err
}
Expand Down
4 changes: 2 additions & 2 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func (s *ApiSuite) TestFrontendCRUD(c *C) {

c.Assert(s.client.UpsertBackend(*b), IsNil)

f, err := engine.NewHTTPFrontend("f1", b.Id, `Path("/")`, engine.HTTPFrontendSettings{})
f, err := engine.NewHTTPFrontend(s.ng.GetRegistry().GetRouter(), "f1", b.Id, `Path("/")`, engine.HTTPFrontendSettings{})
c.Assert(err, IsNil)
fk := engine.FrontendKey{Id: f.Id}

Expand Down Expand Up @@ -260,7 +260,7 @@ func (s *ApiSuite) TestMiddlewareCRUD(c *C) {

c.Assert(s.client.UpsertBackend(*b), IsNil)

f, err := engine.NewHTTPFrontend("f1", b.Id, `Path("/")`, engine.HTTPFrontendSettings{})
f, err := engine.NewHTTPFrontend(s.ng.GetRegistry().GetRouter(), "f1", b.Id, `Path("/")`, engine.HTTPFrontendSettings{})
c.Assert(err, IsNil)
fk := engine.FrontendKey{Id: f.Id}

Expand Down
6 changes: 3 additions & 3 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ func (c *Client) GetFrontend(fk engine.FrontendKey) (*engine.Frontend, error) {
if err != nil {
return nil, err
}
return engine.FrontendFromJSON(response)
return engine.FrontendFromJSON(c.Registry.GetRouter(), response)
}

func (c *Client) GetFrontends() ([]engine.Frontend, error) {
data, err := c.Get(c.endpoint("frontends"), url.Values{})
if err != nil {
return nil, err
}
return engine.FrontendsFromJSON(data)
return engine.FrontendsFromJSON(c.Registry.GetRouter(), data)
}

func (c *Client) TopFrontends(bk *engine.BackendKey, limit int) ([]engine.Frontend, error) {
Expand All @@ -131,7 +131,7 @@ func (c *Client) TopFrontends(bk *engine.BackendKey, limit int) ([]engine.Fronte
if err != nil {
return nil, err
}
return engine.FrontendsFromJSON(response)
return engine.FrontendsFromJSON(c.Registry.GetRouter(), response)
}

func (c *Client) DeleteFrontend(fk engine.FrontendKey) error {
Expand Down
2 changes: 1 addition & 1 deletion engine/etcdng/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func (n *ng) GetFrontend(key engine.FrontendKey) (*engine.Frontend, error) {
if err != nil {
return nil, err
}
return engine.FrontendFromJSON([]byte(bytes), key.Id)
return engine.FrontendFromJSON(n.registry.GetRouter(), []byte(bytes), key.Id)
}

func (n *ng) DeleteFrontend(fk engine.FrontendKey) error {
Expand Down
9 changes: 5 additions & 4 deletions engine/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/mailgun/vulcand/plugin"
"github.com/mailgun/vulcand/router"
)

type rawServers struct {
Expand Down Expand Up @@ -73,15 +74,15 @@ func HostsFromJSON(in []byte) ([]Host, error) {
return out, nil
}

func FrontendsFromJSON(in []byte) ([]Frontend, error) {
func FrontendsFromJSON(router router.Router, in []byte) ([]Frontend, error) {
var rf *rawFrontends
err := json.Unmarshal(in, &rf)
if err != nil {
return nil, err
}
out := make([]Frontend, len(rf.Frontends))
for i, raw := range rf.Frontends {
f, err := FrontendFromJSON(raw)
f, err := FrontendFromJSON(router, raw)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -147,7 +148,7 @@ func KeyPairFromJSON(in []byte) (*KeyPair, error) {
return NewKeyPair(c.Cert, c.Key)
}

func FrontendFromJSON(in []byte, id ...string) (*Frontend, error) {
func FrontendFromJSON(router router.Router, in []byte, id ...string) (*Frontend, error) {
var rf *rawFrontend
if err := json.Unmarshal(in, &rf); err != nil {
return nil, err
Expand All @@ -164,7 +165,7 @@ func FrontendFromJSON(in []byte, id ...string) (*Frontend, error) {
if len(id) != 0 {
rf.Id = id[0]
}
f, err := NewHTTPFrontend(rf.Id, rf.BackendId, rf.Route, s)
f, err := NewHTTPFrontend(router, rf.Id, rf.BackendId, rf.Route, s)
if err != nil {
return nil, err
}
Expand Down
5 changes: 3 additions & 2 deletions engine/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/mailgun/vulcand/Godeps/_workspace/src/github.com/mailgun/oxy/stream"
"github.com/mailgun/vulcand/Godeps/_workspace/src/github.com/mailgun/route"
"github.com/mailgun/vulcand/plugin"
"github.com/mailgun/vulcand/router"
)

// StatsProvider provides realtime stats abount endpoints, backends and locations
Expand Down Expand Up @@ -267,13 +268,13 @@ func NewListener(id, protocol, network, address, scope string, settings *HTTPSLi
}, nil
}

func NewHTTPFrontend(id, backendId string, routeExpr string, settings HTTPFrontendSettings) (*Frontend, error) {
func NewHTTPFrontend(router router.Router, id, backendId string, routeExpr string, settings HTTPFrontendSettings) (*Frontend, error) {
if len(id) == 0 || len(backendId) == 0 {
return nil, fmt.Errorf("supply valid route, id, and backendId")
}

// Make sure location path is a valid route expression
if !route.IsValid(routeExpr) {
if !router.IsValid(routeExpr) {
return nil, fmt.Errorf("route should be a valid route expression: %s", routeExpr)
}

Expand Down
15 changes: 8 additions & 7 deletions engine/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/mailgun/vulcand/Godeps/_workspace/src/github.com/mailgun/route"
. "github.com/mailgun/vulcand/Godeps/_workspace/src/gopkg.in/check.v1"
"github.com/mailgun/vulcand/plugin"
"github.com/mailgun/vulcand/plugin/connlimit"
Expand Down Expand Up @@ -33,7 +34,7 @@ func (s *BackendSuite) TestHostBad(c *C) {
}

func (s *BackendSuite) TestFrontendDefaults(c *C) {
f, err := NewHTTPFrontend("f1", "b1", `Path("/home")`, HTTPFrontendSettings{})
f, err := NewHTTPFrontend(route.NewMux(), "f1", "b1", `Path("/home")`, HTTPFrontendSettings{})
c.Assert(err, IsNil)
c.Assert(f.GetId(), Equals, "f1")
c.Assert(f.String(), Not(Equals), "")
Expand All @@ -50,7 +51,7 @@ func (s *BackendSuite) TestNewFrontendWithOptions(c *C) {
Hostname: "host1",
TrustForwardHeader: true,
}
f, err := NewHTTPFrontend("f1", "b1", `Path("/home")`, settings)
f, err := NewHTTPFrontend(route.NewMux(), "f1", "b1", `Path("/home")`, settings)
c.Assert(err, IsNil)
c.Assert(f.Id, Equals, "f1")

Expand All @@ -66,11 +67,11 @@ func (s *BackendSuite) TestNewFrontendWithOptions(c *C) {

func (s *BackendSuite) TestFrontendBadParams(c *C) {
// Bad route
_, err := NewHTTPFrontend("f1", "b1", "/home -- afawf \\~", HTTPFrontendSettings{})
_, err := NewHTTPFrontend(route.NewMux(), "f1", "b1", "/home -- afawf \\~", HTTPFrontendSettings{})
c.Assert(err, NotNil)

// Empty params
_, err = NewHTTPFrontend("", "", "", HTTPFrontendSettings{})
_, err = NewHTTPFrontend(route.NewMux(), "", "", "", HTTPFrontendSettings{})
c.Assert(err, NotNil)
}

Expand All @@ -81,7 +82,7 @@ func (s *BackendSuite) TestFrontendBadOptions(c *C) {
},
}
for _, s := range settings {
f, err := NewHTTPFrontend("f1", "b", `Path("/home")`, s)
f, err := NewHTTPFrontend(route.NewMux(), "f1", "b", `Path("/home")`, s)
c.Assert(err, NotNil)
c.Assert(f, IsNil)
}
Expand Down Expand Up @@ -317,7 +318,7 @@ func (s *BackendSuite) TestNewListenerBadParams(c *C) {
}

func (s *BackendSuite) TestFrontendsFromJSON(c *C) {
f, err := NewHTTPFrontend("f1", "b1", `Path("/path")`, HTTPFrontendSettings{})
f, err := NewHTTPFrontend(route.NewMux(), "f1", "b1", `Path("/path")`, HTTPFrontendSettings{})
c.Assert(err, IsNil)

bytes, err := json.Marshal(f)
Expand All @@ -329,7 +330,7 @@ func (s *BackendSuite) TestFrontendsFromJSON(c *C) {
r := plugin.NewRegistry()
c.Assert(r.AddSpec(connlimit.GetSpec()), IsNil)

out, err := FrontendsFromJSON(bytes)
out, err := FrontendsFromJSON(route.NewMux(), bytes)
c.Assert(err, IsNil)
c.Assert(out, NotNil)
c.Assert(out, DeepEquals, fs)
Expand Down
Loading

0 comments on commit d598c38

Please sign in to comment.