Table now has a dedicated package inside router package.

This commit is contained in:
Milos Gajdos
2019-07-08 16:51:55 +01:00
parent 0c1a28a9b6
commit cc590f5f2c
9 changed files with 71 additions and 62 deletions

View File

@@ -0,0 +1,293 @@
package table
import (
"fmt"
"strings"
"sync"
"github.com/google/uuid"
"github.com/micro/go-log"
"github.com/olekukonko/tablewriter"
)
// TableOptions specify routing table options
// TODO: table options TBD in the future
type TableOptions struct{}
// table is an in memory routing table
type table struct {
// opts are table options
opts TableOptions
// m stores routing table map
m map[string]map[uint64]Route
// w is a list of table watchers
w map[string]*tableWatcher
sync.RWMutex
}
// newTable creates a new routing table and returns it
func newTable(opts ...TableOption) Table {
// default options
var options TableOptions
// apply requested options
for _, o := range opts {
o(&options)
}
return &table{
opts: options,
m: make(map[string]map[uint64]Route),
w: make(map[string]*tableWatcher),
}
}
// Init initializes routing table with options
func (t *table) Init(opts ...TableOption) error {
for _, o := range opts {
o(&t.opts)
}
return nil
}
// Options returns routing table options
func (t *table) Options() TableOptions {
return t.opts
}
// Add adds a route to the routing table
func (t *table) Add(r Route) error {
destAddr := r.Destination
sum := r.Hash()
t.Lock()
defer t.Unlock()
// check if there are any routes in the table for the route destination
if _, ok := t.m[destAddr]; !ok {
t.m[destAddr] = make(map[uint64]Route)
t.m[destAddr][sum] = r
go t.sendEvent(&Event{Type: Create, Route: r})
return nil
}
// add new route to the table for the route destination
if _, ok := t.m[destAddr][sum]; !ok {
t.m[destAddr][sum] = r
go t.sendEvent(&Event{Type: Create, Route: r})
return nil
}
// only add the route if the route override is explicitly requested
if _, ok := t.m[destAddr][sum]; ok && r.Policy == Override {
t.m[destAddr][sum] = r
go t.sendEvent(&Event{Type: Update, Route: r})
return nil
}
// if we reached this point the route must already exist
// we return nil only if explicitly requested by the client
if r.Policy == Skip {
return nil
}
return ErrDuplicateRoute
}
// Delete deletes the route from the routing table
func (t *table) Delete(r Route) error {
destAddr := r.Destination
sum := r.Hash()
t.Lock()
defer t.Unlock()
if _, ok := t.m[destAddr]; !ok {
return ErrRouteNotFound
}
delete(t.m[destAddr], sum)
go t.sendEvent(&Event{Type: Delete, Route: r})
return nil
}
// Update updates routing table with the new route
func (t *table) Update(r Route) error {
destAddr := r.Destination
sum := r.Hash()
t.Lock()
defer t.Unlock()
// check if the route destination has any routes in the table
if _, ok := t.m[destAddr]; !ok {
if r.Policy == Insert {
t.m[destAddr] = make(map[uint64]Route)
t.m[destAddr][sum] = r
go t.sendEvent(&Event{Type: Create, Route: r})
return nil
}
return ErrRouteNotFound
}
// check if the route for the route destination already exists
// NOTE: We only insert the route if explicitly requested by the client
if _, ok := t.m[destAddr][sum]; !ok && r.Policy == Insert {
t.m[destAddr][sum] = r
go t.sendEvent(&Event{Type: Create, Route: r})
return nil
}
// if the route has been found update it
if _, ok := t.m[destAddr][sum]; ok {
t.m[destAddr][sum] = r
go t.sendEvent(&Event{Type: Update, Route: r})
return nil
}
return ErrRouteNotFound
}
// List returns a list of all routes in the table
func (t *table) List() ([]Route, error) {
t.RLock()
defer t.RUnlock()
var routes []Route
for _, rmap := range t.m {
for _, route := range rmap {
routes = append(routes, route)
}
}
return routes, nil
}
// isMatch checks if the route matches given network and router
func isMatch(route Route, network, router string) bool {
if network == "*" || network == route.Network {
if router == "*" || router == route.Router {
return true
}
}
return false
}
// findRoutes finds all the routes for given network and router and returns them
func findRoutes(routes map[uint64]Route, network, router string) []Route {
var results []Route
for _, route := range routes {
if isMatch(route, network, router) {
results = append(results, route)
}
}
return results
}
// Lookup queries routing table and returns all routes that match the lookup query
func (t *table) Lookup(q Query) ([]Route, error) {
t.RLock()
defer t.RUnlock()
if q.Options().Destination != "*" {
// no routes found for the destination and query policy is not a DiscardIfNone
if _, ok := t.m[q.Options().Destination]; !ok && q.Options().Policy != DiscardIfNone {
return nil, ErrRouteNotFound
}
return findRoutes(t.m[q.Options().Destination], q.Options().Network, q.Options().Router), nil
}
var results []Route
// search through all destinations
for _, routes := range t.m {
results = append(results, findRoutes(routes, q.Options().Network, q.Options().Router)...)
}
return results, nil
}
// Watch returns routing table entry watcher
func (t *table) Watch(opts ...WatchOption) (Watcher, error) {
// by default watch everything
wopts := WatchOptions{
Destination: "*",
}
for _, o := range opts {
o(&wopts)
}
watcher := &tableWatcher{
opts: wopts,
resChan: make(chan *Event, 10),
done: make(chan struct{}),
}
t.Lock()
t.w[uuid.New().String()] = watcher
t.Unlock()
return watcher, nil
}
// sendEvent sends rules to all subscribe watchers
func (t *table) sendEvent(r *Event) {
t.RLock()
defer t.RUnlock()
log.Logf("sending event to %d registered table watchers", len(t.w))
for _, w := range t.w {
select {
case w.resChan <- r:
case <-w.done:
}
}
log.Logf("sending event done")
}
// Size returns the size of the routing table
func (t *table) Size() int {
t.RLock()
defer t.RUnlock()
size := 0
for dest, _ := range t.m {
size += len(t.m[dest])
}
return size
}
// String returns debug information
func (t *table) String() string {
t.RLock()
defer t.RUnlock()
// this will help us build routing table string
sb := &strings.Builder{}
// create nice table printing structure
table := tablewriter.NewWriter(sb)
table.SetHeader([]string{"Destination", "Gateway", "Router", "Network", "Metric"})
for _, destRoute := range t.m {
for _, route := range destRoute {
strRoute := []string{
route.Destination,
route.Gateway,
route.Router,
route.Network,
fmt.Sprintf("%d", route.Metric),
}
table.Append(strRoute)
}
}
// render table into sb
table.Render()
return sb.String()
}

