batch

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package batch processes the records of a batch event one at a time and reports to Lambda only those that failed, so that only they are retried: the partial batch response of Amazon SQS (standard and FIFO queues), Kinesis Data Streams, DynamoDB Streams, and Amazon MSK or self-managed Apache Kafka. It is the Go side of Powertools for AWS Lambda (Python)'s batch processing utility.

Getting started

The event source mapping must enable ReportBatchItemFailures (for Kafka, in provisioned mode). Write a record handler, which returns an error for a record that must be retried, and hand it to the Process function of the event source, or turn it into the Lambda handler with Handler:

func handleMessage(ctx context.Context, msg events.SQSMessage) error {
	return orders.Save(ctx, msg.Body)
}

func main() {
	lambda.Start(batch.Handler(batch.ProcessSQS, handleMessage))
}

Every record reaches the handler, even after some failed. The response lists the failed ones by the identifier Lambda expects: the message id for SQS, the sequence number for Kinesis and DynamoDB, the topic, partition and offset for Kafka. When every record succeeds the list is empty.

Source                 Process function   Response
SQS standard queue     ProcessSQS         events.SQSEventResponse
SQS FIFO queue         ProcessSQSFIFO     events.SQSEventResponse
Kinesis Data Streams   ProcessKinesis     events.KinesisEventResponse
DynamoDB Streams       ProcessDynamoDB    events.DynamoDBEventResponse
Amazon MSK, Kafka      ProcessKafka       KafkaEventResponse

Failures

A record fails when its handler returns an error or panics; the panic is recovered into a *PanicError carrying the panic value and the stack, so that one bad record does not take the batch down. When the invocation's context ends (its deadline passes, or it is cancelled), processing stops: the records that had not started fail with ErrNotProcessed, without reaching the handler.

When every record fails, the Process function returns a *ProcessingError next to the response. Returned from the handler, it fails the invocation: the error shows in the function's metrics and Lambda retries the whole batch. WithRaiseOnEntireBatchFailure(false) returns the response alone instead, listing every record, for functions that use errors as flow control and would otherwise see Lambda scale their SQS pollers down. An event without records fails with ErrUnexpectedBatchType.

SQS FIFO queues

ProcessSQSFIFO keeps the queue's order: it processes the messages one after the other and, at the first failure, fails all the messages that follow with ErrFIFOShortCircuit without processing them, so that the queue redelivers them in order. With WithSkipGroupOnError, only the following messages of the failed message's group fail (ErrFIFOGroupShortCircuit); the other groups go on. A message without a group never stops the others.

Typed records

SQSJSON, KinesisJSON, KafkaJSON and DynamoDBImages adapt a handler that takes the record's payload decoded into a Go type (the order, not the SQS message carrying it), the way Powertools Python decodes records into Pydantic models. A payload that does not decode, or whose Validate method rejects it, fails its record with ErrDecode: a poison pill is reported like any other failure and does not stop the batch.

func handleOrder(ctx context.Context, order Order) error { ... }

lambda.Start(batch.Handler(batch.ProcessSQS, batch.SQSJSON(handleOrder)))

Options

  • WithRaiseOnEntireBatchFailure: whether a batch whose records all failed fails the invocation (default true).
  • WithConcurrency: how many records are processed at the same time (default 1, one after the other). Ignored by ProcessSQSFIFO.
  • WithSkipGroupOnError: ProcessSQSFIFO fails only the failed message's group.
  • WithLogger: a logger that receives a warning for every record whose handler failed.
  • WithBeforeRecord, WithAfterRecord, WithOnFailure: hooks around each record, which take the record type of the batch (events.SQSMessage for ProcessSQS). A hook of another type makes the Process function fail with ErrHookRecordType before any record runs.

The package reads no environment variable. Internal diagnostics go to standard error when POWERTOOLS_DEBUG is on.

Concurrency

