81 lines
3.6 KiB
Go
81 lines
3.6 KiB
Go
package jobs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/riverqueue/river"
|
|
)
|
|
|
|
// Two properties of the queue that are one line each to write and expensive to get wrong, because
|
|
// both fail SILENTLY: a renamed kind orphans every job already in the table (River dispatches by
|
|
// that string, and a job whose kind no worker claims simply waits forever), and a MaxAttempts above
|
|
// one turns "start a run" into "start it again" — the reflex this zone deliberately refuses, since
|
|
// a retry here spawns a SECOND engine rather than finishing lost work.
|
|
func TestTheQueuesKindsAndRetryPolicyAreWhatTheRestOfTheSystemAssumes(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
kind string
|
|
opts river.InsertOpts
|
|
}{
|
|
{"spawn", SpawnArgs{}.Kind(), SpawnArgs{}.InsertOpts()},
|
|
{"parse", ParseArgs{}.Kind(), ParseArgs{}.InsertOpts()},
|
|
{"export", ExportArgs{}.Kind(), ExportArgs{}.InsertOpts()},
|
|
}
|
|
want := map[string]string{"spawn": "tm_spawn_run", "parse": "tm_parse_book", "export": "tm_build_export"}
|
|
for _, tc := range cases {
|
|
if tc.kind != want[tc.name] {
|
|
t.Errorf("%s kind is %q, want %q — a renamed kind orphans the jobs already queued under the old one",
|
|
tc.name, tc.kind, want[tc.name])
|
|
}
|
|
if tc.opts.MaxAttempts != 1 {
|
|
t.Errorf("%s MaxAttempts is %d, want 1: recovery is the reconciler's job, not the queue's",
|
|
tc.name, tc.opts.MaxAttempts)
|
|
}
|
|
if tc.opts.Queue != river.QueueDefault {
|
|
t.Errorf("%s queue is %q, want the default one the client works", tc.name, tc.opts.Queue)
|
|
}
|
|
}
|
|
}
|
|
|
|
// stubParser answers with whatever the case wants, so the worker's own decision is what is judged.
|
|
type stubParser struct{ err error }
|
|
|
|
func (p stubParser) Parse(context.Context, string) error { return p.err }
|
|
|
|
// A pass that established nothing gets its job BACK; anything else the pass says is the job's answer.
|
|
//
|
|
// ⛔ The distinction is the whole of it, and it exists because MaxAttempts is 1 (above). A returned
|
|
// error consumes the single attempt outright, and the book then waits out the intake sweep's whole
|
|
// grace — twenty minutes — over a host that was busy for a moment. A snooze does not increment the
|
|
// attempt, so it is the SAME recovery coming back rather than the second mechanism this queue's
|
|
// policy refuses. Nothing witnessed the mapping until this test: the service's own tests end at the
|
|
// error it returns, and the queue's end at the options it declares.
|
|
func TestAPassThatEstablishedNothingGetsItsJobBackInsteadOfSpendingIt(t *testing.T) {
|
|
failed := errors.New("the store could not be reached")
|
|
for _, c := range []struct {
|
|
what string
|
|
err error
|
|
snooze bool
|
|
}{
|
|
{"a pass that established nothing", fmt.Errorf("%w: the host is busy", ErrTryAgainLater), true},
|
|
{"a pass that failed for its own reasons", failed, false},
|
|
{"a pass that finished", nil, false},
|
|
} {
|
|
w := &parseWorker{svc: stubParser{err: c.err}}
|
|
got := w.Work(t.Context(), &river.Job[ParseArgs]{Args: ParseArgs{BookID: "bk_1"}})
|
|
var snoozed *river.JobSnoozeError
|
|
switch {
|
|
case c.snooze && !errors.As(got, &snoozed):
|
|
t.Errorf("%s: the worker answered %v, want a snooze — this job is the book's only one, and spending it costs the sweep's whole grace", c.what, got)
|
|
case c.snooze && snoozed.Duration != RetryDelay:
|
|
t.Errorf("%s: the job comes back in %v, want %v", c.what, snoozed.Duration, RetryDelay)
|
|
case !c.snooze && errors.As(got, &snoozed):
|
|
t.Errorf("%s: the worker snoozed, so a job that is genuinely finished or genuinely broken would be offered again forever", c.what)
|
|
case !c.snooze && !errors.Is(got, c.err):
|
|
t.Errorf("%s: the worker answered %v, want the pass's own %v", c.what, got, c.err)
|
|
}
|
|
}
|
|
}
|