wasp-lang / wasp

The fastest way to develop full-stack web apps with React & Node.js.
https://wasp-lang.dev
MIT License
13.73k stars 1.18k forks source link

RFC: Support for background jobs - Phase 1 #530

Closed shayneczyzewski closed 2 years ago

shayneczyzewski commented 2 years ago

Support for background jobs

Why do developers need background jobs? We often want to perform some work off of the web server’s main request-response loop, which may include one or more of the following:

In short, we do not want our web server handling this, so we need another way. While users could cobble together ad-hoc solutions, Wasp does not currently offer DSL support to reduce this burden - yet!

This document outlines our near-term and long-term plans for supporting background jobs.

Requirements

Implementation

Phase 1

The goal of this phase is to tackle the basic syntax, learn, iterate, and then plan for how to attack the future phases.

Step 1 - Async job DSL support

Goals:

job SendSignupEmail {
  perform: import perform from "@ext/jobs/sendSignupEmail.js"
}
import SendSignupEmail from '@wasp/jobs/SendSignupEmail'

// This becomes an async-wrapped function call behind the scenes.
const res1 = SendSignupEmail.performAsync({ something: "here" })
res1.result().then(res => { ... })

// This becomes an async sleep, followed by an async-wrapped function call behind the scenes.
const res2 = SendSignupEmail.delay(time).performAsync(args)

// NOTE: In the future res2 would be an opaque, transferrable job handle
// to get status info at any time.

Step 2: Cron job DSL support

Goals:

job SendInternalStatsEmail {
  perform: import perform from "@ext/jobs/sendInternalStatsEmail.js",
  repeat: {
    cron: "0 2 * * *",
    /* cron: { hour: 2, ... }, cron can be a String or Object */
    argument: {=json "foo": [1, 2, 3] json=}
  }
} 

Cron Decision Point

Here we need to decide on how to implement cron support. We can pick zero or more of the following options.

Option 1) In-memory, single server option (node-cron or similar)
Option 2) Persistent, no extra infrastructure option (pg-boss or similar Node + PostgreSQL stack)
Option 3) Additional infrastructure option (celery, temporal.io, or other)

Future Phases

As we add more job processing backends, you can pick which executor your job runs on:

job SendSignupEmail {
  executor: PostgresPersisted,
  perform: import foo from "@ext/jobs/sendSignupEmail.js"
}

/* System defined runtimes. */
/* The executors below can be implicit defaults. */

executor Default {
  runtime: NodeInMemory
}

executor PostgresPersisted {
  runtime: PostgresPersisted
}

And in the future, users can even create their own custom runtimes with Docker stages:

job TensorFlowMagic {
  executor: MachineLearning,
  perform: import add from "@ext/jobs/tensorFlowMagic.py",
  afterPerformJob: ProcessTensorFlowMagicResults
}

executor MachineLearning {
  runtime: PythonML, /* User defined */
  concurrency: 10
}

runtime PythonML {
  language: Python,
  stage: {=dockerfile ... dockerfile=},
  resources: { vcpu: ..., ram: ... } /* Likely punt until `wasp deploy` */
}
shayneczyzewski commented 2 years ago

Notes from meeting Mar 30:

shayneczyzewski commented 2 years ago

We have completed Phase 1, Step 1 - Async job DSL support 🎉 . We tweaked the syntax a bit, and removed the idea of .result() that allows you to await a Promise for the job return value, as we could not envision meaningful use cases and it would complicate the design. We also introduced executor specific functionality in the SubmittedJob result (e.g., pgBoss.details()), so we would not be bound by a least common denominator design. However, your job handler functions will be portable across future executors with a single line change. 🔥

Here is what it ended up looking like:

job mySpecialJob {
  executor: PgBoss,
  perform: {
    fn: import { foo } from "@ext/jobs/bar.js",
    options: {=json { "retryLimit": 1 } json=}
  }
}
console.log('Kicking off Job...')
// Or: const submittedJob = await mySpecialJob.delay(10).submit({ something: "here" })
const submittedJob = await mySpecialJob.submit({ something: "here" })
console.log(submittedJob.jobId, submittedJob.jobName, submittedJob.executorName)
console.log("submittedJob.pgBoss.details()", await submittedJob.pgBoss.details())

PR Reference: https://github.com/wasp-lang/wasp/pull/582

Cron support via pg-boss coming in quickly next, followed by updating the docs, posting a tutorial blog post, and releasing it. 🚀

shayneczyzewski commented 2 years ago

Cron support just added via pg-boss, thus closing Phase 1. Final syntax:

job mySpecialJob {
  executor: PgBoss,
  perform: {
    fn: import { foo } from "@ext/jobs/bar.js",
    executorOptions: {
      pgBoss: {=json { "retryLimit": 1 } json=}
    }
  }
}

job mySpecialScheduledJob {
  executor: PgBoss,
  perform: {
    fn: import { foo } from "@ext/jobs/bar.js"
  },
  schedule: {
    cron: "*/2 * * * *",
    args: {=json { "foo": "bar" } json=},
    executorOptions: {
      pgBoss: {=json { "retryLimit": 2 } json=}
    }
  }
}

PR Reference: https://github.com/wasp-lang/wasp/pull/586