The Process functions keep no state between calls: concurrent invocations (Lambda Managed Instances) can share options and handlers. With WithConcurrency above 1, the handler and the hooks run in several goroutines at once and must be safe for it; the response keeps the order of the batch.

Differences from Powertools Python

  • There are no processor objects, context managers or event type enum: each event source has its Process function, Handler is process_partial_response as a decorator, and the deprecated batch_processor decorators are not ported.
  • Subclassing BatchProcessor to override success_handler and failure_handler, or BasePartialProcessor to write a processor, becomes the record hooks. WithAfterRecord and WithOnFailure receive the same typed record, where Python passes the raw record to success_handler and the typed one to failure_handler. Work that a custom processor does once the batch is over (storing the successful records, say) goes after the Process function returns, with a hook collecting what it needs. Accessing the processed messages works the same way.
  • AsyncBatchProcessor becomes WithConcurrency, with a bound Python does not have; asyncio.gather starts every record at once.
  • The record handler returns only an error: the results Python collects from the handler have no use in the response.
  • The Lambda context is not injected by parameter name: it travels in the context.Context (lambdacontext.FromContext).
  • Pydantic models become the typed adapters. Validation runs when the decoded type has a Validate method.
  • ProcessSQSFIFO honors WithRaiseOnEntireBatchFailure, which Python's SqsFifoPartialProcessor does not expose.
  • A panic fails its record, where Python only catches Exception, and the end of the invocation's context stops the batch.
  • Kafka failures use the key "partition", as Lambda and Powertools Python's code do; the upstream documentation's sample response shows "topic-partition", which Lambda rejects. Records are processed in topic and partition order, since aws-lambda-go hands them over in a map, where Python keeps the order of the JSON event.
  • Kinesis and DynamoDB time windows (tumbling windows) are not supported, as in Python.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrFIFOShortCircuit fails every record of an SQS FIFO batch that
	// follows a failed one, so that the queue keeps its order: Powertools
	// Python's SQSFifoCircuitBreakerError.
	ErrFIFOShortCircuit = errors.New("a previous record failed processing")
	// ErrFIFOGroupShortCircuit fails every record of an SQS FIFO batch that
	// follows a failed one of the same message group, when
	// WithSkipGroupOnError is on: Powertools Python's
	// SQSFifoMessageGroupCircuitBreakerError.
	ErrFIFOGroupShortCircuit = errors.New("a previous record from this message group failed processing")
	// ErrNotProcessed fails the records left when the invocation's context
	// ends. The error wraps the context's cause too.
	ErrNotProcessed = errors.New("record not processed before the invocation ended")
)

Errors a record fails with when its handler never ran.

View Source
var (
	// ErrUnexpectedBatchType reports an event without records, which is what
	// an event of another source decodes to: Powertools Python's
	// UnexpectedBatchTypeError.
	ErrUnexpectedBatchType = errors.New("unexpected batch event type: the event holds no records")
	// ErrHookRecordType reports a hook written for another record type than
	// the batch holds, such as a WithOnFailure taking SQS messages passed to
	// ProcessKinesis.
	ErrHookRecordType = errors.New("hook record type does not match the batch")
)

Errors that fail the whole batch.

View Source
var ErrDecode = errors.New("decode the record payload")

ErrDecode fails a record whose payload does not decode into the handler's type, or decodes into a value whose Validate method rejects it: a poison pill, reported like any other failed record.

Functions

func Handler

func Handler[E, R, Resp any](process ProcessFunc[E, R, Resp], handler RecordHandler[R], opts ...Option) func(ctx context.Context, event E) (Resp, error)

Handler turns a record handler into a Lambda handler for the event that process takes: Powertools Python's process_partial_response as a decorator.

lambda.Start(batch.Handler(batch.ProcessSQS, handleMessage))
Example
package main

