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 }