PostgreSQL Deployment
Ratchet on PostgreSQL 14+.
Prerequisites
- PostgreSQL 14 or later
- UTF-8 encoding
- psql CLI tool
Schema setup
Apply DDL
psql -U ratchet -d mydb -f stores/ratchet-store-postgresql/src/main/resources/ddl/postgresql-schema.sqlOr copy into your migration tool:
cp postgresql-schema.sql src/main/resources/db/migration/V1__ratchet_schema.sql
flyway migrateVerify installation
\dt scheduler_*You should see:
ratchet_schema_versionscheduler_jobscheduler_job_queuescheduler_business_key_reservationscheduler_job_tagscheduler_job_executionscheduler_job_logscheduler_job_archivescheduler_batchscheduler_batch_metricsscheduler_nodescheduler_lockscheduler_resource_limitscheduler_resource_permitscheduler_workflow_conditionscheduler_recurring_jobscheduler_recurring_job_archive
Configuration
DataSource
Configure your data source for PostgreSQL:
<!-- persistence.xml -->
<persistence-unit name="your-application-pu" transaction-type="JTA">
<jta-data-source>java:/RatchetDS</jta-data-source>
<class>run.ratchet.store.entity.JobEntity</class>
<class>run.ratchet.store.entity.JobExecutionEntity</class>
<class>run.ratchet.store.entity.ResourceLimitEntity</class>
<class>run.ratchet.store.entity.BatchMetricsEntity</class>
<class>run.ratchet.store.entity.WorkflowConditionEntity</class>
<class>run.ratchet.store.entity.ArchivedJobEntity</class>
<class>run.ratchet.store.entity.NodeEntity</class>
<class>run.ratchet.store.entity.JobLogEntity</class>
<class>run.ratchet.store.entity.ResourcePermitEntity</class>
<class>run.ratchet.store.entity.BatchEntity</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL14Dialect" />
</properties>
</persistence-unit>The PostgreSQL store does not require a fixed persistence-unit name. By default it uses the deployment's unnamed @PersistenceContext. If your application has multiple persistence units, provide a CDI alternative for RatchetEntityManagerProvider:
@ApplicationScoped
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
public class RatchetPuProvider implements RatchetEntityManagerProvider {
@PersistenceContext(unitName = "your-application-pu")
EntityManager em;
@Override
public EntityManager getEntityManager() {
return em;
}
}WildFly CLI
/subsystem=datasources/data-source=RatchetDS:add( \
jndi-name=java:/RatchetDS, \
driver-name=postgresql, \
connection-url=jdbc:postgresql://localhost:5432/ratchet, \
user-name=ratchet, \
password=secret, \
min-pool-size=5, \
max-pool-size=20, \
valid-connection-checker-class-name=org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker)Connection string
jdbc:postgresql://localhost:5432/ratchetAdvanced features
SKIP LOCKED (optimistic claiming)
Ratchet uses PostgreSQL's SKIP LOCKED clause for lock-free job claiming:
SELECT * FROM scheduler_job_queue
WHERE status = 'PENDING'
AND scheduled_time <= statement_timestamp()
ORDER BY priority + FLOOR(GREATEST(0, EXTRACT(EPOCH FROM (statement_timestamp() - scheduled_time)) / 60) / ?) DESC,
scheduled_time ASC,
job_id ASC
LIMIT ?
FOR UPDATE SKIP LOCKED;Multiple Ratchet nodes can claim different jobs simultaneously without blocking each other.
Active business-key uniqueness
Active-key uniqueness is enforced by a dedicated scheduler_business_key_reservation table, not by an index on scheduler_job. The reservation table holds one row per active business key, with business_key as its primary key:
CREATE TABLE IF NOT EXISTS scheduler_business_key_reservation
(
business_key TEXT NOT NULL,
owner_job_id uuid NOT NULL,
owner_table TEXT NOT NULL,
CONSTRAINT pk_scheduler_business_key_reservation PRIMARY KEY (business_key),
CONSTRAINT chk_bk_owner_table CHECK (owner_table IN ('QUEUE', 'RECURRING'))
);A duplicate active business key fails against pk_scheduler_business_key_reservation. The business_key column on scheduler_job is observability-only and carries a plain (non-unique) idx_job_business_key index.
Generated columns
Target class and method name are extracted from the JSONB payload as generated columns for indexing:
target_class TEXT GENERATED ALWAYS AS (payload ->> 'target') STORED
method_name TEXT GENERATED ALWAYS AS (payload ->> 'method') STOREDCHECK constraints
PostgreSQL uses CHECK constraints for data validation instead of MySQL's ENUM types. Live status is tracked on scheduler_job_queue (chk_queue_status), while scheduler_job records only the terminal status (chk_terminal_status):
-- scheduler_job (cold metadata + terminal fields)
CONSTRAINT chk_terminal_status CHECK (terminal_status IS NULL OR terminal_status IN ('SUCCEEDED', 'FAILED', 'CANCELED'))
CONSTRAINT chk_job_type CHECK (job_type IN ('SINGLE', 'RECURRING', 'BATCH_PARENT', 'BATCH_CHILD', ...))
CONSTRAINT chk_job_priority CHECK (priority BETWEEN 0 AND 4)
-- scheduler_job_queue (live claim path)
CONSTRAINT chk_queue_status CHECK (status IN ('PENDING', 'RUNNING', 'PAUSED', 'WAITING'))Performance tuning
Connection pooling
Use PgBouncer in transaction mode for efficient connection pooling:
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25Shared buffers
Set to 25% of available RAM:
ALTER SYSTEM SET shared_buffers = '4GB';Restart PostgreSQL after changing.
Work memory
Increase for complex queries:
ALTER SYSTEM SET work_mem = '256MB';
SELECT pg_reload_conf();Effective cache size
Help the planner estimate cache hit rates (set to 75% of available RAM):
ALTER SYSTEM SET effective_cache_size = '12GB';
SELECT pg_reload_conf();Autovacuum tuning
Ratchet performs frequent updates and deletes on the hot scheduler_job_queue table. Tune autovacuum to keep up:
ALTER TABLE scheduler_job_queue SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02
);Monitoring
Monitor job queue
-- Live statuses (PENDING/RUNNING) are on scheduler_job_queue; FAILED is terminal and
-- survives as terminal_status on the cold scheduler_job row after the queue row is
-- deleted. UNION the two to count both in one pass.
SELECT
COUNT(*) AS total,
COUNT(CASE WHEN status = 'PENDING' THEN 1 END) AS pending,
COUNT(CASE WHEN status = 'RUNNING' THEN 1 END) AS running,
COUNT(CASE WHEN status = 'FAILED' THEN 1 END) AS failed
FROM (
SELECT status FROM scheduler_job_queue
UNION ALL
SELECT terminal_status AS status FROM scheduler_job WHERE terminal_status IS NOT NULL
) all_jobs;Query performance
Enable pg_stat_statements and find slow queries:
SELECT query, calls, mean_exec_time, max_exec_time
FROM pg_stat_statements
WHERE query LIKE '%scheduler_%'
ORDER BY mean_exec_time DESC
LIMIT 10;Index usage
Verify indexes are being used:
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE indexname LIKE 'idx_%'
AND tablename LIKE 'scheduler_%'
ORDER BY idx_scan ASC;Active nodes
SELECT node_id, heartbeat_ts, started_at
FROM scheduler_node
WHERE heartbeat_ts > NOW() - INTERVAL '30 seconds'
ORDER BY started_at;Maintenance
Vacuum and analyze
Regular maintenance is important for tables with frequent updates:
# Manual vacuum during off-peak
psql -d ratchet -c "VACUUM ANALYZE scheduler_job;"Reindex
Rebuild indexes to reclaim space after heavy updates:
REINDEX TABLE scheduler_job;
REINDEX TABLE scheduler_job_execution;
REINDEX TABLE scheduler_job_archive;Monitor table bloat
SELECT
relname AS table_name,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables
WHERE relname LIKE 'scheduler_%'
ORDER BY n_dead_tup DESC;Backup and recovery
Logical backup
pg_dump -Fc -v ratchet > ratchet-backup.dumpPoint-in-time recovery
pg_basebackup -D /backup -Ft -zRestore
pg_restore -d ratchet ratchet-backup.dumpHigh availability
Streaming replication
For HA, use PostgreSQL streaming replication:
-- On primary
ALTER SYSTEM SET wal_level = replica;
ALTER SYSTEM SET max_wal_senders = 5;
SELECT pg_reload_conf();Failover
Use Patroni or pg_auto_failover for automatic failover. Ratchet reconnects automatically when the connection pool detects a new primary.