import (
	"context"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	handle := func(_ context.Context, rec events.KinesisEventRecord) error {
		fmt.Printf("%s\n", rec.Kinesis.Data)
		return nil
	}

	// In main: lambda.Start(handler)
	handler := batch.Handler(batch.ProcessKinesis, handle)

	event := events.KinesisEvent{Records: []events.KinesisEventRecord{
		{Kinesis: events.KinesisRecord{SequenceNumber: "4950", Data: []byte("hello")}},
	}}
	resp, err := handler(context.Background(), event)
	fmt.Println(len(resp.BatchItemFailures), err)
}
Output:
hello
0 <nil>

func ProcessDynamoDB

ProcessDynamoDB processes the records of a DynamoDB stream: every record goes to handler, and the response lists those that failed, by sequence number. Lambda checkpoints the shard at the lowest one and retries from there.

Batch failures are as for ProcessSQS.

func ProcessKinesis

ProcessKinesis processes the records of a Kinesis data stream: every record goes to handler, and the response lists those that failed, by sequence number. Lambda checkpoints the shard at the lowest one and retries from there.

Batch failures are as for ProcessSQS.

func ProcessSQS

func ProcessSQS(ctx context.Context, event events.SQSEvent, handler RecordHandler[events.SQSMessage], opts ...Option) (events.SQSEventResponse, error)

ProcessSQS processes the messages of a standard SQS queue: every message goes to handler, and the response lists those that failed, by message id, so that only they return to the queue. The event source mapping must enable ReportBatchItemFailures.

When every message fails, it also returns a *ProcessingError (see WithRaiseOnEntireBatchFailure). An event without messages is an ErrUnexpectedBatchType error.

Example
package main

import (
	"context"
	"encoding/json/v2"
	"errors"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	handle := func(_ context.Context, msg events.SQSMessage) error {
		if msg.Body == "" {
			return errors.New("empty message")
		}
		return nil // processed: SQS deletes the message
	}

	event := events.SQSEvent{Records: []events.SQSMessage{
		{MessageId: "059f36b4", Body: "hello"},
		{MessageId: "244fc6b4", Body: ""},
	}}
	resp, err := batch.ProcessSQS(context.Background(), event, handle)
	if err != nil {
		panic(err)
	}
	out, err := json.Marshal(resp)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
}
Output:
{"batchItemFailures":[{"itemIdentifier":"244fc6b4"}]}

func ProcessSQSFIFO

func ProcessSQSFIFO(ctx context.Context, event events.SQSEvent, handler RecordHandler[events.SQSMessage], opts ...Option) (events.SQSEventResponse, error)

ProcessSQSFIFO processes the messages of an SQS FIFO queue, in order. At the first failure it stops: the failed message and every one after it are reported, the latter failing with ErrFIFOShortCircuit without reaching handler, so that the queue delivers them again in the same order. WithSkipGroupOnError limits that to the failed message's group.

It never processes messages concurrently, whatever WithConcurrency says.

Example
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	handle := func(_ context.Context, msg events.SQSMessage) error {
		if msg.Body == "bad" {
			return errors.New("cannot process")
		}
		fmt.Println("processed", msg.MessageId)
		return nil
	}

	group := func(id string) map[string]string { return map[string]string{"MessageGroupId": id} }
	event := events.SQSEvent{Records: []events.SQSMessage{
		{MessageId: "1", Body: "bad", Attributes: group("customer-a")},
		{MessageId: "2", Body: "ok", Attributes: group("customer-b")},
		{MessageId: "3", Body: "ok", Attributes: group("customer-a")},
	}}
	resp, err := batch.ProcessSQSFIFO(context.Background(), event, handle, batch.WithSkipGroupOnError())
	if err != nil {
		panic(err)
	}
	for _, f := range resp.BatchItemFailures {
		fmt.Println("retry", f.ItemIdentifier)
	}
}
Output:
processed 2
retry 1
retry 3

Types

type KafkaBatchItemFailure

type KafkaBatchItemFailure struct {
	ItemIdentifier KafkaItemIdentifier `json:"itemIdentifier"`
}

