feat(11-01): run job workers in serve and queue:work, add queue:clear
- Manager gains the apparatus JobManager surface: StartJob, UpdateJobState, UpdateMetadata, FailJob, CancelJob (is_canceled + STOPPED + River JobCancel), StopJob (STOPPED only), CheckIfCanceled and GetMetadata, all raw column writes so updated_at is untouched - serve starts the in-process worker unless queue.work_in_serve is false and stops it on shutdown; an app without jobs gets an idle worker - queue:work runs a foreground worker with repeatable --queue filters; queue:clear deletes available, scheduled and retryable jobs of one queue - the generated main appends conga.RuntimeCommands; summer delegates queue:work and queue:clear; make:job scaffolds a conga.Job
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/riverqueue/river/rivertype"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -256,6 +257,124 @@ func (m *Manager) CompleteJob(ctx context.Context, id uint, metadata map[string]
|
||||
return m.updateColumns(ctx, gdb, id, cols)
|
||||
}
|
||||
|
||||
// StartJob sets progress to 0, progress_max to total and updated_at to now.
|
||||
func (m *Manager) StartJob(ctx context.Context, id uint, total int) error {
|
||||
gdb, err := m.gdb()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.updateColumns(ctx, gdb, id, map[string]any{"progress": 0, "progress_max": total, "updated_at": time.Now()})
|
||||
}
|
||||
|
||||
// UpdateJobState sets progress only, then replaces metadata when it is
|
||||
// non-empty. updated_at is not touched.
|
||||
func (m *Manager) UpdateJobState(ctx context.Context, id uint, current int, metadata map[string]any) error {
|
||||
gdb, err := m.gdb()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.updateColumns(ctx, gdb, id, map[string]any{"progress": current}); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
return m.UpdateMetadata(ctx, id, metadata)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateMetadata replaces the row's metadata. updated_at is not touched.
|
||||
func (m *Manager) UpdateMetadata(ctx context.Context, id uint, metadata map[string]any) error {
|
||||
gdb, err := m.gdb()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc, err := encodeMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.updateColumns(ctx, gdb, id, map[string]any{"metadata": enc})
|
||||
}
|
||||
|
||||
// FailJob sets StatusError, replacing metadata only when it is non-empty.
|
||||
// Job functions normally just return an error; the worker records
|
||||
// StatusError on the final attempt.
|
||||
func (m *Manager) FailJob(ctx context.Context, id uint, metadata map[string]any) error {
|
||||
return m.setStatus(ctx, id, StatusError, metadata)
|
||||
}
|
||||
|
||||
// StopJob sets StatusStopped only, the WinterCMS cancelJob semantics. A job
|
||||
// calls it on its own row after CheckIfCanceled reports true, then returns.
|
||||
// It neither sets is_canceled nor touches the River job.
|
||||
func (m *Manager) StopJob(ctx context.Context, id uint, metadata map[string]any) error {
|
||||
return m.setStatus(ctx, id, StatusStopped, metadata)
|
||||
}
|
||||
|
||||
// CancelJob cancels a job from outside it: it sets is_canceled and
|
||||
// StatusStopped in one update, then cancels the River job, so a queued job
|
||||
// never starts and a running job's ctx is cancelled. A River job that has
|
||||
// already finished or been removed is not an error.
|
||||
func (m *Manager) CancelJob(ctx context.Context, id uint) error {
|
||||
gdb, err := m.gdb()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.updateColumns(ctx, gdb, id, map[string]any{"is_canceled": true, "status": int(StatusStopped)}); err != nil {
|
||||
return err
|
||||
}
|
||||
rec, err := m.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rec.RiverJobID == nil {
|
||||
return nil
|
||||
}
|
||||
client, err := m.insertClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := client.JobCancel(ctx, *rec.RiverJobID); err != nil && !errors.Is(err, rivertype.ErrNotFound) {
|
||||
return fmt.Errorf("conga: cancel river job %d: %w", *rec.RiverJobID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckIfCanceled reports the row's is_canceled flag. Long jobs call it
|
||||
// between items and stop with StopJob when it is true.
|
||||
func (m *Manager) CheckIfCanceled(ctx context.Context, id uint) (bool, error) {
|
||||
rec, err := m.Get(ctx, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return rec.IsCanceled, nil
|
||||
}
|
||||
|
||||
// GetMetadata decodes the row's metadata. Anything that is not a non-empty
|
||||
// JSON object, including the "" written by a dispatch without metadata,
|
||||
// decodes to an empty map.
|
||||
func (m *Manager) GetMetadata(ctx context.Context, id uint) (map[string]any, error) {
|
||||
gdb, err := m.gdb()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.metadata(ctx, gdb, id)
|
||||
}
|
||||
|
||||
func (m *Manager) setStatus(ctx context.Context, id uint, status Status, metadata map[string]any) error {
|
||||
gdb, err := m.gdb()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cols := map[string]any{"status": int(status)}
|
||||
if len(metadata) > 0 {
|
||||
enc, err := encodeMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cols["metadata"] = enc
|
||||
}
|
||||
return m.updateColumns(ctx, gdb, id, cols)
|
||||
}
|
||||
|
||||
// Get returns the summer_jobs row with id.
|
||||
func (m *Manager) Get(ctx context.Context, id uint) (Record, error) {
|
||||
gdb, err := m.gdb()
|
||||
|
||||
Reference in New Issue
Block a user