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 }