From b7c8c51d6e867735552bae98ca6618db5bd124a7 Mon Sep 17 00:00:00 2001 From: Tomas Sedlacek Date: Mon, 16 Oct 2023 14:16:53 +0200 Subject: [PATCH] update readme --- README.md | 354 ++++++++++++++++++++++++++++++++++---------- docs/monitoring.png | Bin 0 -> 24840 bytes 2 files changed, 279 insertions(+), 75 deletions(-) create mode 100644 docs/monitoring.png diff --git a/README.md b/README.md index bbc3773..a12a9d6 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,38 @@ -# PGQ - go queues using postgres +# PGQ - go queues on top of postgres [![GoDoc](https://pkg.go.dev/badge/go.dataddo.com/pgq)](https://pkg.go.dev/go.dataddo.com/pgq) [![GoReportCard](https://goreportcard.com/badge/go.dataddo.com/pgq)](https://goreportcard.com/report/go.dataddo.com/pgq) pgsq logo -PGQ is a Go package that provides a queue mechanism for your Go applications built on top of the postgres database. -It enables developers to implement efficient and reliable message queues for their microservices architecture using the familiar postgres infrastructure. - -PGQ internally uses the classic `UPDATE` + `SELECT ... FOR UPDATE` postgres statement which creates the transactional lock for the selected rows in the postgres table and enables the table to behave like the queue. -The Select statement is using the `SKIP LOCKED` clause which enables the consumer to fetch the messages in the queue in the order they were created, and doesn't get stuck on the locked rows. -It is intended to replace special message broker in environments where you already use postgres, and you want clean, simple and straightforward communication among your services. +PGQ is a [Go](http://golang.org) package that provides a queuing mechanism for your Go applications built on top of the postgres database. +It enables developers to implement efficient and reliable, but simple message queues for their microservices architecture using the familiar postgres infrastructure. ## Features -- Postgres-backed: Leverages the power of PostgreSQL to store and manage queues. -- Reliable: Guarantees message persistence and delivery, even in the face of failures. -- Efficient: Optimized for high throughput and low-latency message processing. -- Transactional: Supports transactional message handling, ensuring consistency. -- Simple API: Provides a clean and easy-to-use API for interacting with the queue. +- __Postgres__-backed: Leverages the power of PostgreSQL to store and manage queues. +- __Reliable__: Guarantees message persistence and delivery, even if facing the failures. +- __Transactional__: Supports transactional message handling, ensuring consistency. +- __Simple usage__: Provides a clean and easy-to-use API for interacting with the queue. +- __Efficient__: Optimized for high throughput and low-latency message processing. + +## When to pick PGQ? + +Even though there are other great technologies and tools for complex messaging including the robust routing configuration, sometimes you do not need it, and you can be just fine with the simpler tooling. + +Pick pgq if you: +- need to distribute the traffic fairly among your app replicas and int time to protect each of them from the overload +- need the out-of-a-box observability +- already use `postgres` and you don't want to complicate your tech stack +- are ok with basic routing + +No need to bring the new technology to your existing stack when you can be pretty satisfied with `postgres`. +Write the consumers and publishers in various languages with the simple idea behind - __use postgres table as a queue__. +While using `pgq` you have a superb observability of the queue. +You can easily see the payloads of the messages waiting to be processed, but moreover payloads of both the currently being processed and already processed messages. +You can get the processing results, duration and other statistics pretty simply. +As the `pgq` queue table contains the records of already processed jobs too, you already have out of the box the historical statistics of all messages and can view it effortlessly by using simple SQL queries. + +Pgq is intended to replace the specialized message brokers in environments where you already use postgres, and you want clean, simple and straightforward communication among your services. ## Installation To install PGQ, use the go get command: @@ -26,9 +41,10 @@ go get go.dataddo.com/pgq@latest ``` ## Setup -In order to make pgq functional, there must exist the postgres table with all the fields the pgq requires. -You can create the table query on your own, or you can use the query generator for that. -You usually run the create table command just once during the setup of your application. +Prerequisites: + +In order to make the `pgq` functional, there must exist the `postgres table` with all the necessary `pgq` fields. +You can create the table on your own, or you can use the query generator. You usually run the `create table` command just once during the application setup. ```go package main @@ -41,104 +57,292 @@ import ( func main() { queueName := "my_queue" - // create string contains the "CREATE TABLE my_queue ..." which you may use for table creation + // create string contains the "CREATE TABLE my_queue ..." + // which you may use for table creation. + // You may also use the "GenerateDropTableQuery" for dropping the table create := schema.GenerateCreateTableQuery(queueName) fmt.Println(create) - - // drop string contains the "DROP TABLE my_queue ..." which you may use for cleaning hwn you no longer need the queue - drop := schema.GenerateCreateTableQuery(queueName) - fmt.Println(drop) } - ``` ## Usage + +### Publishing the message + +```go +package main + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "go.dataddo.com/pgq" + _ "github.com/jackc/pgx/v4/stdlib" +) + +func main() { + postgresDSN := "your_postgres_dsn" + queueName := "your_queue_name" + + // create a new postgres connection + db, err := sql.Open("pgx", postgresDSN) + if err != nil { + panic(err.Error()) + } + defer db.Close() + + // create the publisher which may be reused for multiple messages + // you may pass the optional PublisherOptions when creating it + publisher := pgq.NewPublisher(db) + + // publish the message to the queue + // provide the payload which is the JSON object + // and optional metadata which is the map[string]string + msg := pgq.NewMessage(nil, json.RawMessage(`{"foo":"bar"}`)) + msgId, err := publisher.Publish(context.Background(), queueName, msg) + if err != nil { + panic(err.Error()) + } + + fmt.Println("Message published with ID:", msgId) +} ``` -import "go.dataddo.com/pgq" -// Create a new consumer/subscriber -// To be added +After the message is successfully published, you can see the new row with given `msgId` in the queue table. -// Create a new publisher -// To be added +### Publisher options -// Publish message -// To be added +Very often you want some metadata to be part of the message, so you can filter the messages in the queue table by it. +Metadata can be any additional information you think is worth to be part of the message, but you do not want to be part of the payload. +It can be the publisher app name/version, payload schema version, customer identifiers etc etc. + +You can simply attach the metadata to single message by: +```go +metadata := pgq.Metadata{ + "publisherHost": "localhost", + "payloadVersion": "v1.0" +} ``` -For more detailed usage examples and API documentation, please refer to the GoDoc page. +or you can configure the `publisher` to attach the metadata to all messages it publishes: +```go +opts := []pgq.PublisherOption{ + pgq.WithMetaInjectors( + pgq.StaticMetaInjector( + pgq.Metadata{ + "publisherHost": "localhost", + "publisherVersion": "commitRSA" + } + ), + ), +}, + +publisher := pgq.NewPublisher(db, opts) +metadata := pgq.Metadata{ + "payloadVersion": "v1.0" // message specific meta field +} +``` +### Consuming the messages -## Message +```go +package main -The message is the essential structure for communication between services using pgq. The message struct matches the postgres table. You can modify the table structure on your own by adding extra columns, but pgq depends on these mandatory fields" -- `id`: The unique ID of the message in the db -- `created_at`: The timestamp when the record in db was created (message received to the queue) -- `payload`: Your custom message content in JSON format -- `metadata`: Your optional custom metadata about the message in JSON format so your `payload` remains clean. This is the good place where to put information like the `publisher` app name, publish timestamp, payload schema version, customer related information etc. -- `started_at`: Timestamp when the consumer started to process the message -- `locked_until`: Timestamp stating until when the consumer wants to have the lock applied. If this field is set, no other consumer will process this message. -- `processed_at`: Timestamp when the message was processed (either success or failure) -- `error_detail`: The reason why the processing of the message failed provided by the consumer. Default `NULL` means no error. -- `consumed_count`: The incremented integer keeping how many times the messages was tried to be consumed. Preventing the everlasting consumption of message which causes the OOM of consumers or other defects. +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "go.dataddo.com/pgq" + _ "github.com/jackc/pgx/v4/stdlib" +) -## Handy queue sql queries +func main() { + postgresDSN := "your_postgres_dsn" + queueName := "your_queue_name" + + // create a new postgres connection and publisher + db, err := sql.Open("pgx", postgresDSN) + if err != nil { + panic(err.Error()) + } + defer db.Close() + + // create the consumer which gets attached to handling function we defined above + h := &handler{} + consumer, err := pgq.NewConsumer(db, queueName, h) + if err != nil { + panic(err.Error()) + } + + err = consumer.Run(context.Background()) + if err != nil { + panic(err.Error()) + } +} +// we must specify the message handler, which implements simple interface +type handler struct {} +func (h *handler) HandleMessage(_ context.Context, msg pgq.Message) (processed bool, err error) { + fmt.Println("Message payload:", string(msg.Payload())) + return true, nil +} ``` -// messages waiting in the queue to be fetched by consumer (good candidate for the queue length metric) -select * from table_name where processed_at is null and locked_until is null; -// messages being processed at the moment (good candidate for the messages WIP metric) -select * from table_name where processed_at is null and locked_until is not null; +### Consumer options + +You can configure the consumer by passing the optional `ConsumeOptions` when creating it. -// messages which failed when being processed -select * from table_name where processed_at is not null and error_detail is not null; +| Option | Description | +|:-----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| WithLogger | Provide your own `*slog.Logger` to have the pgq logs under control | +| WithMaxParallelMessages | Set how many consumers you want to run concurently in your app. | +| WithLockDuration | You can set your own locks effective duration according to your needs `time.Duration`. If you handle messages quickly, set the duration in seconds/minutes. If you play with long-duration jobs it makes sense to set this to bigger value than your longest job takes to be processed. | +| WithPollingInterval | Defines the frequency of asking postgres table for the new message `[time.Duration]`. | +| WithInvalidMessageCallback | Handle the invalid messages which may appear in the queue. You may re-publish it to some junk queue etc. | +| WithHistoryLimit | how far in the history you want to search for messages in the queue. Sometimes you want to ignore messages created days ago even though the are unprocessed. | +| WithMetrics | No problem to attach your own metrics provider (prometheus, ...) here. | -// messages created in last 1 day which have not been processed yet -select * from table_name where processed_at is null and created_at > NOW() - INTERVAL '1 DAY'; +```go +consumer, err := NewConsumer(db, queueName, handler, + WithLogger(slog.New(slog.NewTextHandler(&tbWriter{tb: t}, &slog.HandlerOptions{Level: slog.LevelDebug}))), + WithLockDuration(10 * time.Minute), + WithPollingInterval(2 * time.Second), + WithMaxParallelMessages(1), + WithMetrics(noop.Meter{}), + ) +``` + +For more detailed usage examples and API documentation, please refer to the Dataddo pgq GoDoc page. + +## Message -// messages causing unexpected failures of consumers (OOM ususally) -select * from table_name where consumed_count > 1; +The message is the essential structure for communication between services using `pgq`. +The message struct matches the postgres table schema. You can modify the table structure on your own by adding extra columns, but `pgq` depends on following mandatory fields only: + +| Field | Description | +|:-----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | The unique ID of the message in the db. | +| `payload` | User's custom message content in JSON format. | +| `metadata` | User's custom metadata about the message in JSON format so your `payload` remains cleansed from unnecessary data. This is the good place where to put information like the `publisher` app name, payload schema version, customer related information for easier debugging etc. | +| `created_at` | The timestamp when the record in db was created (message received to the queue). | +| `started_at` | Timestamp indicating when the consumer started to process the message. | +| `locked_until` | Contains the consumer lock validity timestamp. If this field is set and has not expired yet, no other consumer can process the message. | +| `processed_at` | Timestamp when the message was processed (either success or failure). | +| `error_detail` | The reason why the processing of the message failed provided by the consumer. `NULL` means no error. | +| `consumed_count` | The integer incremented by consumer retries is preventing the consumption of the message which can cause infinite processing loops because of OOM errors etc. | + +## Handy pgq SQL queries + +### Queue size +Get the messages waiting in the queue to be fetched by consumer. +```sql +select * from queue_name where processed_at is null and locked_until is null; +``` +Get the number of messages waiting in the queue. A good candidate for the queue length metric in your monitoring system. +```sql +select count(*) from queue_name where processed_at is null and locked_until is null; +``` -// top 10 slowest processed messages -select id, processed_at - started_at as duration from extractor_input where processed_at is not null and started_at is not null order by duration desc limit 10; +### Messages currently being processed +Get the messages being processed at the moment. +```sql +select * from queue_name where processed_at is null and locked_until is null; +``` +Get the number of messages currently being processed. Another good candidate for metric in your monitoring system. +```sql +select count(*) from queue_name where processed_at is null and locked_until is null; ``` -## Optimizing performance +_Tip: You can use the `pgq` table as a source for your monitoring system._ +The queue length and messages being processed example in a Grafana panel populated by data from Prometheus fectehd from postgres queue table -In order to get the most out of your postgres, you should invest some time configuring pgq to optimize it's performance. -- __create indices__ for the fields the pgq uses -- use postgres __tables partitioning__ (pg_partman) to speed up queries +### Processed messages +The messages which have already been successfully processed. +```sql +select * from queue_name where processed_at is not null and error_detail is null; +``` +The messages which have already been processed, but ended with an error. +```sql +select * from queue_name where processed_at is not null and error_detail is not null; +``` -## Use Cases for Queues in Microservice Architecture -- Asynchronous Communication: Queues enable decoupled communication between microservices by allowing them to exchange messages asynchronously. This promotes scalability, fault tolerance, and flexibility in building distributed systems. -- Event-Driven Architecture: Queues serve as a backbone for event-driven architectures, where events are produced by services and consumed by interested consumers. This pattern enables loose coupling and real-time processing of events, facilitating reactive and responsive systems. -- Load Balancing: Queues can distribute the workload across multiple instances of a microservice. They ensure fair and efficient processing of tasks by allowing multiple workers to consume messages from the queue in a load-balanced manner. -- Task Scheduling: Queues can be used for scheduling and executing background tasks or long-running processes asynchronously. This approach helps manage resource-intensive tasks without blocking the main execution flow. +### Other useful queries +```sql +-- messages created in last 1 day which have not been processed yet +select * from queue_name where processed_at is null and created_at > NOW() - INTERVAL '1 DAY'; -## Other Queueing Tools for Microservice Architecture -While PGQ offers a Postgres-based queueing solution, there are several other popular tools available for implementing message queues in microservice architectures: +-- messages causing unexpected failures of consumers (ususally OOM) +select * from queue_name where consumed_count > 1; -__RabbitMQ__: A feature-rich, open-source message broker that supports multiple messaging patterns, such as publish/subscribe and request/reply. It provides robust message queuing, routing, and delivery guarantees. +-- top 10 slowest processed messages +select id, queue_name - started_at as duration from extractor_input where processed_at is not null and started_at is not null order by duration desc limit 10; +``` +## Under the hood -__Kafka__: A distributed streaming platform that is highly scalable and fault-tolerant. Kafka is designed for handling high-throughput, real-time data streams and provides strong durability and fault tolerance guarantees. +The pgq internally uses the classic `UPDATE` + `SELECT ... FOR UPDATE` postgres statement which creates the transactional lock for the selected rows in the postgres table and enables the table to behave like the queue. +The Select statement is using the `SKIP LOCKED` clause +which enables the consumer to fetch the messages in the queue in the order they were created, and doesn't get stuck on the locked rows. -__Amazon Simple Queue Service (SQS)__: A fully managed message queuing service provided by AWS. SQS offers reliable and scalable queues with automatic scaling, high availability, and durability. +## Optimizing performance -__Google Cloud Pub/Sub__: A messaging service from Google Cloud that provides scalable and reliable message queuing and delivery. It offers features like event-driven processing, push and pull subscriptions, and topic-based messaging. +When using the pgq in production environment you should focus on the following areas to improve the performance: +### Queue table Indexes +Having indexes on the fields which are used for sending is the essential key for the good performance. +When postgres lacks the indexes, it can very negatively influence the performance of searching of the queue, which may lead to slowing down the whole database instance. -__Microsoft Azure Service Bus__: A cloud-based messaging service on Microsoft Azure that enables reliable communication between services and applications. It supports various messaging patterns and provides advanced features like message sessions and dead-lettering. +Each queue table should have at least the following indexes: +```sql +CREATE INDEX IDX_CREATED_AT ON my_queue_name (created_at); +CREATE INDEX IDX_PROCESSED_AT_CONSUMED_COUNT ON my_queue_name (consumed_count, processed_at) WHERE (processed_at IS NULL); +``` +These indexes are automatically part of the output query of the `GenerateCreateTableQuery` function. +But if you create tables on your own, please make sure you have them. -Choose a queueing tool based on your specific requirements, such as scalability, fault tolerance, delivery guarantees, integration capabilities, and cloud provider preferences. +### Queue table partitioning -## When to pick PGQ? +Usually you do not need to keep the full history of the queue table in the database for months back. +You may delete such rows with the `DELETE` command in some cron jobs, but do not forget that DELETE is a very expensive operation in postgres, +and it may affect insertions and updates in the table. -Even though the technologies listed above are great for complex messaging including the robust routing configuration, sometimes you do not need it for your simple use cases. +The better solution is to use the __postgres table partitioning__. +The easiest way how to set up the partitioning is to use the `pg_partman` postgres extension. -If you need just the basic routing, distribute the payload fairly to protect your services from overloading and want to use technology which is already in your tech stack (postgres), the __pgq__ is the right choice. No need to bring the new technology to your stack when you can be satisfied with postgres. +If the query returns 0, you need to install the extension first, otherwise you're ready to partition. +```sql +SELECT count(name) FROM pg_available_extensions where name = 'pg_partman'; +``` -Write consumers and publishers in various languages with the simple idea behind - use postgre table as a queue. +1. we create the `template table` to be used for creation of new partitions: -While using pgq you have a superb observability of what is going on in the queue. You can easily see the messages and their content which are currently being processed. You can see how long the processing takes, if it succeeded or and why it failed. As the queue remebers the already processed jobs too, you have out of the box the historical statistics and can view it effortlessly by using regular SQL queries. +The template table must have exactly the same structure as the original queue, and it has the `_template` name suffix. +It must also contain the indexes so the partition derived tables have it too. +```sql +CREATE TABLE my_queue_name_template (id UUID NOT NULL DEFAULT gen_random_uuid(), created_at TIMESTAMP(0) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, payload JSONB DEFAULT NULL, metadata JSONB DEFAULT NULL, locked_until TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, processed_at TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, error_detail TEXT DEFAULT NULL, started_at TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, consumed_count INT DEFAULT 0 NOT NULL, PRIMARY KEY(id, created_at)); +CREATE INDEX IDX_CREATED_AT_TPL ON my_queue_name_template (created_at); +CREATE INDEX IDX_PROCESED_AT_TPL ON my_queue_name_template (consumed_count, processed_at) WHERE (processed_at IS NULL); +``` +2. we create the partitioned table with the same structure as the template table, but with the partitioning key: +```sql +-- DROP the table if it already exists +DROP table IF EXISTS my_queue_name; +-- and let it be created like partman does it. This is the default queue to be used when no partitioned one is matched +CREATE TABLE IF NOT EXISTS my_queue_name +(LIKE my_queue_name_template INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES INCLUDING COMMENTS) PARTITION BY RANGE (created_at); +``` +3. we instruct partman to create the partitions every day automatically: +```sql +SELECT partman.create_parent('my_queue_name', 'created_at', 'native', 'daily', p_template_table := 'my_queue_name_template'; +``` +4. we configure partman how to rotate the tables setting the 14 days retention period: +```sql +UPDATE partman.part_config +SET infinite_time_partitions = true, + retention = '14 days', + retention_keep_table = false, + retention_keep_index = false +WHERE parent_table = 'my_queue_name'; +``` \ No newline at end of file diff --git a/docs/monitoring.png b/docs/monitoring.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ee0a6305daeef1fa693b1e3522945d6e89053b GIT binary patch literal 24840 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 M71ZU+UYdpc50+lp#sB~S literal 0 HcmV?d00001