diff --git a/bird/bird.go b/bird/bird.go index cf18015..29f1de8 100644 --- a/bird/bird.go +++ b/bird/bird.go @@ -20,22 +20,59 @@ var RateLimitConf struct { Conf RateLimitConfig } -var Cache = struct { +type Cache struct { sync.RWMutex m map[string]Parsed -}{m: make(map[string]Parsed)} +} + +var ParsedCache = Cache{m: make(map[string]Parsed)} +var MetaCache = Cache{m: make(map[string]Parsed)} var NilParse Parsed = (Parsed)(nil) var BirdError Parsed = Parsed{"error": "bird unreachable"} +var RunQueue sync.Map + func IsSpecial(ret Parsed) bool { return reflect.DeepEqual(ret, NilParse) || reflect.DeepEqual(ret, BirdError) } -func fromCache(key string) (Parsed, bool) { - Cache.RLock() - val, ok := Cache.m[key] - Cache.RUnlock() +// Determines the key in the cache, where the result of specific functions are stored. +// Eliminates the need to know what command was executed by that function. +func GetCacheKey(fname string, fargs ...interface{}) string { + key := strings.ToLower(fname) + + for _, arg := range fargs { + switch arg.(type) { + case string: + key += "_" + strings.ToLower(arg.(string)) + } + } + + return key +} + +func (c *Cache) Store(key string, val Parsed) { + var ttl int = 5 + if ClientConf.CacheTtl > 0 { + ttl = ClientConf.CacheTtl + } + cachedAt := time.Now().UTC() + cacheTtl := cachedAt.Add(time.Duration(ttl) * time.Minute) + + c.Lock() + // This is not a really ... clean way of doing this. + val["ttl"] = cacheTtl + val["cached_at"] = cachedAt + + c.m[key] = val + c.Unlock() +} + +func (c *Cache) Get(key string) (Parsed, bool) { + c.RLock() + val, ok := c.m[key] + c.RUnlock() if !ok { return NilParse, false } @@ -48,23 +85,6 @@ func fromCache(key string) (Parsed, bool) { return val, ok } -func toCache(key string, val Parsed) { - var ttl int = 5 - if ClientConf.CacheTtl > 0 { - ttl = ClientConf.CacheTtl - } - cachedAt := time.Now().UTC() - cacheTtl := cachedAt.Add(time.Duration(ttl) * time.Minute) - - // This is not a really ... clean way of doing this. - val["ttl"] = cacheTtl - val["cached_at"] = cachedAt - - Cache.Lock() - Cache.m[key] = val - Cache.Unlock() -} - func Run(args string) (io.Reader, error) { args = "-r " + "show " + args // enforce birdc in restricted mode with "-r" argument argsList := strings.Split(args, " ") @@ -111,69 +131,108 @@ func checkRateLimit() bool { return true } -func RunAndParse(cmd string, parser func(io.Reader) Parsed) (Parsed, bool) { - if val, ok := fromCache(cmd); ok { +func RunAndParse(key string, cmd string, parser func(io.Reader) Parsed, updateCache func(*Parsed)) (Parsed, bool) { + if val, ok := ParsedCache.Get(cmd); ok { return val, true } + var wg sync.WaitGroup + wg.Add(1) + if queueGroup, queueLoaded := RunQueue.LoadOrStore(cmd, &wg); queueLoaded { + (*queueGroup.(*sync.WaitGroup)).Wait() + + if val, ok := ParsedCache.Get(cmd); ok { + return val, true + } else { + // TODO BirdError should also be signaled somehow + return NilParse, false + } + } + if !checkRateLimit() { + wg.Done() + RunQueue.Delete(cmd) return NilParse, false } out, err := Run(cmd) if err != nil { // ignore errors for now + wg.Done() + RunQueue.Delete(cmd) return BirdError, false } parsed := parser(out) - toCache(cmd, parsed) + + if updateCache != nil { + updateCache(&parsed) + } + + ParsedCache.Store(cmd, parsed) + + wg.Done() + + RunQueue.Delete(cmd) + return parsed, false } func Status() (Parsed, bool) { - birdStatus, from_cache := RunAndParse("status", parseStatus) - if IsSpecial(birdStatus) { - return birdStatus, from_cache + updateParsedCache := func(p *Parsed) { + status := (*p)["status"].(Parsed) + + // Last Reconfig Timestamp source: + var lastReconfig string + switch StatusConf.ReconfigTimestampSource { + case "bird": + lastReconfig = status["last_reconfig"].(string) + break + case "config_modified": + lastReconfig = lastReconfigTimestampFromFileStat( + ClientConf.ConfigFilename, + ) + case "config_regex": + lastReconfig = lastReconfigTimestampFromFileContent( + ClientConf.ConfigFilename, + StatusConf.ReconfigTimestampMatch, + ) + } + + status["last_reconfig"] = lastReconfig + + // Filter fields + for _, field := range StatusConf.FilterFields { + status[field] = nil + } } - if from_cache { - return birdStatus, from_cache - } - - status := birdStatus["status"].(Parsed) - - // Last Reconfig Timestamp source: - var lastReconfig string - switch StatusConf.ReconfigTimestampSource { - case "bird": - lastReconfig = status["last_reconfig"].(string) - break - case "config_modified": - lastReconfig = lastReconfigTimestampFromFileStat( - ClientConf.ConfigFilename, - ) - case "config_regex": - lastReconfig = lastReconfigTimestampFromFileContent( - ClientConf.ConfigFilename, - StatusConf.ReconfigTimestampMatch, - ) - } - - status["last_reconfig"] = lastReconfig - - // Filter fields - for _, field := range StatusConf.FilterFields { - status[field] = nil - } - - birdStatus["status"] = status - + birdStatus, from_cache := RunAndParse(GetCacheKey("Status"), "status", parseStatus, updateParsedCache) return birdStatus, from_cache } func Protocols() (Parsed, bool) { - return RunAndParse("protocols all", parseProtocols) + createMetaCache := func(p *Parsed) { + metaProtocol := Parsed{"protocols": Parsed{"bird_protocol": Parsed{}}} + + for key, _ := range (*p)["protocols"].(Parsed) { + parsed := (*p)["protocols"].(Parsed)[key].(Parsed) + + protocol := parsed["protocol"].(string) + + birdProtocol := parsed["bird_protocol"].(string) + // Check if the structure for the current birdProtocol already exists inside the metaProtocol cache, if not create it (BGP|Pipe|etc) + if _, ok := metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol]; !ok { + metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol] = Parsed{} + } + metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol].(Parsed)[protocol] = &parsed + } + + MetaCache.Store(GetCacheKey("Protocols"), metaProtocol) + } + + res, from_cache := RunAndParse(GetCacheKey("Protocols"), "protocols all", parseProtocols, createMetaCache) + return res, from_cache } func ProtocolsBgp() (Parsed, bool) { @@ -182,12 +241,13 @@ func ProtocolsBgp() (Parsed, bool) { return protocols, from_cache } + protocolsMeta, _ := MetaCache.Get(GetCacheKey("Protocols")) + metaProtocol := protocolsMeta["protocols"].(Parsed) + bgpProtocols := Parsed{} - for key, protocol := range protocols["protocols"].(Parsed) { - if protocol.(Parsed)["bird_protocol"] == "BGP" { - bgpProtocols[key] = protocol - } + for key, protocol := range metaProtocol["bird_protocol"].(Parsed)["BGP"].(Parsed) { + bgpProtocols[key] = *(protocol.(*Parsed)) } return Parsed{"protocols": bgpProtocols, @@ -196,82 +256,100 @@ func ProtocolsBgp() (Parsed, bool) { } func Symbols() (Parsed, bool) { - return RunAndParse("symbols", parseSymbols) + return RunAndParse(GetCacheKey("Symbols"), "symbols", parseSymbols, nil) } func RoutesPrefixed(prefix string) (Parsed, bool) { cmd := routeQueryForChannel("route " + prefix + " all") - return RunAndParse(cmd, parseRoutes) + return RunAndParse(GetCacheKey("RoutesPrefixed", prefix), cmd, parseRoutes, nil) } func RoutesProto(protocol string) (Parsed, bool) { cmd := routeQueryForChannel("route all protocol " + protocol) - return RunAndParse(cmd, parseRoutes) + return RunAndParse(GetCacheKey("RoutesProto", protocol), cmd, parseRoutes, nil) } func RoutesProtoCount(protocol string) (Parsed, bool) { cmd := routeQueryForChannel("route protocol "+protocol) + " count" - return RunAndParse(cmd, parseRoutesCount) + return RunAndParse(GetCacheKey("RoutesProtoCount", protocol), cmd, parseRoutesCount, nil) } func RoutesProtoPrimaryCount(protocol string) (Parsed, bool) { cmd := routeQueryForChannel("route primary protocol "+protocol) + " count" - return RunAndParse(cmd, parseRoutesCount) + return RunAndParse(GetCacheKey("RoutesProtoPrimaryCount", protocol), cmd, parseRoutesCount, nil) +} + +func PipeRoutesFilteredCount(pipe string, table string, neighborAddress string) (Parsed, bool) { + cmd := "route table " + table + " noexport " + pipe + " where from=" + neighborAddress + " count" + return RunAndParse(GetCacheKey("PipeRoutesFilteredCount", table, pipe, neighborAddress), cmd, parseRoutesCount, nil) +} + +func PipeRoutesFiltered(pipe string, table string) (Parsed, bool) { + cmd := routeQueryForChannel("route table '" + table + "' noexport '" + pipe + "' all") + return RunAndParse(GetCacheKey("PipeRoutesFiltered", table, pipe), cmd, parseRoutes, nil) } func RoutesFiltered(protocol string) (Parsed, bool) { cmd := routeQueryForChannel("route all filtered protocol " + protocol) - return RunAndParse(cmd, parseRoutes) + return RunAndParse(GetCacheKey("RoutesFiltered", protocol), cmd, parseRoutes, nil) } func RoutesExport(protocol string) (Parsed, bool) { cmd := routeQueryForChannel("route all export " + protocol) - return RunAndParse(cmd, parseRoutes) + return RunAndParse(GetCacheKey("RoutesExport", protocol), cmd, parseRoutes, nil) } func RoutesNoExport(protocol string) (Parsed, bool) { - // In case we have a multi table setup, we have to query // the pipe protocol. if ParserConf.PerPeerTables && strings.HasPrefix(protocol, ParserConf.PeerProtocolPrefix) { + protocolsRes, from_cache := ProtocolsBgp() + if IsSpecial(protocolsRes) { + return protocolsRes, from_cache + } + if _, ok := protocolsRes["protocols"].(Parsed)[protocol]; !ok { + return NilParse, false + } + // Replace prefix protocol = ParserConf.PipeProtocolPrefix + protocol[len(ParserConf.PeerProtocolPrefix):] } cmd := routeQueryForChannel("route all noexport " + protocol) - return RunAndParse(cmd, parseRoutes) + return RunAndParse(GetCacheKey("RoutesNoExport", protocol), cmd, parseRoutes, nil) } func RoutesExportCount(protocol string) (Parsed, bool) { cmd := routeQueryForChannel("route export "+protocol) + " count" - return RunAndParse(cmd, parseRoutesCount) + return RunAndParse(GetCacheKey("RoutesExportCount", protocol), cmd, parseRoutesCount, nil) } func RoutesTable(table string) (Parsed, bool) { - return RunAndParse("route table "+table+" all", parseRoutes) + return RunAndParse(GetCacheKey("RoutesTable", table), "route table "+table+" all", parseRoutes, nil) } func RoutesTableCount(table string) (Parsed, bool) { - return RunAndParse("route table "+table+" count", parseRoutesCount) + return RunAndParse(GetCacheKey("RoutesTableCount", table), "route table "+table+" count", parseRoutesCount, nil) } func RoutesLookupTable(net string, table string) (Parsed, bool) { - return RunAndParse("route for "+net+" table "+table+" all", parseRoutes) + return RunAndParse(GetCacheKey("RoutesLookupTable", net, table), "route for "+net+" table "+table+" all", parseRoutes, nil) } func RoutesLookupProtocol(net string, protocol string) (Parsed, bool) { - return RunAndParse("route for "+net+" protocol "+protocol+" all", parseRoutes) + return RunAndParse(GetCacheKey("RoutesLookupProtocol", net, protocol), "route for "+net+" protocol "+protocol+" all", parseRoutes, nil) } func RoutesPeer(peer string) (Parsed, bool) { cmd := routeQueryForChannel("route export " + peer) - return RunAndParse(cmd, parseRoutes) + return RunAndParse(GetCacheKey("RoutesPeer", peer), cmd, parseRoutes, nil) } func RoutesDump() (Parsed, bool) { + // TODO insert hook to update the cache with the route count information if ParserConf.PerPeerTables { return RoutesDumpPerPeerTable() } @@ -280,8 +358,14 @@ func RoutesDump() (Parsed, bool) { } func RoutesDumpSingleTable() (Parsed, bool) { - importedRes, cached := RunAndParse(routeQueryForChannel("route all"), parseRoutes) - filteredRes, _ := RunAndParse(routeQueryForChannel("route all filtered"), parseRoutes) + importedRes, cached := RunAndParse(GetCacheKey("RoutesDumpSingleTable", "imported"), routeQueryForChannel("route all"), parseRoutes, nil) + if IsSpecial(importedRes) { + return importedRes, cached + } + filteredRes, cached := RunAndParse(GetCacheKey("RoutesDumpSingleTable", "filtered"), routeQueryForChannel("route all filtered"), parseRoutes, nil) + if IsSpecial(filteredRes) { + return filteredRes, cached + } imported := importedRes["routes"] filtered := filteredRes["routes"] @@ -295,12 +379,18 @@ func RoutesDumpSingleTable() (Parsed, bool) { } func RoutesDumpPerPeerTable() (Parsed, bool) { - importedRes, cached := RunAndParse(routeQueryForChannel("route all"), parseRoutes) + importedRes, cached := RunAndParse(GetCacheKey("RoutesDumpPerPeerTable", "imported"), routeQueryForChannel("route all"), parseRoutes, nil) + if IsSpecial(importedRes) { + return importedRes, cached + } imported := importedRes["routes"] filtered := []Parsed{} // Get protocols with filtered routes - protocolsRes, _ := ProtocolsBgp() + protocolsRes, cached := ProtocolsBgp() + if IsSpecial(protocolsRes) { + return protocolsRes, cached + } protocols := protocolsRes["protocols"].(Parsed) for protocol, details := range protocols { @@ -315,11 +405,7 @@ func RoutesDumpPerPeerTable() (Parsed, bool) { continue // nothing to do here. } // Lookup filtered routes - pfilteredRes, from_cache := RoutesFiltered(protocol) - if reflect.DeepEqual(pfilteredRes, BirdError) { - return pfilteredRes, from_cache - } - + pfilteredRes, _ := RoutesFiltered(protocol) pfiltered, ok := pfilteredRes["routes"].([]Parsed) if !ok { continue // something went wrong... @@ -338,6 +424,10 @@ func RoutesDumpPerPeerTable() (Parsed, bool) { func routeQueryForChannel(cmd string) string { status, _ := Status() + if IsSpecial(status) { + return cmd + } + birdStatus, ok := status["status"].(Parsed) if !ok { return cmd diff --git a/endpoints/endpoint.go b/endpoints/endpoint.go index e967744..709e995 100644 --- a/endpoints/endpoint.go +++ b/endpoints/endpoint.go @@ -54,8 +54,8 @@ func Endpoint(wrapped endpoint) httprouter.Handle { } res := make(map[string]interface{}) - ret, from_cache := wrapped(r, ps) + if reflect.DeepEqual(ret, bird.NilParse) { w.WriteHeader(http.StatusTooManyRequests) return diff --git a/test/protocols_bgp_pipe.sample b/test/protocols_bgp_pipe.sample index 4718922..d6c2e5d 100644 --- a/test/protocols_bgp_pipe.sample +++ b/test/protocols_bgp_pipe.sample @@ -39,4 +39,5 @@ R194_42 BGP T65001_nada_co_ripe up 2018-05-31 15:38:40 Established Route limit: 710/200000 Hold timer: 151/180 Keepalive timer: 43/60 + Last error: Socket: Connection closed