Skip to content

Getting Started

Mauricio Gomes edited this page Jan 3, 2026 · 4 revisions

This guide will walk you through installing Senna and running your first background job.

Prerequisites

  • Go 1.21 or later
  • Redis 6.2+ or Valkey 7.2+

Installation

go get github.com/mgomes/senna

Setting Up Redis

Senna requires Redis 6.0+ or Valkey 7.2+ as its backend. Make sure you have one running:

# Using Docker
docker run -d --name redis -p 6379:6379 redis:7

# Or using Valkey
docker run -d --name valkey -p 6379:6379 valkey/valkey:7.2

Important

Configure Redis with maxmemory-policy noeviction to prevent data loss. If Redis evicts keys due to memory pressure, jobs and batch state can be lost.

Creating a Client

The client is used to enqueue jobs from your application.

package main

import (
    "context"
    "log"

    "github.com/mgomes/senna"
    "github.com/mgomes/senna/client"
)

func main() {
    c, err := client.New(&client.Config{
        Redis: senna.RedisConfig{
            Addr: "localhost:6379",
        },
        Namespace: "myapp",
    })
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    ctx := context.Background()

    // Enqueue a job
    job, err := c.Enqueue(ctx, "send_email", map[string]any{
        "to":      "user@example.com",
        "subject": "Welcome!",
        "body":    "Thanks for signing up.",
    })
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Enqueued job: %s", job.ID)
}

Creating a Worker

The worker processes jobs from the queue using registered handlers.

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/mgomes/senna"
    "github.com/mgomes/senna/worker"
)

func main() {
    w, err := worker.New(&worker.Config{
        Redis: senna.RedisConfig{
            Addr: "localhost:6379",
        },
        Namespace: "myapp",
        Settings: senna.WorkerSettings{
            Concurrency: 10,
            Queues: []senna.QueueConfig{
                {Name: "default", Priority: 5},
            },
            ShutdownTimeout: 30 * time.Second,
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    // Register a handler for "send_email" jobs
    w.Register("send_email", func(ctx context.Context, job *senna.Job) error {
        to := job.Args["to"].(string)
        subject := job.Args["subject"].(string)
        body := job.Args["body"].(string)

        fmt.Printf("Sending email to %s: %s\n", to, subject)
        fmt.Printf("Body: %s\n", body)

        // Your email sending logic here...

        return nil
    })

    // Run the worker (blocks until shutdown signal)
    ctx := context.Background()
    if err := w.Run(ctx); err != nil {
        log.Fatal(err)
    }
}

Complete Example

Here's a complete example with both client and worker in a single file for testing:

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/mgomes/senna"
    "github.com/mgomes/senna/client"
    "github.com/mgomes/senna/worker"
)

func main() {
    ctx := context.Background()
    redisConfig := senna.RedisConfig{Addr: "localhost:6379"}
    namespace := "example"

    // Create and start the worker in a goroutine
    w, err := worker.New(&worker.Config{
        Redis:     redisConfig,
        Namespace: namespace,
        Settings: senna.WorkerSettings{
            Concurrency: 5,
            Queues: []senna.QueueConfig{
                {Name: "default", Priority: 5},
            },
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    // Register job handler
    w.Register("greet", func(ctx context.Context, job *senna.Job) error {
        name := job.Args["name"].(string)
        fmt.Printf("Hello, %s!\n", name)
        return nil
    })

    // Start worker in background
    go func() {
        if err := w.Run(ctx); err != nil {
            log.Printf("Worker error: %v", err)
        }
    }()

    // Give worker time to start
    time.Sleep(100 * time.Millisecond)

    // Create client and enqueue jobs
    c, err := client.New(&client.Config{
        Redis:     redisConfig,
        Namespace: namespace,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    // Enqueue some jobs
    for _, name := range []string{"Alice", "Bob", "Charlie"} {
        job, err := c.Enqueue(ctx, "greet", map[string]any{"name": name})
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Enqueued job %s for %s\n", job.ID, name)
    }

    // Wait for interrupt signal
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    <-sigCh

    fmt.Println("\nShutting down...")
    w.Stop()
}

Run this example:

go run main.go

Output:

Enqueued job abc123 for Alice
Enqueued job def456 for Bob
Enqueued job ghi789 for Charlie
Hello, Alice!
Hello, Bob!
Hello, Charlie!

Namespaces

The Namespace configuration prefixes all Redis keys, allowing multiple applications to share the same Redis instance:

// Application A
client.New(&client.Config{
    Redis:     redisConfig,
    Namespace: "app_a",
})

// Application B
client.New(&client.Config{
    Redis:     redisConfig,
    Namespace: "app_b",
})

Each application's jobs and queues are isolated from each other.

Tip

Use descriptive namespace names that identify your application. This makes it easier to debug and monitor in production.

Next Steps

Clone this wiki locally