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 <[email protected]>
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)
                }

Reply via email to