View File

@@ -0,0 +1,182 @@
package table
import "testing"
func testSetup() (Table, Route) {
table := NewTable()
route := Route{
Destination: "dest.svc",
Gateway: "dest.gw",
Router: "dest.router",
Network: "dest.network",
Metric: 10,
}
return table, route
}
func TestAdd(t *testing.T) {
table, route := testSetup()
testTableSize := table.Size()
if err := table.Add(route); err != nil {
t.Errorf("error adding route: %s", err)
}
testTableSize += 1
// adds new route for the original destination
route.Gateway = "dest.gw2"
if err := table.Add(route); err != nil {
t.Errorf("error adding route: %s", err)
}
testTableSize += 1
// overrides an existing route
route.Metric = 100
route.Policy = Override
if err := table.Add(route); err != nil {
t.Errorf("error adding route: %s", err)
}
// the size of the table should not change when Override policy is used
if table.Size() != testTableSize {
t.Errorf("invalid number of routes. expected: %d, given: %d", testTableSize, table.Size())
}
// dont add new route if it already exists
route.Policy = Skip
if err := table.Add(route); err != nil {
t.Errorf("error adding route: %s", err)
}
// the size of the table should not change if Skip policy is used
if table.Size() != testTableSize {
t.Errorf("invalid number of routes. expected: %d, given: %d", testTableSize, table.Size())
}
// adding the same route under Insert policy must error
route.Policy = Insert
if err := table.Add(route); err != ErrDuplicateRoute {
t.Errorf("error adding route. Expected error: %s, Given: %s", ErrDuplicateRoute, err)
}
}
func TestDelete(t *testing.T) {
table, route := testSetup()
testTableSize := table.Size()
if err := table.Add(route); err != nil {
t.Errorf("error adding route: %s", err)
}
testTableSize += 1
// should fail to delete non-existant route
prevDest := route.Destination
route.Destination = "randDest"
if err := table.Delete(route); err != ErrRouteNotFound {
t.Errorf("error deleting route. Expected error: %s, given: %s", ErrRouteNotFound, err)
}
// we should be able to delete the existing route
route.Destination = prevDest
if err := table.Delete(route); err != nil {
t.Errorf("error deleting route: %s", err)
}
testTableSize -= 1
if table.Size() != testTableSize {
t.Errorf("invalid number of routes. expected: %d, given: %d", testTableSize, table.Size())
}
}
func TestUpdate(t *testing.T) {
table, route := testSetup()
testTableSize := table.Size()
if err := table.Add(route); err != nil {
t.Errorf("error adding route: %s", err)
}
testTableSize += 1
// change the metric of the original route
route.Metric = 200
if err := table.Update(route); err != nil {
t.Errorf("error updating route: %s", err)
}
// the size of the table should not change as we're only updating the metric of an existing route
if table.Size() != testTableSize {
t.Errorf("invalid number of routes. expected: %d, given: %d", testTableSize, table.Size())
}
// this should add a new route
route.Destination = "new.dest"
if err := table.Update(route); err != nil {
t.Errorf("error updating route: %s", err)
}
testTableSize += 1
// Default policy is Insert so the new route will be added here since the route does not exist
if table.Size() != testTableSize {
t.Errorf("invalid number of routes. expected: %d, given: %d", testTableSize, table.Size())
}
// this should add a new route
route.Gateway = "new.gw"
if err := table.Update(route); err != nil {
t.Errorf("error updating route: %s", err)
}
testTableSize += 1
if table.Size() != testTableSize {
t.Errorf("invalid number of routes. expected: %d, given: %d", testTableSize, table.Size())
}
// this should NOT add a new route as we are setting the policy to Skip
route.Destination = "rand.dest"
route.Policy = Skip
if err := table.Update(route); err != ErrRouteNotFound {
t.Errorf("error updating route. Expected error: %s, given: %s", ErrRouteNotFound, err)
}
if table.Size() != 3 {
t.Errorf("invalid number of routes. expected: %d, given: %d", testTableSize, table.Size())
}
}
func TestList(t *testing.T) {
table, route := testSetup()
dest := []string{"one.svc", "two.svc", "three.svc"}
for i := 0; i < len(dest); i++ {
route.Destination = dest[i]
if err := table.Add(route); err != nil {
t.Errorf("error adding route: %s", err)
}
}
routes, err := table.List()
if err != nil {
t.Errorf("error listing routes: %s", err)
}
if len(routes) != len(dest) {
t.Errorf("incorrect number of routes listed. Expected: %d, Given: %d", len(dest), len(routes))
}
if len(routes) != table.Size() {
t.Errorf("mismatch number of routes and table size. Routes: %d, Size: %d", len(routes), table.Size())
}
}