KafkaBatchItemFailure is a Kafka record that failed processing.

type KafkaEventResponse

type KafkaEventResponse struct {
	BatchItemFailures []KafkaBatchItemFailure `json:"batchItemFailures"`
}

KafkaEventResponse is the partial batch response of a Kafka event (Amazon MSK or self-managed Apache Kafka), which aws-lambda-go does not define.

func ProcessKafka

func ProcessKafka(ctx context.Context, event events.KafkaEvent, handler RecordHandler[events.KafkaRecord], opts ...Option) (KafkaEventResponse, error)

ProcessKafka processes the records of an Amazon MSK or self-managed Apache Kafka event: every record goes to handler, and the response lists those that failed, by topic, partition and offset. Lambda supports it on event source mappings in provisioned mode.

The event groups records by topic and partition in a map, which has no order in Go: the records are processed topic by topic, partition by partition in ascending order, each partition's records in the order of the event. Batch failures are as for ProcessSQS.

Example
package main

import (
	"context"
	"encoding/json/v2"
	"errors"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	handle := func(_ context.Context, rec events.KafkaRecord) error {
		if rec.Offset == 16 {
			return errors.New("poison pill")
		}
		return nil
	}

	event := events.KafkaEvent{Records: map[string][]events.KafkaRecord{
		"mytopic-0": {{Topic: "mytopic", Partition: 0, Offset: 15}, {Topic: "mytopic", Partition: 0, Offset: 16}},
	}}
	resp, err := batch.ProcessKafka(context.Background(), event, handle)
	if err != nil {
		panic(err)
	}
	out, err := json.Marshal(resp)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
}
Output:
{"batchItemFailures":[{"itemIdentifier":{"partition":"mytopic-0","offset":16}}]}

type KafkaItemIdentifier

type KafkaItemIdentifier struct {
	Partition string `json:"partition"`
	Offset    int64  `json:"offset"`
}

KafkaItemIdentifier identifies a Kafka record: Lambda wants the topic and the partition joined as "<topic>-<partition>" under the key "partition", and the record's offset.

type Option

type Option func(*config)

Option configures how a batch is processed.

func WithAfterRecord

func WithAfterRecord[R any](fn func(ctx context.Context, record R, err error)) Option

WithAfterRecord runs fn once the outcome of each record is known, err being nil when the record succeeded: Powertools Python's success_handler and failure_handler together. It sees every record, skipped ones included, after the WithOnFailure hooks. Record types and concurrency are as for WithBeforeRecord.

Example

A custom processor in Powertools Python stores the successful records once the batch is done; here a hook collects them, safely even when the records run concurrently.

package main

import (
	"context"
	"fmt"
	"sync"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	var (
		mu        sync.Mutex
		succeeded []string
	)
	collect := batch.WithAfterRecord(func(_ context.Context, msg events.SQSMessage, err error) {
		if err == nil {
			mu.Lock()
			defer mu.Unlock()
			succeeded = append(succeeded, msg.MessageId)
		}
	})

	event := events.SQSEvent{Records: []events.SQSMessage{{MessageId: "a"}, {MessageId: "b"}}}
	_, err := batch.ProcessSQS(context.Background(), event,
		func(context.Context, events.SQSMessage) error { return nil },
		collect, batch.WithConcurrency(2))
	if err != nil {
		panic(err)
	}
	// Write them all at once, as the custom processor does in _clean.
	fmt.Println(len(succeeded), "records to store")
}
Output:
2 records to store

func WithBeforeRecord

func WithBeforeRecord[R any](fn func(ctx context.Context, record R)) Option

WithBeforeRecord runs fn right before the handler receives each record. Records that are skipped (after a FIFO failure, or when the invocation ends) never reach it.

The record type R must be the batch's: events.SQSMessage for ProcessSQS, events.KinesisEventRecord for ProcessKinesis, and so on. With WithConcurrency, hooks run concurrently and must be safe for it.

func WithConcurrency

