github.com/alloyci/alloy-runner@v1.0.1-0.20180222164613-925503ccafd6/network/trace.go (about)

     1  package network
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"sync"
    11  	"time"
    12  
    13  	"gitlab.com/gitlab-org/gitlab-runner/common"
    14  	"gitlab.com/gitlab-org/gitlab-runner/helpers"
    15  )
    16  
    17  type tracePatch struct {
    18  	trace  bytes.Buffer
    19  	offset int
    20  	limit  int
    21  }
    22  
    23  func (tp *tracePatch) Patch() []byte {
    24  	return tp.trace.Bytes()[tp.offset:tp.limit]
    25  }
    26  
    27  func (tp *tracePatch) Offset() int {
    28  	return tp.offset
    29  }
    30  
    31  func (tp *tracePatch) Limit() int {
    32  	return tp.limit
    33  }
    34  
    35  func (tp *tracePatch) SetNewOffset(newOffset int) {
    36  	tp.offset = newOffset
    37  }
    38  
    39  func (tp *tracePatch) ValidateRange() bool {
    40  	if tp.limit >= tp.offset {
    41  		return true
    42  	}
    43  
    44  	return false
    45  }
    46  
    47  func newTracePatch(trace bytes.Buffer, offset int) (*tracePatch, error) {
    48  	patch := &tracePatch{
    49  		trace:  trace,
    50  		offset: offset,
    51  		limit:  trace.Len(),
    52  	}
    53  
    54  	if !patch.ValidateRange() {
    55  		return nil, errors.New("Range is invalid, limit can't be less than offset")
    56  	}
    57  
    58  	return patch, nil
    59  }
    60  
    61  type clientJobTrace struct {
    62  	*io.PipeWriter
    63  
    64  	client         common.Network
    65  	config         common.RunnerConfig
    66  	jobCredentials *common.JobCredentials
    67  	id             int
    68  	bytesLimit     int
    69  	cancelFunc     context.CancelFunc
    70  
    71  	log           bytes.Buffer
    72  	lock          sync.RWMutex
    73  	state         common.JobState
    74  	failureReason common.JobFailureReason
    75  	finished      chan bool
    76  
    77  	sentTrace int
    78  	sentTime  time.Time
    79  	sentState common.JobState
    80  
    81  	updateInterval      time.Duration
    82  	forceSendInterval   time.Duration
    83  	finishRetryInterval time.Duration
    84  
    85  	failuresCollector common.FailuresCollector
    86  }
    87  
    88  func (c *clientJobTrace) Success() {
    89  	c.Fail(nil, common.NoneFailure)
    90  }
    91  
    92  func (c *clientJobTrace) Fail(err error, failureReason common.JobFailureReason) {
    93  	c.lock.Lock()
    94  
    95  	if c.state != common.Running {
    96  		c.lock.Unlock()
    97  		return
    98  	}
    99  
   100  	if err == nil {
   101  		c.state = common.Success
   102  	} else {
   103  		c.setFailure(failureReason)
   104  	}
   105  
   106  	c.lock.Unlock()
   107  	c.finish()
   108  }
   109  
   110  func (c *clientJobTrace) SetCancelFunc(cancelFunc context.CancelFunc) {
   111  	c.cancelFunc = cancelFunc
   112  }
   113  
   114  func (c *clientJobTrace) SetFailuresCollector(fc common.FailuresCollector) {
   115  	c.failuresCollector = fc
   116  }
   117  
   118  func (c *clientJobTrace) IsStdout() bool {
   119  	return false
   120  }
   121  
   122  func (c *clientJobTrace) setFailure(reason common.JobFailureReason) {
   123  	c.state = common.Failed
   124  	c.failureReason = reason
   125  	if c.failuresCollector != nil {
   126  		c.failuresCollector.RecordFailure(reason, c.config.ShortDescription())
   127  	}
   128  }
   129  
   130  func (c *clientJobTrace) start() {
   131  	reader, writer := io.Pipe()
   132  	c.PipeWriter = writer
   133  	c.finished = make(chan bool)
   134  	c.state = common.Running
   135  	go c.process(reader)
   136  	go c.watch()
   137  }
   138  
   139  func (c *clientJobTrace) finish() {
   140  	c.Close()
   141  	c.finished <- true
   142  
   143  	// Do final upload of job trace
   144  	for {
   145  		if c.fullUpdate() != common.UpdateFailed {
   146  			return
   147  		}
   148  		time.Sleep(c.finishRetryInterval)
   149  	}
   150  }
   151  
   152  func (c *clientJobTrace) writeRune(r rune) (n int, err error) {
   153  	c.lock.Lock()
   154  	defer c.lock.Unlock()
   155  
   156  	n, err = c.log.WriteRune(r)
   157  	if c.log.Len() < c.bytesLimit {
   158  		return
   159  	}
   160  
   161  	c.log.WriteString(c.limitExceededMessage())
   162  	err = io.EOF
   163  	return
   164  }
   165  
   166  func (c *clientJobTrace) process(pipe *io.PipeReader) {
   167  	defer pipe.Close()
   168  
   169  	stopped := false
   170  	reader := bufio.NewReader(pipe)
   171  
   172  	c.setupLogLimit()
   173  
   174  	for {
   175  		r, s, err := reader.ReadRune()
   176  		if s <= 0 {
   177  			break
   178  		} else if stopped {
   179  			// ignore symbols if job log exceeded limit
   180  			continue
   181  		} else if err == nil {
   182  			_, err = c.writeRune(r)
   183  			if err == io.EOF {
   184  				stopped = true
   185  			}
   186  		} else {
   187  			// ignore invalid characters
   188  			continue
   189  		}
   190  	}
   191  }
   192  
   193  func (c *clientJobTrace) incrementalUpdate() common.UpdateState {
   194  	c.lock.RLock()
   195  	state := c.state
   196  	trace := c.log
   197  	c.lock.RUnlock()
   198  
   199  	if c.sentState == state &&
   200  		c.sentTrace == trace.Len() &&
   201  		time.Since(c.sentTime) < c.forceSendInterval {
   202  		return common.UpdateSucceeded
   203  	}
   204  
   205  	if c.sentState != state {
   206  		jobInfo := common.UpdateJobInfo{
   207  			ID:            c.id,
   208  			State:         state,
   209  			FailureReason: c.failureReason,
   210  		}
   211  		c.client.UpdateJob(c.config, c.jobCredentials, jobInfo)
   212  		c.sentState = state
   213  	}
   214  
   215  	tracePatch, err := newTracePatch(trace, c.sentTrace)
   216  	if err != nil {
   217  		c.config.Log().Errorln("Error while creating a tracePatch", err.Error())
   218  	}
   219  
   220  	update := c.client.PatchTrace(c.config, c.jobCredentials, tracePatch)
   221  	if update == common.UpdateNotFound {
   222  		return update
   223  	}
   224  
   225  	if update == common.UpdateRangeMismatch {
   226  		update = c.resendPatch(c.jobCredentials.ID, c.config, c.jobCredentials, tracePatch)
   227  	}
   228  
   229  	if update == common.UpdateSucceeded {
   230  		c.sentTrace = tracePatch.Limit()
   231  		c.sentTime = time.Now()
   232  	}
   233  
   234  	return update
   235  }
   236  
   237  func (c *clientJobTrace) resendPatch(id int, config common.RunnerConfig, jobCredentials *common.JobCredentials, tracePatch common.JobTracePatch) (update common.UpdateState) {
   238  	if !tracePatch.ValidateRange() {
   239  		config.Log().Warningln(id, "Full job update is needed")
   240  		fullTrace := c.log.String()
   241  
   242  		jobInfo := common.UpdateJobInfo{
   243  			ID:            c.id,
   244  			State:         c.state,
   245  			Trace:         &fullTrace,
   246  			FailureReason: c.failureReason,
   247  		}
   248  
   249  		return c.client.UpdateJob(c.config, jobCredentials, jobInfo)
   250  	}
   251  
   252  	config.Log().Warningln(id, "Resending trace patch due to range mismatch")
   253  
   254  	update = c.client.PatchTrace(config, jobCredentials, tracePatch)
   255  	if update == common.UpdateRangeMismatch {
   256  		config.Log().Errorln(id, "Appending trace to coordinator...", "failed due to range mismatch")
   257  
   258  		return common.UpdateFailed
   259  	}
   260  
   261  	return
   262  }
   263  
   264  func (c *clientJobTrace) fullUpdate() common.UpdateState {
   265  	c.lock.RLock()
   266  	state := c.state
   267  	trace := c.log.String()
   268  	c.lock.RUnlock()
   269  
   270  	if c.sentState == state &&
   271  		c.sentTrace == len(trace) &&
   272  		time.Since(c.sentTime) < c.forceSendInterval {
   273  		return common.UpdateSucceeded
   274  	}
   275  
   276  	jobInfo := common.UpdateJobInfo{
   277  		ID:            c.id,
   278  		State:         state,
   279  		Trace:         &trace,
   280  		FailureReason: c.failureReason,
   281  	}
   282  
   283  	update := c.client.UpdateJob(c.config, c.jobCredentials, jobInfo)
   284  	if update == common.UpdateSucceeded {
   285  		c.sentTrace = len(trace)
   286  		c.sentState = state
   287  		c.sentTime = time.Now()
   288  	}
   289  
   290  	return update
   291  }
   292  
   293  func (c *clientJobTrace) abort() bool {
   294  	if c.cancelFunc != nil {
   295  		c.cancelFunc()
   296  		c.cancelFunc = nil
   297  		return true
   298  	}
   299  	return false
   300  }
   301  
   302  func (c *clientJobTrace) watch() {
   303  	for {
   304  		select {
   305  		case <-time.After(c.updateInterval):
   306  			state := c.incrementalUpdate()
   307  			if state == common.UpdateAbort && c.abort() {
   308  				<-c.finished
   309  				return
   310  			}
   311  			break
   312  
   313  		case <-c.finished:
   314  			return
   315  		}
   316  	}
   317  }
   318  
   319  func (c *clientJobTrace) setupLogLimit() {
   320  	c.bytesLimit = c.config.OutputLimit
   321  	if c.bytesLimit == 0 {
   322  		c.bytesLimit = common.DefaultOutputLimit
   323  	}
   324  	// configuration values are expressed in KB
   325  	c.bytesLimit *= 1024
   326  }
   327  
   328  func (c *clientJobTrace) limitExceededMessage() string {
   329  	return fmt.Sprintf("\n%sJob's log exceeded limit of %v bytes.%s\n", helpers.ANSI_BOLD_RED, c.bytesLimit, helpers.ANSI_RESET)
   330  }
   331  
   332  func newJobTrace(client common.Network, config common.RunnerConfig, jobCredentials *common.JobCredentials) *clientJobTrace {
   333  	return &clientJobTrace{
   334  		client:              client,
   335  		config:              config,
   336  		jobCredentials:      jobCredentials,
   337  		id:                  jobCredentials.ID,
   338  		updateInterval:      common.UpdateInterval,
   339  		forceSendInterval:   common.ForceTraceSentInterval,
   340  		finishRetryInterval: common.UpdateRetryInterval,
   341  	}
   342  }