184 lines
4.0 KiB
Go
184 lines
4.0 KiB
Go
package job
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Manager owns every job in the process.
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
jobs map[string]*Job
|
|
// keep bounds how many finished jobs are retained.
|
|
keep int
|
|
}
|
|
|
|
// NewManager creates an empty job manager.
|
|
func NewManager() *Manager {
|
|
return &Manager{jobs: map[string]*Job{}, keep: 50}
|
|
}
|
|
|
|
// ErrNotFound is returned for an unknown job id.
|
|
var ErrNotFound = errors.New("job not found")
|
|
|
|
// Run creates a job and executes fn in the background. fn receives a context
|
|
// that is canceled when the job is canceled, and the job handle for progress
|
|
// reporting.
|
|
func (m *Manager) Run(parent context.Context, kind Kind, title string, dryRun bool, fn func(context.Context, *Job) error) *Job {
|
|
j := newJob(newID(), kind, title, dryRun)
|
|
ctx, cancel := context.WithCancel(parent)
|
|
j.cancel = cancel
|
|
|
|
m.mu.Lock()
|
|
m.jobs[j.snap.ID] = j
|
|
m.mu.Unlock()
|
|
m.prune()
|
|
|
|
go func() {
|
|
defer cancel()
|
|
j.start()
|
|
err := fn(ctx, j)
|
|
if err == nil && ctx.Err() != nil {
|
|
err = context.Canceled
|
|
}
|
|
if errors.Is(err, context.Canceled) {
|
|
err = context.Canceled
|
|
}
|
|
j.finish(err)
|
|
}()
|
|
return j
|
|
}
|
|
|
|
// Get returns a job by id.
|
|
func (m *Manager) Get(id string) (*Job, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
j, ok := m.jobs[id]
|
|
if !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
return j, nil
|
|
}
|
|
|
|
// List returns every job, newest first.
|
|
func (m *Manager) List() []Snapshot {
|
|
m.mu.RLock()
|
|
jobs := make([]*Job, 0, len(m.jobs))
|
|
for _, j := range m.jobs {
|
|
jobs = append(jobs, j)
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
out := make([]Snapshot, 0, len(jobs))
|
|
for _, j := range jobs {
|
|
s := j.Snapshot()
|
|
// The list view does not need the full log.
|
|
if len(s.Log) > 5 {
|
|
s.Log = s.Log[len(s.Log)-5:]
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
sort.Slice(out, func(i, k int) bool { return out[i].CreatedAt.After(out[k].CreatedAt) })
|
|
return out
|
|
}
|
|
|
|
// Delete removes a finished job. A running job is canceled instead.
|
|
func (m *Manager) Delete(id string) error {
|
|
m.mu.Lock()
|
|
j, ok := m.jobs[id]
|
|
if !ok {
|
|
m.mu.Unlock()
|
|
return ErrNotFound
|
|
}
|
|
if !j.Snapshot().State.Terminal() {
|
|
m.mu.Unlock()
|
|
j.Cancel()
|
|
return nil
|
|
}
|
|
delete(m.jobs, id)
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// prune drops the oldest finished jobs once the retention limit is exceeded.
|
|
func (m *Manager) prune() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if len(m.jobs) <= m.keep {
|
|
return
|
|
}
|
|
type entry struct {
|
|
id string
|
|
at time.Time
|
|
}
|
|
var finished []entry
|
|
for id, j := range m.jobs {
|
|
s := j.Snapshot()
|
|
if s.State.Terminal() {
|
|
finished = append(finished, entry{id, s.CreatedAt})
|
|
}
|
|
}
|
|
sort.Slice(finished, func(i, k int) bool { return finished[i].at.Before(finished[k].at) })
|
|
for i := 0; i < len(finished) && len(m.jobs) > m.keep; i++ {
|
|
delete(m.jobs, finished[i].id)
|
|
}
|
|
}
|
|
|
|
func newID() string {
|
|
b := make([]byte, 8)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return hex.EncodeToString([]byte(time.Now().Format("150405.000000")))
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
// CountingReader wraps a reader and reports every read to a job step, which is
|
|
// how transfer progress reaches the UI.
|
|
type CountingReader struct {
|
|
R io.Reader
|
|
Job *Job
|
|
St *Step
|
|
|
|
pending int64
|
|
lastFlush time.Time
|
|
}
|
|
|
|
// NewCountingReader builds a progress-reporting reader.
|
|
func NewCountingReader(r io.Reader, j *Job, st *Step) *CountingReader {
|
|
return &CountingReader{R: r, Job: j, St: st, lastFlush: time.Now()}
|
|
}
|
|
|
|
// Read implements io.Reader, batching counter updates so a fast transfer does
|
|
// not flood subscribers with notifications.
|
|
func (c *CountingReader) Read(p []byte) (int, error) {
|
|
n, err := c.R.Read(p)
|
|
if n > 0 {
|
|
c.pending += int64(n)
|
|
if c.pending >= 4<<20 || time.Since(c.lastFlush) > 200*time.Millisecond {
|
|
c.flush()
|
|
}
|
|
}
|
|
if err != nil {
|
|
c.flush()
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
// Flush pushes any buffered byte count to the job.
|
|
func (c *CountingReader) Flush() { c.flush() }
|
|
|
|
func (c *CountingReader) flush() {
|
|
if c.pending == 0 {
|
|
return
|
|
}
|
|
c.Job.AddBytes(c.St, c.pending)
|
|
c.pending = 0
|
|
c.lastFlush = time.Now()
|
|
}
|