func WithConcurrency(n int) Option

WithConcurrency processes up to n records at the same time, each in its own goroutine; the default, 1, processes them one after the other. It is Powertools Python's AsyncBatchProcessor, with a bound. Use it only when the records do not depend on each other's order. ProcessSQSFIFO ignores it: a FIFO queue's records are always processed in order.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger writes a warning to logger for every record whose handler failed, with the error, as Powertools Python does when given a logger. Records skipped without running the handler are not logged.

func WithOnFailure

func WithOnFailure[R any](fn func(ctx context.Context, record R, err error)) Option

WithOnFailure runs fn for every record that failed, skipped ones included: Powertools Python's failure_handler, the place to send errors to a tracking service or to count failures in a metric. Record types and concurrency are as for WithBeforeRecord.

func WithRaiseOnEntireBatchFailure

func WithRaiseOnEntireBatchFailure(raise bool) Option

WithRaiseOnEntireBatchFailure decides what happens when every record of the batch fails. On (the default), the Process function returns a *ProcessingError next to the response, so that the invocation fails and Lambda retries the whole batch. Off, it returns the response alone, with every record in it: the invocation succeeds, which keeps Lambda from scaling the SQS pollers down when errors are part of the flow.

Example
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	failing := func(context.Context, events.SQSMessage) error { return errors.New("downstream unavailable") }
	event := events.SQSEvent{Records: []events.SQSMessage{{MessageId: "a"}}}

	_, err := batch.ProcessSQS(context.Background(), event, failing)
	batchFailure, ok := errors.AsType[*batch.ProcessingError](err)
	fmt.Println("default:", ok, len(batchFailure.Errors))

	resp, err := batch.ProcessSQS(context.Background(), event, failing, batch.WithRaiseOnEntireBatchFailure(false))
	fmt.Println("disabled:", err, len(resp.BatchItemFailures))
}
Output:
default: true 1
disabled: <nil> 1

func WithSkipGroupOnError

func WithSkipGroupOnError() Option

WithSkipGroupOnError makes ProcessSQSFIFO keep going after a failure: it fails only the remaining records of the failed record's message group and processes the other groups. The other Process functions ignore it.

type PanicError

type PanicError struct {
	// Value is what the handler passed to panic.
	Value any
	// Stack is the stack trace of the panicking goroutine.
	Stack []byte
}

PanicError is the failure of a record whose handler panicked.

func (*PanicError) Error

func (e *PanicError) Error() string

func (*PanicError) Unwrap

func (e *PanicError) Unwrap() error

Unwrap returns the panic value when it is an error.

type ProcessFunc

type ProcessFunc[E, R, Resp any] = func(ctx context.Context, event E, handler RecordHandler[R], opts ...Option) (Resp, error)

ProcessFunc is the shape of ProcessSQS and its siblings, which Handler turns into a Lambda handler.

type ProcessingError

type ProcessingError struct {
	// Errors holds one error per record, in the order of the batch.
	Errors []error
}

ProcessingError reports a batch whose records all failed: returned from the Lambda handler, it makes Lambda retry the whole batch and shows in the function's error metrics. It is Powertools Python's BatchProcessingError.

func (*ProcessingError) Error

func (e *ProcessingError) Error() string

func (*ProcessingError) Unwrap

func (e *ProcessingError) Unwrap() []error

Unwrap returns the errors of the records, so that errors.Is and errors.AsType look into each of them.

type RecordHandler

type RecordHandler[R any] = func(ctx context.Context, record R) error

RecordHandler processes one record of a batch. It returns nil when the record is done and an error when the record failed and must be retried. A panic counts as a failure too.

The context is the invocation's: lambdacontext.FromContext reads the Lambda context from it, ctx.Deadline tells how much time is left.

func DynamoDBImages

func DynamoDBImages[T any](handler func(ctx context.Context, newImage, oldImage *T) error) RecordHandler[events.DynamoDBEventRecord]

