github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/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 "context" 23 "encoding/json" 24 "fmt" 25 stdio "io" 26 "net/http" 27 "sort" 28 "sync" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 33 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 34 "sigs.k8s.io/prow/pkg/io" 35 ) 36 37 // Mock out time for unit testing. 38 var now = time.Now 39 40 // History uses a `*recordLog` per pool to store a record of recent actions that 41 // Tide has taken. Using a log per pool ensure that history is retained 42 // for inactive pools even if other pools are very active. 43 type History struct { 44 logs map[string]*recordLog 45 sync.Mutex 46 logSizeLimit int 47 48 opener opener 49 path string 50 } 51 52 // opener has methods to read and write paths 53 type opener interface { 54 Reader(ctx context.Context, path string) (io.ReadCloser, error) 55 Writer(ctx context.Context, path string, opts ...io.WriterOptions) (io.WriteCloser, error) 56 } 57 58 func readHistory(maxRecordsPerKey int, opener opener, path string) (map[string]*recordLog, error) { 59 reader, err := opener.Reader(context.Background(), path) 60 if io.IsNotExist(err) { // No history exists yet. This is not an error. 61 return map[string]*recordLog{}, nil 62 } 63 if err != nil { 64 return nil, fmt.Errorf("open: %w", err) 65 } 66 defer io.LogClose(reader) 67 raw, err := stdio.ReadAll(reader) 68 if err != nil { 69 return nil, fmt.Errorf("read: %w", err) 70 } 71 var recordsByPool map[string][]*Record 72 if err := json.Unmarshal(raw, &recordsByPool); err != nil { 73 return nil, fmt.Errorf("unmarshal: %w", err) 74 } 75 76 // Load records into a new recordLog map. 77 logsByPool := make(map[string]*recordLog, len(recordsByPool)) 78 for poolKey, records := range recordsByPool { 79 logsByPool[poolKey] = newRecordLog(maxRecordsPerKey) 80 limit := maxRecordsPerKey 81 if len(records) < limit { 82 limit = len(records) 83 } 84 for i := limit - 1; i >= 0; i-- { 85 logsByPool[poolKey].add(records[i]) 86 } 87 } 88 return logsByPool, nil 89 } 90 91 func writeHistory(opener opener, path string, hist map[string][]*Record) error { 92 // a write's duration will scale with the volume of data to write but large 93 // data sets can finish in about 500ms; a timeout of 30s should not evict 94 // well-behaved writes 95 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 96 defer cancel() 97 writer, err := opener.Writer(ctx, path) 98 if err != nil { 99 return fmt.Errorf("open: %w", err) 100 } 101 b, err := json.Marshal(hist) 102 if err != nil { 103 return fmt.Errorf("marshal: %w", err) 104 } 105 if _, err := fmt.Fprint(writer, string(b)); err != nil { 106 io.LogClose(writer) 107 return fmt.Errorf("write: %w", err) 108 } 109 if err := writer.Close(); err != nil { 110 return fmt.Errorf("close: %w", err) 111 } 112 return nil 113 } 114 115 // Record is an entry describing one action that Tide has taken (e.g. TRIGGER or MERGE). 116 type Record struct { 117 Time time.Time `json:"time"` 118 Action string `json:"action"` 119 BaseSHA string `json:"baseSHA,omitempty"` 120 Target []prowapi.Pull `json:"target,omitempty"` 121 Err string `json:"err,omitempty"` 122 TenantIDs []string `json:"tenantids"` 123 } 124 125 // New creates a new History struct with the specificed recordLog size limit. 126 func New(maxRecordsPerKey int, opener io.Opener, path string) (*History, error) { 127 hist := &History{ 128 logs: map[string]*recordLog{}, 129 logSizeLimit: maxRecordsPerKey, 130 opener: opener, 131 path: path, 132 } 133 134 if path != "" { 135 // Load existing history from GCS. 136 var err error 137 start := time.Now() 138 hist.logs, err = readHistory(maxRecordsPerKey, hist.opener, hist.path) 139 if err != nil { 140 return nil, err 141 } 142 logrus.WithFields(logrus.Fields{ 143 "duration": time.Since(start).String(), 144 "path": hist.path, 145 }).Debugf("Successfully read action history for %d pools.", len(hist.logs)) 146 } 147 148 return hist, nil 149 } 150 151 // Record appends an entry to the recordlog specified by the poolKey. 152 func (h *History) Record(poolKey, action, baseSHA, err string, targets []prowapi.Pull, tenantIDs []string) { 153 t := now() 154 sort.Sort(ByNum(targets)) 155 h.addRecord( 156 poolKey, 157 &Record{ 158 Time: t, 159 Action: action, 160 BaseSHA: baseSHA, 161 Target: targets, 162 Err: err, 163 TenantIDs: tenantIDs, 164 }, 165 ) 166 } 167 168 func (h *History) addRecord(poolKey string, rec *Record) { 169 h.Lock() 170 defer h.Unlock() 171 if _, ok := h.logs[poolKey]; !ok { 172 h.logs[poolKey] = newRecordLog(h.logSizeLimit) 173 } 174 h.logs[poolKey].add(rec) 175 } 176 177 // ServeHTTP serves a JSON mapping from pool key -> sorted records for the pool. 178 func (h *History) ServeHTTP(w http.ResponseWriter, r *http.Request) { 179 b, err := json.Marshal(h.AllRecords()) 180 if err != nil { 181 logrus.WithError(err).Error("Encoding JSON history.") 182 b = []byte("{}") 183 } 184 if _, err = w.Write(b); err != nil { 185 logrus.WithError(err).Debug("Writing JSON history response.") 186 } 187 } 188 189 // Flush writes the action history to persistent storage if configured to do so. 190 func (h *History) Flush() { 191 if h.path == "" { 192 return 193 } 194 records := h.AllRecords() 195 start := time.Now() 196 err := writeHistory(h.opener, h.path, records) 197 log := logrus.WithFields(logrus.Fields{ 198 "duration": time.Since(start).String(), 199 "path": h.path, 200 }) 201 if err != nil { 202 log.WithError(err).Error("Error flushing action history to GCS.") 203 } else { 204 log.Debugf("Successfully flushed action history for %d pools.", len(h.logs)) 205 } 206 } 207 208 // AllRecords generates a map from pool key -> sorted records for the pool. 209 func (h *History) AllRecords() map[string][]*Record { 210 h.Lock() 211 defer h.Unlock() 212 213 res := make(map[string][]*Record, len(h.logs)) 214 for key, log := range h.logs { 215 res[key] = log.toSlice() 216 } 217 return res 218 } 219 220 // recordLog is a space efficient, limited size, append only list. 221 type recordLog struct { 222 buff []*Record 223 head int 224 limit int 225 226 // cachedSlice is the cached, in-order slice. Use toSlice(), don't access directly. 227 // We cache this value because most pools don't change between sync loops. 228 cachedSlice []*Record 229 } 230 231 func newRecordLog(sizeLimit int) *recordLog { 232 return &recordLog{ 233 head: -1, 234 limit: sizeLimit, 235 } 236 } 237 238 func (rl *recordLog) add(rec *Record) { 239 // Start by invalidating cached slice. 240 rl.cachedSlice = nil 241 242 rl.head = (rl.head + 1) % rl.limit 243 if len(rl.buff) < rl.limit { 244 // The log is not yet full. Append the record. 245 rl.buff = append(rl.buff, rec) 246 } else { 247 // The log is full. Overwrite the oldest record. 248 rl.buff[rl.head] = rec 249 } 250 } 251 252 func (rl *recordLog) toSlice() []*Record { 253 if rl.cachedSlice != nil { 254 return rl.cachedSlice 255 } 256 257 res := make([]*Record, 0, len(rl.buff)) 258 for i := 0; i < len(rl.buff); i++ { 259 index := (rl.limit + rl.head - i) % rl.limit 260 res = append(res, rl.buff[index]) 261 } 262 rl.cachedSlice = res 263 return res 264 } 265 266 // ByNum implements sort.Interface for []PRMeta to sort by ascending PR number. 267 type ByNum []prowapi.Pull 268 269 func (prs ByNum) Len() int { return len(prs) } 270 func (prs ByNum) Swap(i, j int) { prs[i], prs[j] = prs[j], prs[i] } 271 func (prs ByNum) Less(i, j int) bool { return prs[i].Number < prs[j].Number }