github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/tide/history/history.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package history provides an append only, size limited log of recent actions 18 // that Tide has taken for each subpool. 19 package history 20 21 import ( 22 "encoding/json" 23 "net/http" 24 "sort" 25 "sync" 26 "time" 27 28 "github.com/sirupsen/logrus" 29 ) 30 31 // Mock out time for unit testing. 32 var now = time.Now 33 34 // History uses a `*recordLog` per pool to store a record of recent actions that 35 // Tide has taken. Using a log per pool ensure that history is retained 36 // for inactive pools even if other pools are very active. 37 type History struct { 38 logs map[string]*recordLog 39 sync.Mutex 40 41 logSizeLimit int 42 } 43 44 // Record is an entry describing one action that Tide has taken (e.g. TRIGGER or MERGE). 45 type Record struct { 46 Time time.Time `json:"time"` 47 Action string `json:"action"` 48 BaseSHA string `json:"baseSHA,omitempty"` 49 Target []PRMeta `json:"target,omitempty"` 50 Err string `json:"err,omitempty"` 51 } 52 53 // PRMeta stores metadata about a PR at the time of the action. 54 type PRMeta struct { 55 Num int `json:"num"` 56 Author string `json:"author"` 57 Title string `json:"title"` 58 SHA string `json:"SHA"` 59 } 60 61 // New creates a new History struct with the specificed recordLog size limit. 62 func New(maxRecordsPerKey int) *History { 63 return &History{ 64 logs: make(map[string]*recordLog), 65 logSizeLimit: maxRecordsPerKey, 66 } 67 } 68 69 // Record appends an entry to the recordlog specified by the poolKey. 70 func (h *History) Record(poolKey, action, baseSHA, err string, targets []PRMeta) { 71 t := now() 72 sort.Sort(ByNum(targets)) 73 74 h.Lock() 75 defer h.Unlock() 76 if _, ok := h.logs[poolKey]; !ok { 77 h.logs[poolKey] = newRecordLog(h.logSizeLimit) 78 } 79 h.logs[poolKey].add(&Record{ 80 Time: t, 81 Action: action, 82 BaseSHA: baseSHA, 83 Target: targets, 84 Err: err, 85 }) 86 } 87 88 // ServeHTTP serves a JSON mapping from pool key -> sorted records for the pool. 89 func (h *History) ServeHTTP(w http.ResponseWriter, r *http.Request) { 90 b, err := json.Marshal(h.AllRecords()) 91 if err != nil { 92 logrus.WithError(err).Error("Encoding JSON history.") 93 b = []byte("{}") 94 } 95 if _, err = w.Write(b); err != nil { 96 logrus.WithError(err).Error("Writing JSON history response.") 97 } 98 } 99 100 // AllRecords generates a map from pool key -> sorted records for the pool. 101 func (h *History) AllRecords() map[string][]*Record { 102 h.Lock() 103 defer h.Unlock() 104 105 res := make(map[string][]*Record, len(h.logs)) 106 for key, log := range h.logs { 107 res[key] = log.toSlice() 108 } 109 return res 110 } 111 112 // recordLog is a space efficient, limited size, append only list. 113 type recordLog struct { 114 buff []*Record 115 head int 116 limit int 117 118 // cachedSlice is the cached, in-order slice. Use toSlice(), don't access directly. 119 // We cache this value because most pools don't change between sync loops. 120 cachedSlice []*Record 121 } 122 123 func newRecordLog(sizeLimit int) *recordLog { 124 return &recordLog{ 125 head: -1, 126 limit: sizeLimit, 127 } 128 } 129 130 func (rl *recordLog) add(rec *Record) { 131 // Start by invalidating cached slice. 132 rl.cachedSlice = nil 133 134 rl.head = (rl.head + 1) % rl.limit 135 if len(rl.buff) < rl.limit { 136 // The log is not yet full. Append the record. 137 rl.buff = append(rl.buff, rec) 138 } else { 139 // The log is full. Overwrite the oldest record. 140 rl.buff[rl.head] = rec 141 } 142 } 143 144 func (rl *recordLog) toSlice() []*Record { 145 if rl.cachedSlice != nil { 146 return rl.cachedSlice 147 } 148 149 res := make([]*Record, 0, len(rl.buff)) 150 for i := 0; i < len(rl.buff); i++ { 151 index := (rl.limit + rl.head - i) % rl.limit 152 res = append(res, rl.buff[index]) 153 } 154 rl.cachedSlice = res 155 return res 156 } 157 158 // ByNum implements sort.Interface for []PRMeta to sort by ascending PR number. 159 type ByNum []PRMeta 160 161 func (prs ByNum) Len() int { return len(prs) } 162 func (prs ByNum) Swap(i, j int) { prs[i], prs[j] = prs[j], prs[i] } 163 func (prs ByNum) Less(i, j int) bool { return prs[i].Num < prs[j].Num }