You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by sr...@apache.org on 2022/05/26 16:19:45 UTC

[trafficcontrol] branch master updated: Add TO option to cache server update status in memory (#6838)

This is an automated email from the ASF dual-hosted git repository.

srijeet0406 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 31205d568c Add TO option to cache server update status in memory (#6838)
31205d568c is described below

commit 31205d568ce80a3dc8eeb18209f46814768e52f6
Author: Rawlin Peters <ra...@apache.org>
AuthorDate: Thu May 26 10:19:41 2022 -0600

    Add TO option to cache server update status in memory (#6838)
    
    * Add TO option to cache server update status in memory
    
    To reduce load on the Traffic Ops database, add an option to
    periodically read all server update status data from the database and
    serve all requests to /servers/{hostname}/update_status from memory.
    
    With this option enabled, `t3c` runs on cache servers that don't have
    updates pending don't cause any TODB queries, which means TODB has more
    capacity for actually propagating changes.
    
    The tradeoff is that it can take up to
    `server_update_status_cache_refresh_interval_sec` seconds for cache
    servers to see updates/revalidations pending. However, this is better
    because it allows a cache server to get updates sooner than it would
    otherwise (due to being able to run `t3c` more frequently).
    
    * Address review comments
    
    * Add defaults to CIAB cdn.conf
---
 CHANGELOG.md                                       |   1 +
 docs/source/admin/traffic_ops.rst                  |   6 +
 .../ansible/roles/traffic_ops/defaults/main.yml    |   1 +
 .../roles/traffic_ops/templates/cdn.conf.j2        |   1 +
 infrastructure/cdn-in-a-box/traffic_ops/config.sh  |   2 +
 lib/go-tc/enum.go                                  |   5 +
 traffic_ops/app/conf/cdn.conf                      |   1 +
 traffic_ops/traffic_ops_golang/auth/usercache.go   |  14 +-
 traffic_ops/traffic_ops_golang/config/config.go    |  58 ++---
 .../traffic_ops_golang/config/config_test.go       |   6 +-
 .../server/servers_update_status.go                | 260 ++++++++++++++++++++-
 .../server/servers_update_status_test.go           | 131 ++++++++++-
 .../traffic_ops_golang/traffic_ops_golang.go       |  14 +-
 13 files changed, 452 insertions(+), 48 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ec525f362..532c07e57c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Monitor config option `distributed_polling` which enables the ability for Traffic Monitor to poll a subset of the CDN and divide into "local peer groups" and "distributed peer groups". Traffic Monitors in the same group are local peers, while Traffic Monitors in other groups are distibuted peers. Each TM group polls the same set of cachegroups and gets availability data for the other cachegroups from other TM groups. This allows each TM to be responsible for polling a subset of [...]
 - Added support for a new Traffic Ops GLOBAL profile parameter -- `tm_query_status_override` -- to override which status of Traffic Monitors to query (default: ONLINE).
 - Traffic Ops: added new `cdn.conf` option -- `user_cache_refresh_interval_sec` -- which enables an in-memory users cache to improve performance. Default: 0 (disabled).
+- Traffic Ops: added new `cdn.conf` option -- `server_update_status_cache_refresh_interval_sec` -- which enables an in-memory server update status cache to improve performance. Default: 0 (disabled).
 - Traffic Router: Add support for `file`-protocol URLs for the `geolocation.polling.url` for the Geolocation database.
 - Replaces all Traffic Portal Tenant select boxes with a novel tree select box [#6427](https://github.com/apache/trafficcontrol/issues/6427).
 - Traffic Monitor: Add support for `access.log` to TM.
diff --git a/docs/source/admin/traffic_ops.rst b/docs/source/admin/traffic_ops.rst
index 6ea3cbf479..0a3c28c66c 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -511,6 +511,12 @@ This file deals with the configuration parameters of running Traffic Ops itself.
 
 	.. versionadded:: 7.0
 
+:server_update_status_cache_refresh_interval_sec: This optional integer value specifies the interval (in seconds) between refreshing the in-memory server update status cache. Default: 0 (disabled).
+
+	.. warning:: Enabling the server update status cache improves performance by reducing the number of queries made to the Traffic Ops database, but it means that it may take up to this many seconds before any server updates or revalidations are reflected in the :ref:`to-api-servers-hostname-update_status` API.
+
+	.. versionadded:: 7.0
+
 
 Example cdn.conf
 ''''''''''''''''
diff --git a/infrastructure/ansible/roles/traffic_ops/defaults/main.yml b/infrastructure/ansible/roles/traffic_ops/defaults/main.yml
index bec28e6916..c368069358 100644
--- a/infrastructure/ansible/roles/traffic_ops/defaults/main.yml
+++ b/infrastructure/ansible/roles/traffic_ops/defaults/main.yml
@@ -45,6 +45,7 @@ to_disable_auto_cert_deletion: false
 to_use_ims: true
 to_use_rbp: true
 to_user_cache_refresh_interval_sec: 0
+to_server_update_status_cache_refresh_interval_sec: 0
 to_heartbeat_timeout: 20
 to_hypnotoad_number_of_workers: 12
 to_cors_access_control_allow_origin: "http://localhost:8080"
diff --git a/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2 b/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2
index 4244b7fcc9..5dfeaccfca 100644
--- a/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2
+++ b/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2
@@ -50,6 +50,7 @@
       "address" : "{{ to_smtp_address }}"
    },
    "user_cache_refresh_interval_sec": {{ to_user_cache_refresh_interval_sec }},
+   "server_update_status_cache_refresh_interval_sec": {{ to_server_update_status_cache_refresh_interval_sec }},
    "disable_auto_cert_deletion": {{ to_disable_auto_cert_deletion | bool | lower }},
    "use_ims": {{ to_use_ims | bool | lower }},
    "role_based_permissions": {{ to_use_rbp | bool | lower }},
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/config.sh b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
index d31393fd96..c44525a839 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/config.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
@@ -81,6 +81,8 @@ cdn_conf=/opt/traffic_ops/app/conf/cdn.conf
     },
     "disable_auto_cert_deletion": false,
     "use_ims": true,
+    "server_update_status_cache_refresh_interval_sec": 0,
+    "user_cache_refresh_interval_sec": 0,
     "role_based_permissions": true,
     "traffic_ops_golang" : {
           "traffic_vault_backend": "$TV_BACKEND",
diff --git a/lib/go-tc/enum.go b/lib/go-tc/enum.go
index 8d100fa683..453270e751 100644
--- a/lib/go-tc/enum.go
+++ b/lib/go-tc/enum.go
@@ -107,6 +107,11 @@ func CacheTypeFromString(s string) CacheType {
 	return CacheTypeInvalid
 }
 
+// IsValidCacheType returns true if the given string represents a valid cache type.
+func IsValidCacheType(s string) bool {
+	return CacheTypeFromString(s) != CacheTypeInvalid
+}
+
 // InterfaceName is the name of a server interface.
 type InterfaceName string
 
diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf
index eb3ce5778e..9eceeefb35 100644
--- a/traffic_ops/app/conf/cdn.conf
+++ b/traffic_ops/app/conf/cdn.conf
@@ -45,6 +45,7 @@
     },
     "disable_auto_cert_deletion": false,
     "user_cache_refresh_interval_sec": 0,
+    "server_update_status_cache_refresh_interval_sec": 0,
     "use_ims": false,
     "role_based_permissions": true,
     "cors" : {
diff --git a/traffic_ops/traffic_ops_golang/auth/usercache.go b/traffic_ops/traffic_ops_golang/auth/usercache.go
index 95520bbbb9..1cfebef9ab 100644
--- a/traffic_ops/traffic_ops_golang/auth/usercache.go
+++ b/traffic_ops/traffic_ops_golang/auth/usercache.go
@@ -127,17 +127,17 @@ func startUsersCacheRefresher(interval time.Duration, db *sql.DB, timeout time.D
 }
 
 func refreshUsersCache(db *sql.DB, timeout time.Duration) {
-	usersCache.Lock()
-	defer usersCache.Unlock()
 	newUsers, err := getUsers(db, timeout)
 	if err != nil {
 		log.Errorf("refreshing users cache: %s", err.Error())
-	} else {
-		usersCache.userMap = newUsers
-		usersCache.usernamesByToken = createTokenToUsernameMap(newUsers)
-		usersCache.initialized = true
-		log.Infof("refreshed users cache (len = %d)", len(usersCache.userMap))
+		return
 	}
+	usersCache.Lock()
+	defer usersCache.Unlock()
+	usersCache.userMap = newUsers
+	usersCache.usernamesByToken = createTokenToUsernameMap(newUsers)
+	usersCache.initialized = true
+	log.Infof("refreshed users cache (len = %d)", len(usersCache.userMap))
 }
 
 func createTokenToUsernameMap(users map[string]user) map[string]string {
diff --git a/traffic_ops/traffic_ops_golang/config/config.go b/traffic_ops/traffic_ops_golang/config/config.go
index 521da1d9bd..37cffbe7c2 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -67,33 +67,34 @@ type BackendConfig struct {
 
 // Config reflects the structure of the cdn.conf file
 type Config struct {
-	URL                         *url.URL `json:"-"`
-	CertPath                    string   `json:"-"`
-	KeyPath                     string   `json:"-"`
-	ConfigHypnotoad             `json:"hypnotoad"`
-	ConfigTrafficOpsGolang      `json:"traffic_ops_golang"`
-	ConfigTO                    *ConfigTO   `json:"to"`
-	SMTP                        *ConfigSMTP `json:"smtp"`
-	ConfigPortal                `json:"portal"`
-	ConfigLetsEncrypt           `json:"lets_encrypt"`
-	ConfigAcmeRenewal           `json:"acme_renewal"`
-	AcmeAccounts                []ConfigAcmeAccount `json:"acme_accounts"`
-	DB                          ConfigDatabase      `json:"db"`
-	Secrets                     []string            `json:"secrets"`
-	TrafficVaultEnabled         bool
-	ConfigLDAP                  *ConfigLDAP
-	UserCacheRefreshIntervalSec int `json:"user_cache_refresh_interval_sec"`
-	LDAPEnabled                 bool
-	LDAPConfPath                string `json:"ldap_conf_location"`
-	ConfigInflux                *ConfigInflux
-	InfluxEnabled               bool
-	InfluxDBConfPath            string `json:"influxdb_conf_path"`
-	Version                     string
-	DisableAutoCertDeletion     bool                    `json:"disable_auto_cert_deletion"`
-	UseIMS                      bool                    `json:"use_ims"`
-	RoleBasedPermissions        bool                    `json:"role_based_permissions"`
-	DefaultCertificateInfo      *DefaultCertificateInfo `json:"default_certificate_info"`
-	Cdni                        *CdniConf               `json:"cdni"`
+	URL                                       *url.URL `json:"-"`
+	CertPath                                  string   `json:"-"`
+	KeyPath                                   string   `json:"-"`
+	ConfigHypnotoad                           `json:"hypnotoad"`
+	ConfigTrafficOpsGolang                    `json:"traffic_ops_golang"`
+	ConfigTO                                  *ConfigTO   `json:"to"`
+	SMTP                                      *ConfigSMTP `json:"smtp"`
+	ConfigPortal                              `json:"portal"`
+	ConfigLetsEncrypt                         `json:"lets_encrypt"`
+	ConfigAcmeRenewal                         `json:"acme_renewal"`
+	AcmeAccounts                              []ConfigAcmeAccount `json:"acme_accounts"`
+	DB                                        ConfigDatabase      `json:"db"`
+	Secrets                                   []string            `json:"secrets"`
+	TrafficVaultEnabled                       bool
+	ConfigLDAP                                *ConfigLDAP
+	UserCacheRefreshIntervalSec               int `json:"user_cache_refresh_interval_sec"`
+	ServerUpdateStatusCacheRefreshIntervalSec int `json:"server_update_status_cache_refresh_interval_sec"`
+	LDAPEnabled                               bool
+	LDAPConfPath                              string `json:"ldap_conf_location"`
+	ConfigInflux                              *ConfigInflux
+	InfluxEnabled                             bool
+	InfluxDBConfPath                          string `json:"influxdb_conf_path"`
+	Version                                   string
+	DisableAutoCertDeletion                   bool                    `json:"disable_auto_cert_deletion"`
+	UseIMS                                    bool                    `json:"use_ims"`
+	RoleBasedPermissions                      bool                    `json:"role_based_permissions"`
+	DefaultCertificateInfo                    *DefaultCertificateInfo `json:"default_certificate_info"`
+	Cdni                                      *CdniConf               `json:"cdni"`
 }
 
 // ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server
@@ -490,6 +491,9 @@ func ParseConfig(cfg Config) (Config, error) {
 	if cfg.UserCacheRefreshIntervalSec < 0 {
 		cfg.UserCacheRefreshIntervalSec = 0
 	}
+	if cfg.ServerUpdateStatusCacheRefreshIntervalSec < 0 {
+		cfg.ServerUpdateStatusCacheRefreshIntervalSec = 0
+	}
 
 	invalidTOURLStr := ""
 	var err error
diff --git a/traffic_ops/traffic_ops_golang/config/config_test.go b/traffic_ops/traffic_ops_golang/config/config_test.go
index 70c03d1f41..ee01de555b 100644
--- a/traffic_ops/traffic_ops_golang/config/config_test.go
+++ b/traffic_ops/traffic_ops_golang/config/config_test.go
@@ -99,6 +99,7 @@ const (
 		"workers" : 12
 	},
 	"user_cache_refresh_interval_sec": 30,
+	"server_update_status_cache_refresh_interval_sec": 15,
 	"disable_auto_cert_deletion": true,
 	"traffic_ops_golang" : {
 		"port" : "443",
@@ -239,7 +240,10 @@ func TestLoadConfig(t *testing.T) {
 		t.Errorf("expected traffic_vault_backend to be 'something', actual: '%s'", cfg.TrafficVaultBackend)
 	}
 	if cfg.UserCacheRefreshIntervalSec != 30 {
-		t.Errorf("expected user_refresh_interval_sec: 30, actual: %d", cfg.UserCacheRefreshIntervalSec)
+		t.Errorf("expected user_cache_refresh_interval_sec: 30, actual: %d", cfg.UserCacheRefreshIntervalSec)
+	}
+	if cfg.ServerUpdateStatusCacheRefreshIntervalSec != 15 {
+		t.Errorf("expected server_update_status_cache_refresh_interval_sec: 15, actual: %d", cfg.ServerUpdateStatusCacheRefreshIntervalSec)
 	}
 	tvConfig := make(map[string]string)
 	err = json.Unmarshal(cfg.TrafficVaultConfig, &tvConfig)
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status.go b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
index bdc2e9aa0d..589d643f6f 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
@@ -20,16 +20,18 @@ package server
  */
 
 import (
+	"context"
 	"database/sql"
 	"fmt"
 	"net/http"
-
-	"github.com/lib/pq"
+	"sync"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+
+	"github.com/lib/pq"
 )
 
 func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
@@ -40,7 +42,7 @@ func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	serverUpdateStatuses, err, _ := getServerUpdateStatus(inf.Tx.Tx, inf.Config, inf.Params["host_name"])
+	serverUpdateStatuses, err, _ := getServerUpdateStatus(inf.Tx.Tx, inf.Params["host_name"])
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
@@ -56,7 +58,10 @@ func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func getServerUpdateStatus(tx *sql.Tx, cfg *config.Config, hostName string) ([]tc.ServerUpdateStatusV40, error, error) {
+func getServerUpdateStatus(tx *sql.Tx, hostName string) ([]tc.ServerUpdateStatusV40, error, error) {
+	if serverUpdateStatusCacheIsInitialized() {
+		return getServerUpdateStatusFromCache(hostName), nil, nil
+	}
 
 	updateStatuses := []tc.ServerUpdateStatusV40{}
 
@@ -175,3 +180,248 @@ ORDER BY s.id
 	}
 	return updateStatuses, nil, nil
 }
+
+type serverUpdateStatuses struct {
+	serverMap map[string][]tc.ServerUpdateStatusV40
+	*sync.RWMutex
+	initialized bool
+	enabled     bool // note: enabled is only written to once at startup, before serving requests, so it doesn't need synchronized access
+}
+
+var serverUpdateStatusCache = serverUpdateStatuses{RWMutex: &sync.RWMutex{}}
+
+func serverUpdateStatusCacheIsInitialized() bool {
+	if serverUpdateStatusCache.enabled {
+		serverUpdateStatusCache.RLock()
+		defer serverUpdateStatusCache.RUnlock()
+		return serverUpdateStatusCache.initialized
+	}
+	return false
+}
+
+func getServerUpdateStatusFromCache(hostname string) []tc.ServerUpdateStatusV40 {
+	serverUpdateStatusCache.RLock()
+	defer serverUpdateStatusCache.RUnlock()
+	return serverUpdateStatusCache.serverMap[hostname]
+}
+
+var once = sync.Once{}
+
+func InitServerUpdateStatusCache(interval time.Duration, db *sql.DB, timeout time.Duration) {
+	once.Do(func() {
+		if interval <= 0 {
+			return
+		}
+		serverUpdateStatusCache.enabled = true
+		refreshServerUpdateStatusCache(db, timeout)
+		startServerUpdateStatusCacheRefresher(interval, db, timeout)
+	})
+}
+
+func startServerUpdateStatusCacheRefresher(interval time.Duration, db *sql.DB, timeout time.Duration) {
+	go func() {
+		for {
+			time.Sleep(interval)
+			refreshServerUpdateStatusCache(db, timeout)
+		}
+	}()
+}
+
+func refreshServerUpdateStatusCache(db *sql.DB, timeout time.Duration) {
+	newServerUpdateStatuses, err := getServerUpdateStatuses(db, timeout)
+	if err != nil {
+		log.Errorf("refreshing server update status cache: %s", err.Error())
+		return
+	}
+	serverUpdateStatusCache.Lock()
+	defer serverUpdateStatusCache.Unlock()
+	serverUpdateStatusCache.serverMap = newServerUpdateStatuses
+	serverUpdateStatusCache.initialized = true
+	log.Infof("refreshed server update status cache (len = %d)", len(serverUpdateStatusCache.serverMap))
+}
+
+type serverInfo struct {
+	id               int
+	hostName         string
+	typeName         string
+	cdnId            int
+	status           string
+	cachegroup       int
+	configUpdateTime *time.Time
+	configApplyTime  *time.Time
+	revalUpdateTime  *time.Time
+	revalApplyTime   *time.Time
+}
+
+const getUseRevalPendingQuery = `
+	SELECT value::BOOLEAN
+	FROM parameter
+	WHERE name = 'use_reval_pending' AND config_file = 'global'
+	UNION ALL SELECT FALSE FETCH FIRST 1 ROW ONLY
+`
+
+const getServerInfoQuery = `
+	SELECT
+		s.id,
+		s.host_name,
+		t.name,
+		s.cdn_id,
+		st.name,
+		s.cachegroup,
+		s.config_update_time,
+		s.config_apply_time,
+		s.revalidate_update_time,
+		s.revalidate_apply_time
+	FROM server s
+	JOIN type t ON t.id = s.type
+	JOIN status st ON st.id = s.status
+`
+
+const getCacheGroupsQuery = `
+	SELECT
+		c.id,
+		c.parent_cachegroup_id,
+		c.secondary_parent_cachegroup_id
+	FROM cachegroup c
+`
+
+const getTopologyCacheGroupParentsQuery = `
+	SELECT
+		cg_child.id,
+		ARRAY_AGG(DISTINCT cg_parent.id)
+	FROM topology_cachegroup_parents tcp
+	JOIN topology_cachegroup tc_child ON tc_child.id = tcp.child
+	JOIN cachegroup cg_child ON cg_child.name = tc_child.cachegroup
+	JOIN topology_cachegroup tc_parent ON tc_parent.id = tcp.parent
+	JOIN cachegroup cg_parent ON cg_parent.name = tc_parent.cachegroup
+	GROUP BY cg_child.id
+`
+
+func getServerUpdateStatuses(db *sql.DB, timeout time.Duration) (map[string][]tc.ServerUpdateStatusV40, error) {
+	dbCtx, dbClose := context.WithTimeout(context.Background(), timeout)
+	defer dbClose()
+	serversByID := make(map[int]serverInfo)
+	updatePendingByCDNCachegroup := make(map[int]map[int]bool)
+	revalPendingByCDNCachegroup := make(map[int]map[int]bool)
+	tx, err := db.BeginTx(dbCtx, nil)
+	if err != nil {
+		return nil, fmt.Errorf("beginning server update status transaction: %w", err)
+	}
+	defer func() {
+		if err := tx.Commit(); err != nil && err != sql.ErrTxDone {
+			log.Errorln("committing server update status transaction: " + err.Error())
+		}
+	}()
+
+	useRevalPending := false
+	if err := tx.QueryRowContext(dbCtx, getUseRevalPendingQuery).Scan(&useRevalPending); err != nil {
+		return nil, fmt.Errorf("querying use_reval_pending param: %w", err)
+	}
+
+	// get all servers and build map of update/revalPending by cachegroup+CDN
+	serverRows, err := tx.QueryContext(dbCtx, getServerInfoQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying servers: %w", err)
+	}
+	defer log.Close(serverRows, "closing server rows")
+	for serverRows.Next() {
+		s := serverInfo{}
+		if err := serverRows.Scan(&s.id, &s.hostName, &s.typeName, &s.cdnId, &s.status, &s.cachegroup, &s.configUpdateTime, &s.configApplyTime, &s.revalUpdateTime, &s.revalApplyTime); err != nil {
+			return nil, fmt.Errorf("scanning servers: %w", err)
+		}
+		serversByID[s.id] = s
+		if _, ok := updatePendingByCDNCachegroup[s.cdnId]; !ok {
+			updatePendingByCDNCachegroup[s.cdnId] = make(map[int]bool)
+		}
+		if _, ok := revalPendingByCDNCachegroup[s.cdnId]; !ok {
+			revalPendingByCDNCachegroup[s.cdnId] = make(map[int]bool)
+		}
+		status := tc.CacheStatusFromString(s.status)
+		if tc.IsValidCacheType(s.typeName) && (status == tc.CacheStatusOnline || status == tc.CacheStatusReported || status == tc.CacheStatusAdminDown) {
+			if s.configUpdateTime.After(*s.configApplyTime) {
+				updatePendingByCDNCachegroup[s.cdnId][s.cachegroup] = true
+			}
+			if s.revalUpdateTime.After(*s.revalApplyTime) {
+				revalPendingByCDNCachegroup[s.cdnId][s.cachegroup] = true
+			}
+		}
+	}
+	if err := serverRows.Err(); err != nil {
+		return nil, fmt.Errorf("iterating over server rows: %w", err)
+	}
+
+	// get all legacy cachegroup parents
+	cacheGroupParents := make(map[int]map[int]struct{})
+	cacheGroupRows, err := tx.QueryContext(dbCtx, getCacheGroupsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying cachegroups: %w", err)
+	}
+	defer log.Close(cacheGroupRows, "closing cachegroup rows")
+	for cacheGroupRows.Next() {
+		id := 0
+		parentID := new(int)
+		secondaryParentID := new(int)
+		if err := cacheGroupRows.Scan(&id, &parentID, &secondaryParentID); err != nil {
+			return nil, fmt.Errorf("scanning cachegroups: %w", err)
+		}
+		cacheGroupParents[id] = make(map[int]struct{})
+		if parentID != nil {
+			cacheGroupParents[id][*parentID] = struct{}{}
+		}
+		if secondaryParentID != nil {
+			cacheGroupParents[id][*secondaryParentID] = struct{}{}
+		}
+	}
+	if err := cacheGroupRows.Err(); err != nil {
+		return nil, fmt.Errorf("iterating over cachegroup rows: %w", err)
+	}
+
+	// get all topology-based cachegroup parents
+	topologyCachegroupRows, err := tx.QueryContext(dbCtx, getTopologyCacheGroupParentsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying topology cachegroups: %w", err)
+	}
+	defer log.Close(topologyCachegroupRows, "closing topology cachegroup rows")
+	for topologyCachegroupRows.Next() {
+		id := 0
+		parents := []int32{}
+		if err := topologyCachegroupRows.Scan(&id, pq.Array(&parents)); err != nil {
+			return nil, fmt.Errorf("scanning topology cachegroup rows: %w", err)
+		}
+		for _, p := range parents {
+			cacheGroupParents[id][int(p)] = struct{}{}
+		}
+	}
+	if err = topologyCachegroupRows.Err(); err != nil {
+		return nil, fmt.Errorf("iterating over topology cachegroup rows: %w", err)
+	}
+
+	serverUpdateStatuses := make(map[string][]tc.ServerUpdateStatusV40, len(serversByID))
+	for serverID, server := range serversByID {
+		updateStatus := tc.ServerUpdateStatusV40{
+			HostName:             server.hostName,
+			UpdatePending:        server.configUpdateTime.After(*server.configApplyTime),
+			RevalPending:         server.revalUpdateTime.After(*server.revalApplyTime),
+			UseRevalPending:      useRevalPending,
+			HostId:               serverID,
+			Status:               server.status,
+			ParentPending:        getParentPending(cacheGroupParents[server.cachegroup], updatePendingByCDNCachegroup[server.cdnId]),
+			ParentRevalPending:   getParentPending(cacheGroupParents[server.cachegroup], revalPendingByCDNCachegroup[server.cdnId]),
+			ConfigUpdateTime:     server.configUpdateTime,
+			ConfigApplyTime:      server.configApplyTime,
+			RevalidateUpdateTime: server.revalUpdateTime,
+			RevalidateApplyTime:  server.revalApplyTime,
+		}
+		serverUpdateStatuses[server.hostName] = append(serverUpdateStatuses[server.hostName], updateStatus)
+	}
+	return serverUpdateStatuses, nil
+}
+
+func getParentPending(parents map[int]struct{}, pendingByCacheGroup map[int]bool) bool {
+	for parent := range parents {
+		if pendingByCacheGroup[parent] {
+			return true
+		}
+	}
+	return false
+}
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
index bdcbf4e91a..0d0ce5e7ef 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
@@ -26,7 +26,7 @@ import (
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+
 	"github.com/jmoiron/sqlx"
 	sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
@@ -58,7 +58,7 @@ func TestGetServerUpdateStatus(t *testing.T) {
 	}
 	defer tx.Commit()
 
-	result, err, _ := getServerUpdateStatus(tx, &config.Config{ConfigTrafficOpsGolang: config.ConfigTrafficOpsGolang{DBQueryTimeoutSeconds: 20}}, "host_name_1")
+	result, err, _ := getServerUpdateStatus(tx, "host_name_1")
 	if err != nil {
 		t.Errorf("getServerUpdateStatus: %v", err)
 	}
@@ -75,3 +75,130 @@ func TestGetServerUpdateStatus(t *testing.T) {
 
 	reflect.DeepEqual(expected, result)
 }
+
+func TestGetServerUpdateStatuses(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	mock.ExpectBegin()
+	revalPendingRows := sqlmock.NewRows([]string{"value"})
+	revalPendingRows.AddRow(true)
+	mock.ExpectQuery("SELECT").WillReturnRows(revalPendingRows)
+
+	serverInfoRows := sqlmock.NewRows([]string{"id", "host_name", "type", "cdn_id", "status",
+		"cachegroup", "config_update_time", "config_apply_time", "revalidate_update_time",
+		"revalidate_apply_time"})
+	tenSecAfter := time.UnixMilli(10000)
+	epoch := time.UnixMilli(0)
+	serverInfoRows.AddRow(1, "edge1", tc.CacheTypeEdge.String(), 1, tc.CacheStatusReported.String(), 1, tenSecAfter, tenSecAfter, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(2, "mid1", tc.CacheTypeMid.String(), 1, tc.CacheStatusReported.String(), 2, tenSecAfter, epoch, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(3, "edge2", tc.CacheTypeEdge.String(), 2, tc.CacheStatusReported.String(), 1, tenSecAfter, tenSecAfter, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(4, "mid2", tc.CacheTypeMid.String(), 2, tc.CacheStatusReported.String(), 2, tenSecAfter, tenSecAfter, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(5, "mid3", tc.CacheTypeMid.String(), 2, tc.CacheStatusReported.String(), 3, tenSecAfter, tenSecAfter, tenSecAfter, epoch)
+	mock.ExpectQuery("SELECT").WillReturnRows(serverInfoRows)
+
+	cachegroupRows := sqlmock.NewRows([]string{"id", "parent_cachegroup_id", "secondary_parent_cachegroup_id"})
+	cachegroupRows.AddRow(1, 2, nil)
+	cachegroupRows.AddRow(2, nil, nil)
+	cachegroupRows.AddRow(3, nil, nil)
+	mock.ExpectQuery("SELECT").WillReturnRows(cachegroupRows)
+
+	topologyCachegroupRows := sqlmock.NewRows([]string{"id", "array_agg"})
+	topologyCachegroupRows.AddRow(1, "{3}")
+	mock.ExpectQuery("SELECT").WillReturnRows(topologyCachegroupRows)
+
+	mock.ExpectCommit()
+
+	expected := map[string][]tc.ServerUpdateStatusV40{
+		"edge1": {
+			{
+				HostName:             "edge1",
+				UpdatePending:        false,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               1,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        true,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"mid1": {
+			{
+				HostName:             "mid1",
+				UpdatePending:        true,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               2,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &epoch,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"edge2": {
+			{
+				HostName:             "edge2",
+				UpdatePending:        false,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               3,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   true,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"mid2": {
+			{
+				HostName:             "mid2",
+				UpdatePending:        false,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               4,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"mid3": {
+			{
+				HostName:             "mid3",
+				UpdatePending:        false,
+				RevalPending:         true,
+				UseRevalPending:      true,
+				HostId:               5,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &epoch,
+			},
+		},
+	}
+	actual, err := getServerUpdateStatuses(mockDB, 20*time.Second)
+	if err != nil {
+		t.Fatalf("unexpected error getting server update statuses: %s", err)
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("getting server update statuses - expected: %+v, actual: %+v", expected, actual)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
index 42a3acf45e..60b9b4c2ad 100644
--- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
+++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
@@ -41,6 +41,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/plugin"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/server"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault"
 	_ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault/backends" // init traffic vault backends
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled"
@@ -146,6 +147,7 @@ func main() {
 	db.SetConnMaxLifetime(time.Duration(cfg.DBConnMaxLifetimeSeconds) * time.Second)
 
 	auth.InitUsersCache(time.Duration(cfg.UserCacheRefreshIntervalSec)*time.Second, db.DB, time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
+	server.InitServerUpdateStatusCache(time.Duration(cfg.ServerUpdateStatusCacheRefreshIntervalSec)*time.Second, db.DB, time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
 
 	trafficVault := setupTrafficVault(*riakConfigFileName, &cfg)
 
@@ -186,7 +188,7 @@ func main() {
 
 	log.Infof("Listening on " + cfg.Port)
 
-	server := &http.Server{
+	httpServer := &http.Server{
 		Addr:              ":" + cfg.Port,
 		TLSConfig:         cfg.TLSConfig,
 		ReadTimeout:       time.Duration(cfg.ReadTimeout) * time.Second,
@@ -195,11 +197,11 @@ func main() {
 		IdleTimeout:       time.Duration(cfg.IdleTimeout) * time.Second,
 		ErrorLog:          log.Error,
 	}
-	if server.TLSConfig == nil {
-		server.TLSConfig = &tls.Config{}
+	if httpServer.TLSConfig == nil {
+		httpServer.TLSConfig = &tls.Config{}
 	}
 	// Deprecated in 5.0
-	server.TLSConfig.InsecureSkipVerify = cfg.Insecure
+	httpServer.TLSConfig.InsecureSkipVerify = cfg.Insecure
 	// end deprecated block
 
 	go func() {
@@ -226,8 +228,8 @@ func main() {
 		} else {
 			file.Close()
 		}
-		server.Handler = mux
-		if err := server.ListenAndServeTLS(cfg.CertPath, cfg.KeyPath); err != nil {
+		httpServer.Handler = mux
+		if err := httpServer.ListenAndServeTLS(cfg.CertPath, cfg.KeyPath); err != nil {
 			log.Errorf("stopping server: %v\n", err)
 			os.Exit(1)
 		}