Architecture

Anatomy of Qron

The architecture of Qron is pretty simple. It can be simpliefied by the following diagram:

Architecture

It essentially consists of 3 main components:

  • The Qron binary which is responsible for polling the data store for available jobs in order to execute them
  • A Postgres database used as persistence layer for jobs and their execution state
  • Your app which needs to be accessible over the internet

Qron will make sure that the scheduled jobs and workflows are always run on time and they are resumed with the correct state.

What makes Qron powerful is the simple semantic required to define workflows. In fact, creating a workflow with Qron is as simple as creating any other function. The only difference will be that the workflow function, on each execution, will receive a state as on of its input arguments. The state can be anything you would like it to be (well, anything that can be serialized to json, but stil). Making easier to program custom logic that can be run over time repeatedly and reacting to specific conditions.

Stateful workflow

States

A job always have a state associated with it. The state is an indicator for Qron to know which operations can be performed on the job. The state can be one of the following:

  • READY - The job is waiting to be executed
  • RUNNING - The job is currently being executed
  • PAUSED - The job is paused and will not be executed until it is resumed
  • FAILED - The job has failed and will not be executed until it is manually retried
  • SUCCESS - The job has been executed successfully and will not be executed again unless it is manually retried

States are updated upon each job execution. The state, unless external errors occur, will always be responsibility of the serverless function. Which will decide how the workflow should look like. Making it as simple or as complex as you like.

Terminal states

Primitives are provided in order to reprogram job executions at a future date. For example, this is a sample workflow that will try to remind a user of their billing status 10 times. After 10 failed attempts, the job will be marked as FAILED and will not be executed again unless it is manually retried.

const remindq = createQueue('remindq', async ({ retry, fail, commit, state }) => {
  const attempt = state.attempts + 1;

  const hasPaid = await checkIfUserHasPaid(state.email);
  if (hasPaid) {
    // job will not be retried
    return commit();
  } 

  if (attempt >= 10) {
    // job failed too many times, will not be retried
    return fail();
  }

  // job will be retried in 1 day
  await sendReminderEmail(state.email, 'Billing reminder');
  return retry({
    ...state,
    attempts: attempt
  }).afterDays(1);
}, z.object({
  email: z.string(),
  attempts: z.number().default(0)
}));