DynamoDBImages adapts handler, which takes the item images of a stream record decoded into a T, to a record handler for ProcessDynamoDB.

An image is nil when the record lacks it: there is no new image in a REMOVE record, no old one in an INSERT, and the stream's view type decides which images the others carry. Attribute values become JSON first, the way Powertools Python's TypeDeserializer reads them (numbers as numbers, binary as base64 strings, sets as arrays), then decode into T as for SQSJSON.

Example
package main

import (
	"context"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	type Product struct {
		SKU   string `json:"sku"`
		Stock int    `json:"stock"`
	}
	handle := func(_ context.Context, newImage, oldImage *Product) error {
		switch {
		case oldImage == nil:
			fmt.Println("new product", newImage.SKU, newImage.Stock)
		case newImage == nil:
			fmt.Println("removed product", oldImage.SKU)
		default:
			fmt.Println("stock of", newImage.SKU, "went from", oldImage.Stock, "to", newImage.Stock)
		}
		return nil
	}

	image := func(stock string) map[string]events.DynamoDBAttributeValue {
		return map[string]events.DynamoDBAttributeValue{
			"sku":   events.NewStringAttribute("sku-1"),
			"stock": events.NewNumberAttribute(stock),
		}
	}
	event := events.DynamoDBEvent{Records: []events.DynamoDBEventRecord{
		{Change: events.DynamoDBStreamRecord{SequenceNumber: "1", NewImage: image("5")}},
		{Change: events.DynamoDBStreamRecord{SequenceNumber: "2", NewImage: image("3"), OldImage: image("5")}},
		{Change: events.DynamoDBStreamRecord{SequenceNumber: "3", OldImage: image("3")}},
	}}
	if _, err := batch.ProcessDynamoDB(context.Background(), event, batch.DynamoDBImages(handle)); err != nil {
		panic(err)
	}
}
Output:
new product sku-1 5
stock of sku-1 went from 5 to 3
removed product sku-1

func KafkaJSON

func KafkaJSON[T any](handler func(ctx context.Context, value T) error) RecordHandler[events.KafkaRecord]

KafkaJSON adapts handler, which takes the record's value decoded from base64 and then from JSON into a T, to a record handler for ProcessKafka. Decoding is as for SQSJSON. For Avro and Protobuf values, see the kafka package.

func KinesisJSON

func KinesisJSON[T any](handler func(ctx context.Context, data T) error) RecordHandler[events.KinesisEventRecord]

KinesisJSON adapts handler, which takes the record's data decoded from JSON into a T, to a record handler for ProcessKinesis. Decoding is as for SQSJSON; aws-lambda-go has already decoded the data from base64.

func SQSJSON

func SQSJSON[T any](handler func(ctx context.Context, body T) error) RecordHandler[events.SQSMessage]

SQSJSON adapts handler, which takes the message body decoded from JSON into a T, to a record handler for ProcessSQS and ProcessSQSFIFO: the Powertools Python Pydantic integration, with a Go struct as the model.

A body that is not JSON, or does not fit T, fails the message with ErrDecode. Decoding is lenient: unknown fields are ignored, missing ones stay zero. When T (or *T) has a Validate() error method, it runs after decoding and its error fails the message too.

Example
package main

import (
	"context"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/batch"
)

func main() {
	type Order struct {
		Item string `json:"item"`
	}
	handle := func(_ context.Context, order Order) error {
		fmt.Println("ordered", order.Item)
		return nil
	}

	event := events.SQSEvent{Records: []events.SQSMessage{
		{MessageId: "1", Body: `{"item":"coffee"}`},
		{MessageId: "2", Body: `not an order`},
	}}
	resp, err := batch.ProcessSQS(context.Background(), event, batch.SQSJSON(handle))
	if err != nil {
		panic(err)
	}
	fmt.Println("failed:", resp.BatchItemFailures[0].ItemIdentifier)
}
Output:
ordered coffee
failed: 2

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL