Files
liteq/README.md
2024-02-15 21:43:08 -08:00

173 lines
5.5 KiB
Markdown

# liteq
Library to have a persistent job queue in Go backed by a SQLite DB.
## Motivation
I needed a way to have a persistent queue for a small app so it would survive restarts and allow me to schedule jobs in the future.
Since I already had SQLite as a dependency, I didn't want to add another dependency to make the app work. Especially not an external one like RabbitMQ, Redis, SQS or others.
liteq allows to run tens of thousands of jobs per second if needed. It can also be made to use more than a single DB file to keep growing the concurrency should you need it.
## Usage
### Install
```sh
go get github.com/khepin/liteq
```
### Setup and DB creation
```go
import (
"github.com/khepin/liteq"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// Open the sqlite3 DB in the file "liteq.db"
liteqDb, err := sql.Open("sqlite3", "liteq.db")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Create the DB table if it doesn't exist
liteq.Setup(liteqDb)
// create a job queue
jqueue := liteq.New(liteqDb)
}
```
### Queuing a job
```go
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "notify.email",
Job: `{"email_address": "bob@example.com", "content": "..."}`,
})
```
This will send the job with the given payload on a queue called `notify.email`.
### Consuming
To consume jobs from the queue, you call the consume method:
```go
jqueue.Consume(context.Background(), liteq.ConsumeParams{
Queue: "notify.email",
PoolSize: 3,
VisibilityTimeout: 20,
Worker: func (ctx context.Context, job *liteq.Job) error {
return sendEmail(job)
},
})
```
- `context.Background()` You can pass in a cancellable context and the queue will stop processing when the context is canceled
- `Queue` this is the name of the queue we want this consumer to consume from
- `PoolSize` this is the number of concurrent consumer we want for this queue
- `VisibilityTimeout` is the time that a job will remain reserved to a consumer. After that time has elapsed, if the job hasn't been marked either failed
or successful, it will be put back in the queue for others to consume. The error will be added to the job's error list and the number of remaining attempts will be
decreased.
- `Worker` A callback to process the job. When an error is returned, the job is either returned to the queue for processing with a decreased number of remaining attempts or marked as failed if no more attempts remain.
### Multiple attempts
When queueing a job, it is possible to decide how many times this job can be attempted in case of failures:
```go
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "notify.email",
RemainingAttempts: 3,
Job: `{"email_address": "bob@example.com", "content": "..."}`,
})
```
### Delayed jobs
When queueing a job, you can decide to execute it at a later point in time:
```go
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "notify.email",
ExecuteAfter: time.Now().Add(6*time.Minute).Unix(),
Job: `{"email_address": "bob@example.com", "content": "..."}`,
})
```
In this case, the job won't run until the given time.
### Deduplication
Sometimes it can be useful to prevent the queueing of multiple messages that would essentially be performing the same task to avoid un-necessary work.
This is possible in `liteq` via the `DedupingKey` job parameter. There are 2 types of deduping keys:
- `IgnoreDuplicate` will ignore the new job that was sent and keep the one that was already on the queue
- `ReplaceDuplicate` will instead remove the job currently on the queue and use the new one instead
Assuming we have the following consumer:
```go
jqueue.Consume(context.Background(), liteq.ConsumeParams{
Queue: "print",
VisibilityTimeout: 20,
Worker: func (ctx context.Context, job *liteq.Job) error {
fmt.Println(job.Payload)
return nil
},
})
```
And we send the following jobs:
```go
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "print",
Job: `first`,
DedupingKey: liteq.IgnoreDuplicate("print.job")
})
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "print",
Job: `second`,
DedupingKey: liteq.IgnoreDuplicate("print.job")
})
```
Then the result would be a single output line:
```
first
```
If instead we use `liteq.ReplaceDuplicate`
```go
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "print",
Job: `third`,
DedupingKey: liteq.ReplaceDuplicate("print.job")
})
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "print",
Job: `fourth`,
DedupingKey: liteq.ReplaceDuplicate("print.job")
})
```
We will output `fourth`
If we think for example of the scenario of sending email or text notifications about an order to a customer, we could construct a deduping key like:
```go
jqueue.QueueJob(context.Background(), liteq.QueueJobParams{
Queue: "email.notify",
Job: `{"order_id": 123, "customer_id": "abc"}`,
DedupingKey: liteq.ReplaceDuplicate("email.notify:%s:%s", customer.ID, order.ID)
})
```
That way if the `Order Prepared` status update email hasn't been sent yet by the time we're ready to send the `Order Shipped` email, we can skip the `Order Prepared` one and only send the most recent update to the customer.