Vapor and Job Queues: Getting Started

Using Vapor’s Redis and Queues libraries, learn how to configure, dispatch, and process various jobs in a queue. By Heidi Hermann.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Dispatching Your First Job

When a new reader signs up to your newsletter, you want to send them a welcome email to say, “Thank you”.

This is a typical example of a process that you would dispatch to a job because you don’t want the responsiveness of your webpage to depend on a third-party email API to completing.

Creating RecipientWelcomeEmailJob

In Xcode, add a new Jobs folder to your app and add a RecipientWelcomeEmailJob.swift file.

Open the file and add the following:

// 1
import Queues
import Vapor

// 2
struct WelcomeEmail: Codable {
 let to: String
 let name: String
}

// 3
struct RecipientWelcomeEmailJob: Job {
  // 4
  typealias Payload = WelcomeEmail
  // 5
  func dequeue(
    _ context: QueueContext,
    _ payload: WelcomeEmail
  ) -> EventLoopFuture<Void> {
    print("Send welcome email to \(payload.to). Greet the user as \(payload.name).")
    return context.eventLoop.future()
  }
  // 6
  func error(
    _ context: QueueContext,
    _ error: Error,
    _ payload: WelcomeEmail
  ) -> EventLoopFuture<Void> {
    return context.eventLoop.future()
  }
}

Here’s what this file does:

  1. Imports Queues and Vapor, exposing their APIs in the file
  2. Creates a struct — WelcomeEmail — to hold the information you need to send your welcome email
  3. Creates a struct — RecipientWelcomeEmailJob — that conforms to the Job protocol
  4. Defines WelcomeEmail as your job payload
  5. Adds a dequeue(_:_:) method that processes the job on dequeue. In this tutorial, it prints a greeting to the logs and returns EventLoopFuture.
  6. Adds an error(_:_:_:) method, which handles any errors that might occur. This tutorial ignores error handling.

Now, open configure.swift and, between configuring and starting the queue, add the following:

let recipientJob = RecipientWelcomeEmailJob()
app.queues.add(recipientJob)

Here, you instantiate the RecipientWelcomeEmailJob and register it in the queues namespace.

Finally, open NewsletterRecipientAPIController.swift and replace the // TODO on line 54 with:

.flatMap { recipient in
  req.queue.dispatch(
    RecipientWelcomeEmailJob.self,
    WelcomeEmail(to: recipient.email, name: recipient.name)
  )
  .transform(to: recipient)
}

This closure dispatches RecipientWelcomeEmailJob to the queue. It has a payload containing the email and name of the created recipient.

Finally, you transform the response back to the recipient, and the method can finish.

Now, build and run.

Console output after you have registered the scheduled job

Console output after scheduled job registration

Console output after you have registered the scheduled job

In Paw, post a new recipient. You should see that the response doesn’t look different from before. This is expected.

Navigate back to Xcode, and you’ll see four new lines in your debug editor:

[ INFO ] POST /api/newsletter/sign-up [request-id: EBC14B25-4F8A-4AC9-87C8-0001859FD203]
[ INFO ] Dispatched queue job [job_id: 3A4B0EBE-09AA-4E7B-A9E0-8D715F0C75A9, job_name: RecipientWelcomeEmailJob, queue: default, request-id: EBC14B25-4F8A-4AC9-87C8-0001859FD203]
[ INFO ] Dequeuing job [job_id: 3A4B0EBE-09AA-4E7B-A9E0-8D715F0C75A9, job_name: RecipientWelcomeEmailJob, queue: default]
Send welcome email to test2@newslettering.com. Greet the user as Test User 2.

Here’s a breakdown of what the logs tell you:

  1. Which endpoint was called and what the unique request-id is
  2. Your app dispatched a RecipientWelcomeEmailJob to the default queue with the provided job_id
  3. A job from the default queue was dequeued
  4. The print message you created earlier

It’s now time to fill in some gaps regarding the job.

Options When Dispatching to the Queue

When you dispatch a job, you must provide the job’s type and the required payload.

In the example above, the job type was RecipientWelcomeEmailJob and the payload was WelcomeEmail.

When you dispatch a job, you also can provide two other options:

  1. maxRetryCount, which takes an Int. It is zero by default.
  2. delayUntil, which takes an optional Date. It is nil by default.

maxRetryCount is the number of times to retry the job on failure. This is especially important if you are calling an external API and you want to make sure it goes through.

By setting delayUntil, you delay the process of the job until after the date you provided. If the driver dequeues the job too early, it’ll make sure to re-queue it until after the delay time.

When delayUntil is nil or some date in the past, the driver processes the job the first time it is dequeued.

Those are helpful. But what if you want jobs to repeat at specific days or times?

Scheduling Jobs

If you want to run a task at a certain time, such as:

  • Sending a newsletter of the first of every month;
  • Greeting all your friends and family with a Christmas email every December;
  • Or reminding your kid that you are the coolest parent every hour.

You can arrange it as a scheduled job in your Vapor app.

Creating SendNewsletterJob

In Xcode, create a new file, SendNewsletterJob.swift, inside the Jobs folder.

Now, insert:

// 1
import Fluent
import Queues
import Vapor

// 2
struct SendNewsletterJob: ScheduledJob {
  // 3
  func run(context: QueueContext) -> EventLoopFuture<Void> {
    return getNewsletter(on: context.application.db)
      .and(self.getRecipients(on: context.application.db))
      .map { newsletter, recipients in
        // 4
        let message: String = recipients
          .map(\.name)
          .map { "Send newsletter with title: \(newsletter.title) to: \($0)" }
          .joined(separator: "\n")
        print(message)
    }
  }

  // 5
  private func getNewsletter(on db: Database) -> EventLoopFuture<Newsletter> {
    let today = Calendar.current.startOfDay(for: Date())
    return Newsletter.query(on: db)
      .filter(\.$sendAt, .equal, today)
      .first()
      .unwrap(or: Abort(.notFound))
  }

  // 6
  private func getRecipients(
    on db: Database
  ) -> EventLoopFuture<[NewsletterRecipient]> {
    return NewsletterRecipient.query(on: db).all()
  }
}

Here, you:

  1. Import Fluent, Queues and Vapor and expose their APIs in the file
  2. Create a SendNewsletterJob struct and make it conform to ScheduledJob
  3. Create the required run(context:) method. It fetches the newsletter that should be sent out (#5) and all the newsletter recipients (#6).
  4. Map the list of recipients to a list of messages that informs you who received the email. Then print the message.
  5. Create private helper method that fetches the current newsletter. If the newsletter doesn’t exist, the method returns a failed EventLoopFuture with an error.
  6. Create private helper method to fetch all the recipients

Next, open configure.swift and, under the recipientJob, add the following:

let sendNewsletterJob = SendNewsletterJob()
app.queues.schedule(sendNewsletterJob).minutely().at(5)

Here, you instantiate the SendNewsletterJob and register it to run on the fifth second of every minute.

The queues package comes with a handful of convenient helpers to schedule your job:

  • at(_:) takes a specific date for a job that should run only once.
  • yearly() identifies a yearly occurrence. It can be further configured with the month it should run.
  • monthly() sets a monthly schedule, and can be further configured with the day it should run.
  • weekly() specifies that the job should occur weekly, and you can further specify on which day of the week it should run.
  • daily() schedules the job to execute daily. You can further specify the time it should run.
  • hourly() sets the job to an hourly schedule, and can be further configured with the minutes it should run.
  • minutely() configures the job to run every minute. You can further specify which seconds it should run.
  • everySecond() schedules the job for every second and has no further configuration.

Ready to take this out for a spin?