Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 37m ago 32290114
history blame raw 2.6 KiB
1package agent
2
3// The attempt log is streamed to the server while the job runs, over the same
4// runner channel that carries leases and heartbeats. The spool file is the source:
5// the streamer reads new bytes from it and sends chunks; the file still bounds the
6// log and still holds the step offsets.
7
8import (
9 "errors"
10 "fmt"
11 "os"
12 "time"
13)
14
15const (
16 // logChunk is how much log one message carries. The JSON encoding adds base64,
17 // and the control channel's line bound has room for this plus a record.
18 logChunk = 128 << 10
19 // logPoll is how often the streamer looks for new bytes. It is a reading
20 // latency, not a durability interval: the bytes are already in the file.
21 logPoll = 100 * time.Millisecond
22)
23
24type logStream struct {
25 s *runnerSession
26 job *runnerJob
27 log *spool
28 stopc chan struct{}
29 done chan struct{}
30 err error
31}
32
33func (s *runnerSession) startLogStream(job *runnerJob, log *spool) *logStream {
34 l := &logStream{s: s, job: job, log: log, stopc: make(chan struct{}), done: make(chan struct{})}
35 go l.run()
36 return l
37}
38
39func (l *logStream) run() {
40 defer close(l.done)
41 f, err := os.Open(l.log.Path())
42 if err != nil {
43 l.err = err
44 return
45 }
46 defer f.Close()
47 ticker := time.NewTicker(logPoll)
48 defer ticker.Stop()
49 offset := int64(0)
50 for {
51 select {
52 case <-l.stopc:
53 l.send(f, &offset) // one last pass after seal wrote the truncation note
54 return
55 case <-ticker.C:
56 if !l.send(f, &offset) {
57 return
58 }
59 }
60 }
61}
62
63func (l *logStream) send(f *os.File, offset *int64) bool {
64 size := l.log.Len()
65 for *offset < size {
66 n := min(int64(logChunk), size-*offset)
67 buf := make([]byte, n)
68 if _, err := f.ReadAt(buf, *offset); err != nil {
69 l.err = err
70 return false
71 }
72 rep, err := l.s.call(runnerRequest{
73 Op: "log", Run: l.job.ID,
74 Log: &runnerLogChunk{Attempt: l.job.Attempt, Offset: *offset, Data: buf},
75 })
76 if err != nil {
77 l.err = err
78 return false
79 }
80 if rep.Error != "" {
81 l.err = errors.New(rep.Error)
82 return false
83 }
84 if !rep.OK {
85 l.err = errors.New("the server refused the log chunk")
86 return false
87 }
88 if rep.LogSize != *offset+n {
89 l.err = fmt.Errorf("the server has %d log bytes, expected %d", rep.LogSize, *offset+n)
90 return false
91 }
92 *offset += n
93 }
94 return true
95}
96
97// drain stops after the final bytes and returns the first error. finish calls it
98// after sealing the spool and before sending the record, so the record's log size
99// describes bytes the server already has.
100func (l *logStream) drain() error {
101 l.stop()
102 return l.err
103}
104
105func (l *logStream) stop() {
106 select {
107 case <-l.stopc:
108 default:
109 close(l.stopc)
110 }
111 <-l.done
112}
113