View File

@@ -0,0 +1,132 @@
package table
import (
"fmt"
"strings"
"github.com/olekukonko/tablewriter"
)
// LookupPolicy defines query policy
type LookupPolicy int
const (
// DiscardIfNone discards query when no route is found
DiscardIfNone LookupPolicy = iota
// ClosestMatch returns closest match to supplied query
ClosestMatch
)
// String returns human representation of LookupPolicy
func (lp LookupPolicy) String() string {
switch lp {
case DiscardIfNone:
return "DISCARD"
case ClosestMatch:
return "CLOSEST"
default:
return "UNKNOWN"
}
}
// QueryOption sets routing table query options
type QueryOption func(*QueryOptions)
// QueryOptions are routing table query options
type QueryOptions struct {
// Destination is destination address
Destination string
// Network is network address
Network string
// Router is router address
Router string
// Policy is query lookup policy
Policy LookupPolicy
}
// QueryDestination sets destination address
func QueryDestination(d string) QueryOption {
return func(o *QueryOptions) {
o.Destination = d
}
}
// QueryNetwork sets route network address
func QueryNetwork(a string) QueryOption {
return func(o *QueryOptions) {
o.Network = a
}
}
// QueryRouter sets route router address
func QueryRouter(r string) QueryOption {
return func(o *QueryOptions) {
o.Router = r
}
}
// QueryPolicy sets query policy
// NOTE: this might be renamed to filter or some such
func QueryPolicy(p LookupPolicy) QueryOption {
return func(o *QueryOptions) {
o.Policy = p
}
}
// Query is routing table query
type Query interface {
// Options returns query options
Options() QueryOptions
}
// query is a basic implementation of Query
type query struct {
opts QueryOptions
}
// NewQuery creates new query and returns it
func NewQuery(opts ...QueryOption) Query {
// default options
// NOTE: by default we use DefaultNetworkMetric
qopts := QueryOptions{
Destination: "*",
Network: "*",
Policy: DiscardIfNone,
}
for _, o := range opts {
o(&qopts)
}
return &query{
opts: qopts,
}
}
// Options returns query options
func (q *query) Options() QueryOptions {
return q.opts
}
// String prints routing table query in human readable form
func (q query) String() string {
// this will help us build routing table string
sb := &strings.Builder{}
// create nice table printing structure
table := tablewriter.NewWriter(sb)
table.SetHeader([]string{"Destination", "Network", "Router", "Policy"})
strQuery := []string{
q.opts.Destination,
q.opts.Network,
q.opts.Router,
fmt.Sprintf("%s", q.opts.Policy),
}
table.Append(strQuery)
// render table into sb
table.Render()
return sb.String()
}

