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 }