Feature 04 task 5: Async job runner and GET /api/v1/jobs/{id} (#29)
Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com> Co-committed-by: dev_agent <dev_agent@cubecraftlabs.com>
This commit was merged in pull request #29.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/jobs"
|
||||
)
|
||||
|
||||
// TestNewJobRunnerRunsNoop proves the runner run builds can execute a job:
|
||||
// a noop job enqueued before Start, as at boot, succeeds with its result.
|
||||
func TestNewJobRunnerRunsNoop(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db, err := openStore(ctx, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
runner, kinds, err := newJobRunner(db, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id, err := kinds.builtins.Noop.Enqueue(ctx, jobs.NoopPayload{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
jobsCtx, stopJobs := context.WithCancel(ctx)
|
||||
if err := runner.Start(jobsCtx); err != nil {
|
||||
stopJobs()
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
stopJobs()
|
||||
runner.Wait()
|
||||
})
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
job, err := runner.Get(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if job.Status == jobs.StatusSucceeded || job.Status == jobs.StatusFailed {
|
||||
if job.Kind != jobs.KindNoop || job.Status != jobs.StatusSucceeded || string(job.Result) != `{"ok":true}` || job.Error != nil {
|
||||
t.Fatalf("noop job = %+v (result %s), want succeeded with {\"ok\":true}", job, job.Result)
|
||||
}
|
||||
return
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("noop job never finished; last %+v", job)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
+50
-1
@@ -18,6 +18,7 @@ import (
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/forwarded"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/frontend"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/jobs"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/secretbox"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/server"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
||||
@@ -27,6 +28,9 @@ import (
|
||||
// version is set at build time with -ldflags "-X main.version=<version>".
|
||||
var version = "dev"
|
||||
|
||||
// jobWorkers is the number of background jobs run at once.
|
||||
const jobWorkers = 2
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
@@ -63,6 +67,10 @@ func run(cfg config.Config, logger *slog.Logger) error {
|
||||
return err
|
||||
}
|
||||
settings := household.NewSettingsService(db)
|
||||
runner, _, err := newJobRunner(db, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessions, err := auth.NewSessionManager(db, cfg.SessionIdle, cfg.SessionMax)
|
||||
if err != nil {
|
||||
@@ -80,10 +88,22 @@ func run(cfg config.Config, logger *slog.Logger) error {
|
||||
return err
|
||||
}
|
||||
|
||||
router, err := server.NewRouter(logger, version, cfg.TrustedProxy, authFlow, sessions, settings, frontend.Assets())
|
||||
router, err := server.NewRouter(logger, version, cfg.TrustedProxy, authFlow, sessions, settings, runner, frontend.Assets())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start fails the jobs a previous process left running before any
|
||||
// worker runs. The workers stop, and are waited for, before the
|
||||
// database closes.
|
||||
jobsCtx, stopJobs := context.WithCancel(ctx)
|
||||
defer func() {
|
||||
stopJobs()
|
||||
runner.Wait()
|
||||
}()
|
||||
if err := runner.Start(jobsCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: cfg.HTTPAddr,
|
||||
Handler: router,
|
||||
@@ -165,6 +185,35 @@ func seedDefaults(ctx context.Context, db *store.Store, defaultTZ string) error
|
||||
return household.SeedNotificationPrefs(ctx, db)
|
||||
}
|
||||
|
||||
// jobKinds enqueues every job kind the server runs.
|
||||
type jobKinds struct {
|
||||
builtins *jobs.Builtins
|
||||
}
|
||||
|
||||
// newJobRunner returns the job runner with every job kind registered, ready
|
||||
// to Start.
|
||||
func newJobRunner(db *store.Store, logger *slog.Logger) (*jobs.Runner, *jobKinds, error) {
|
||||
runner, err := jobs.NewRunner(db, logger, jobWorkers)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
kinds, err := registerJobKinds(runner)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return runner, kinds, nil
|
||||
}
|
||||
|
||||
// registerJobKinds registers every job kind's handler on runner. Each
|
||||
// feature's job kinds are registered here.
|
||||
func registerJobKinds(runner *jobs.Runner) (*jobKinds, error) {
|
||||
builtins, err := jobs.RegisterBuiltins(runner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &jobKinds{builtins: builtins}, nil
|
||||
}
|
||||
|
||||
// logListening logs the startup line. Only the public URL's origin is logged
|
||||
// so no credential-bearing URL component can reach the log.
|
||||
func logListening(logger *slog.Logger, cfg config.Config) {
|
||||
|
||||
Reference in New Issue
Block a user