Initial push
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user