Public API design from real usage

July 26, 2026 ยท View on GitHub

The public API was derived from concrete producer and consumer call sites before implementation. The snippets below match the public headers. Full programs are under examples/.

Client construction and capabilities

auto client = std::make_shared<pgmq::Client>(pgmq::ClientOptions{
    .connection_string = database_url,
    .pool_size = 8,
    .connect_timeout = std::chrono::seconds{5},
    .application_name = "thumbnail-service",
    .statement_timeout = std::chrono::seconds{30},
    .pool_acquire_timeout = std::chrono::seconds{30},
});

const auto capabilities = client->capabilities();
if (!capabilities.notifications) {
  log_warning("PGMQ insert notifications unavailable; worker will poll");
}

Client is movable but not copyable. It owns a lazy bounded pool and may be shared safely between application components. A checked QueueName is used where an operation can derive a physical queue identifier. Exhausted-pool waits are bounded by pool_acquire_timeout; expiry throws a timeout-category pgmq::Error.

Producer and regular worker

const pgmq::QueueName queue{"thumbnail_jobs"};
client->create_queue(queue);

const auto message_id = client->send(
    queue,
    pgmq::Json{{"image_id", 42}},
    std::optional<pgmq::Json>{
        pgmq::Json{{"trace_id", "abc"}}},
    std::chrono::seconds{5});
(void)message_id;

pgmq::QueueWorkerConfig config{queue};
config.concurrency = 4;
config.claim_batch_size = 8;
config.max_in_flight = 16;
config.visibility_timeout = std::chrono::seconds{30};
config.lease_renewal_interval = std::chrono::seconds{10};
config.handler =
    [](const pgmq::Task& task, pgmq::HandlerContext& context) {
      render_thumbnail(task.message.body, context.stop_token);
      return pgmq::HandlerResult::success_delete();
    };

pgmq::WorkerRuntime runtime{client};
runtime.add_queue(std::move(config));
runtime.start();
const auto stopped = runtime.shutdown(std::chrono::seconds{30});

QueueWorkerConfig has an explicit QueueName constructor and is not a designated aggregate. max_in_flight bounds claimed pending plus executing tasks.

Read variants

pgmq::ReadOptions options;
options.visibility_timeout = std::chrono::seconds{30};
options.quantity = 10;
options.strategy = pgmq::ReadStrategy::grouped_heads;
options.max_poll_time = std::chrono::seconds{5};
options.poll_interval = std::chrono::milliseconds{100};

const auto messages = client->read(pgmq::QueueName{"ordered_jobs"}, options);

For standard reads, options.conditional carries the experimental upstream JSON predicate. It cannot be combined with grouped strategies.

Each returned Message carries both visible_at and database_observed_at. The latter is PostgreSQL clock_timestamp() projected by the same query, so consumers can interpret the visibility timestamp in the database clock domain instead of subtracting the application host's wall clock.

Caller-controlled transaction

auto transaction = client->begin_transaction();
const auto updated = transaction.execute(
    "UPDATE orders SET state = \$1 WHERE order_id = \$2::bigint",
    {std::string{"paid"}, std::string{"42"}});
const auto event_id = transaction.send(
    pgmq::QueueName{"order_events"}, pgmq::Json{{"order_id", 42}});
(void)updated;
(void)event_id;
transaction.commit();

No SDK method commits a caller-owned transaction. If an active Transaction leaves scope, its destructor attempts rollback. A successful explicit commit() or rollback() returns the pinned connection to the pool immediately, so later client calls in the same C++ scope do not wait for the transaction object's destructor.

Transactional worker

pgmq::QueueWorkerConfig config{pgmq::QueueName{"order_jobs"}};
config.mode = pgmq::ProcessingMode::transactional;
config.concurrency = 4;
config.max_in_flight = 4;
config.transactional_handler =
    [](const pgmq::Task& task,
       pgmq::WorkerTransaction& transaction,
       pgmq::HandlerContext&) {
      const auto inserted = transaction.execute(
          "INSERT INTO fulfilled_orders(order_id) VALUES (\$1::bigint) "
          "ON CONFLICT DO NOTHING",
          {std::to_string(
              task.message.body.at("order_id").get<long long>())});
      (void)inserted;
      return pgmq::HandlerResult::success_delete();
    };

The claim, business statement, acknowledgement, and commit use one PostgreSQL transaction. This mode is intentionally for short database-only work. The handler receives a non-owning WorkerTransaction view with only execute/send methods; direct commit, rollback, delete, archive, and visibility methods are deliberately absent to prevent accidental misuse. execute() still accepts trusted arbitrary SQL. The handler contract forbids transaction-control statements and SQL that acknowledges or changes the current task; this API is not a SQL security sandbox.

Error handling

try {
  const auto metrics = client->metrics(queue);
  consume(metrics);
} catch (const pgmq::Error& error) {
  log_database_error(pgmq::to_string(error.category()),
                     error.sqlstate(),
                     error.operation(),
                     error.detail());
  if (error.retryable()) {
    schedule_controlled_retry();
  }
}

SDK validation and operational failures use pgmq::Error; PostgreSQL SQLSTATE and operation context are retained when available. Invalid QueryResult row/column access uses std::out_of_range, and application code that directly uses nlohmann/json retains that library's exception behavior.