View File

@@ -0,0 +1,91 @@
package table
import (
"fmt"
"hash/fnv"
"strings"
"github.com/olekukonko/tablewriter"
)
var (
// DefaultLocalMetric is default route cost metric for the local network
DefaultLocalMetric = 1
// DefaultNetworkMetric is default route cost metric for the micro network
DefaultNetworkMetric = 10
)
// RoutePolicy defines routing table policy
type RoutePolicy int
const (
// Insert inserts a new route if it does not already exist
Insert RoutePolicy = iota
// Override overrides the route if it already exists
Override
// Skip skips modifying the route if it already exists
Skip
)
// String returns human reprensentation of policy
func (p RoutePolicy) String() string {
switch p {
case Insert:
return "INSERT"
case Override:
return "OVERRIDE"
case Skip:
return "SKIP"
default:
return "UNKNOWN"
}
}
// Route is network route
type Route struct {
// Destination is destination address
Destination string
// Gateway is route gateway
Gateway string
// Router is the router address
Router string
// Network is network address
Network string
// Metric is the route cost metric
Metric int
// Policy defines route policy
Policy RoutePolicy
}
// Hash returns route hash sum.
func (r *Route) Hash() uint64 {
h := fnv.New64()
h.Reset()
h.Write([]byte(r.Destination + r.Gateway + r.Network))
return h.Sum64()
}
// String returns human readable route
func (r Route) String() string {
// this will help us build routing table string
sb := &strings.Builder{}
// create nice table printing structure
table := tablewriter.NewWriter(sb)
table.SetHeader([]string{"Destination", "Gateway", "Router", "Network", "Metric"})
strRoute := []string{
r.Destination,
r.Gateway,
r.Router,
r.Network,
fmt.Sprintf("%d", r.Metric),
}
table.Append(strRoute)
// render table into sb
table.Render()
return sb.String()
}

View File

@@ -0,0 +1,44 @@
package table
import (
"errors"
)
var (
// ErrRouteNotFound is returned when no route was found in the routing table
ErrRouteNotFound = errors.New("route not found")
// ErrDuplicateRoute is returned when the route already exists
ErrDuplicateRoute = errors.New("duplicate route")
)
// Table defines routing table interface
type Table interface {
// Init initializes the router with options
Init(...TableOption) error
// Options returns the router options
Options() TableOptions
// Add adds new route to the routing table
Add(Route) error
// Delete deletes existing route from the routing table
Delete(Route) error
// Update updates route in the routing table
Update(Route) error
// List returns the list of all routes in the table
List() ([]Route, error)
// Lookup looks up routes in the routing table and returns them
Lookup(Query) ([]Route, error)
// Watch returns a watcher which allows to track updates to the routing table
Watch(opts ...WatchOption) (Watcher, error)
// Size returns the size of the routing table
Size() int
// String prints the routing table
String() string
}
// TableOption used by the routing table
type TableOption func(*TableOptions)
// NewTable creates new routing table and returns it
func NewTable(opts ...TableOption) Table {
return newTable(opts...)
}

View File

@@ -0,0 +1,144 @@
package table
import (
"errors"
"fmt"
"strings"
"time"
"github.com/micro/go-log"
"github.com/olekukonko/tablewriter"
)
var (
// ErrWatcherStopped is returned when routing table watcher has been stopped
ErrWatcherStopped = errors.New("watcher stopped")
)
// EventType defines routing table event
type EventType int
const (
// Create is emitted when a new route has been created
Create EventType = iota
// Delete is emitted when an existing route has been deleted
Delete
// Update is emitted when an existing route has been updated
Update
)
// String returns string representation of the event
func (et EventType) String() string {
switch et {
case Create:
return "CREATE"
case Delete:
return "DELETE"
case Update:
return "UPDATE"
default:
return "UNKNOWN"
}
}
// Event is returned by a call to Next on the watcher.
type Event struct {
// Type defines type of event
Type EventType
// Timestamp is event timestamp
Timestamp time.Time
// Route is table route
Route Route
}
// String prints human readable Event
func (e Event) String() string {
return fmt.Sprintf("[EVENT] %s:\nRoute:\n%s", e.Type, e.Route)
}
// WatchOption is used to define what routes to watch in the table
type WatchOption func(*WatchOptions)
// Watcher defines routing table watcher interface
// Watcher returns updates to the routing table
type Watcher interface {
// Next is a blocking call that returns watch result
Next() (*Event, error)
// Chan returns event channel
Chan() (<-chan *Event, error)
// Stop stops watcher
Stop()
}
// WatchOptions are table watcher options
type WatchOptions struct {
// Specify destination address to watch
Destination string
}
// WatchDestination sets what destination to watch
// Destination is usually microservice name
func WatchDestination(d string) WatchOption {
return func(o *WatchOptions) {
o.Destination = d
}
}
type tableWatcher struct {
opts WatchOptions
resChan chan *Event
done chan struct{}
}
// Next returns the next noticed action taken on table
// TODO: this needs to be thought through properly;
// right now we only allow to watch destination
func (w *tableWatcher) Next() (*Event, error) {
for {
select {
case res := <-w.resChan:
switch w.opts.Destination {
case res.Route.Destination, "*":
return res, nil
default:
log.Logf("no table watcher available to receive the event")
continue
}
case <-w.done:
return nil, ErrWatcherStopped
}
}
}
// Chan returns watcher events channel
func (w *tableWatcher) Chan() (<-chan *Event, error) {
return w.resChan, nil
}
// Stop stops routing table watcher
func (w *tableWatcher) Stop() {
select {
case <-w.done:
return
default:
close(w.done)
}
}
// String prints debug information
func (w *tableWatcher) String() string {
sb := &strings.Builder{}
table := tablewriter.NewWriter(sb)
table.SetHeader([]string{"Destination"})
data := []string{
w.opts.Destination,
}
table.Append(data)
// render table into sb
table.Render()
return sb.String()
}