- PgQueuer follows semantic versioning from v1.0.0 onward.
- Schema changes are applied by
pgq install/pgq upgrade. Run the upgrade before starting workers on a new version;QueueManager.run()verifies the schema at startup and refuses to boot against an old one. - Identifiers below use the default unprefixed names; substitute your own if
you set
--prefix/PGQUEUER_PREFIX. - The top section is the next release. Add to it under the version the change warrants and cut the tag from it when it ships. Release dates live in the git tags, so a section is written once and never revisited.
--dry-runonpgq install,uninstall,upgrade,durabilityandautovac. Usepgq sql installand its siblings, which print the same SQL.pgq upgrade --durability/-d. It was already ignored; usepgq durabilityto change durability.pgqueuer.domain.settings.add_prefix().DBSettingsappliesPGQUEUER_PREFIXitself; useDBSettings(prefix=...)or the environment variable.Driver.tmproperty andTaskManagerPortprotocol. Drivers manage background tasks privately without exposing a task manager port;PsycopgDriveruses an internal task set for its NOTIFY watcher (#892).
- Index
pgqueuer_picked_heartbeat_idxonpgqueuer (heartbeat) WHERE status = 'picked'. Dequeue's stale-job check used to read every picked row to find the few with a timed-out heartbeat; now it reads only those.pgq upgradebuilds it with a plainCREATE INDEX, which blocks writes topgqueuerfor the build. Heartbeat updates now also write this index. enqueue(..., on_conflict="update_priority"): a duplicatededupe_keyraises the priority of the job that holds it, and never lowers it, instead of raisingDuplicateJobError. It returns that job's id. See the reliability guide.
- PgQueuer reads its
PGQUEUER_*table and channel settings from the environment once per process and shares them, instead of on every use. The default channel ofQueueManagerandPgQueuernow follows aPGQUEUER_PREFIXset afterimport pgqueuer; it used to be fixed at import.
- PostgreSQL 13 support. It reached end of life in November 2025, and CI now tests PostgreSQL 14 to 18. If you run PostgreSQL 13, stay on v1.5.0.
- Releases no longer publish the
ghcr.io/janbjorge/pgqueuer-webandghcr.io/janbjorge/pgqueuer-prometheusDocker images. The tags already pushed,1.5.0andlatest, stay pullable but get no newer versions. Build fromtools/web/Dockerfileandtools/prometheus/Dockerfileinstead; see the Docker images guide. - The
devanddocsextras. They held PgQueuer's own test, lint, and docs tooling and are now uv dependency groups, which are not published. Install the runtime extras you use, such aspgqueuer[asyncpg], instead.
- Reads the installed schema from
pg_catalog, compares it with the schema this release declares, and runs only the difference (ADR-0016). A current database runs nothing:PgQueuer schema is already up to date.v1.4.0 re-ran a fixed script every time, which rewrotepgqueuer_statisticsunderACCESS EXCLUSIVEand dropped and rebuiltpgqueuer_log_not_aggregated. - Run
pgq upgrade --planfirst. It prints the statements and thenote:lines (index rebuilds, table rewrites) and applies nothing. A plainpgq upgradeprints its notes only after it has applied. - Every database coming from v1.4.0 gets:
ALTER TABLE pgqueuer_statistics ALTER COLUMN created SET DEFAULT date_trunc('sec', now()). Catalog only; stored rows are unchanged. The old default stored the wrong instant when an insert left outcreatedand the sessionTimeZonewas not UTC.- on PostgreSQL 14+, a one-time rebuild of
pgqueuer_statistics_unique_count, because PostgreSQL stores v1.4.0's key ascreated AT TIME ZONE 'UTC'and the declaration saystimezone('UTC', created). It is preceded by a fold of duplicate statistics buckets, a full scan that changes nothing on a v1.4.0 table. The build holds aSHARElock that blocks writes topgqueuer_statisticsfor a time that scales with row count. - if it ever ran
pgq upgrade, a drop of the retiredpgqueuer_heartbeat_id_id1_idx, which everypgq upgradeup to v1.4.0 created. BriefACCESS EXCLUSIVElock onpgqueuer.
- Depending on its history, a database can also get:
- missing labels, tables, columns, indexes, the trigger function and the trigger, or everything in an empty database;
- a rebuild of any other index whose definition differs from the declared
one (
SHARElock, reported as anote:). The replacement is built beside the old index aspgq_rebuild_*and swapped in once the build succeeds, so a failed build leaves the old index in place. An upgrade interrupted before the swap is finished by the next one; int4id columns and sequences widened toBIGINT(still gated by--widen-id/--no-widen-id) and statisticsstatusmoved off the pre-v0.27 enum. Both rewrite the table underACCESS EXCLUSIVE(reported as anote:). With--no-widen-id, each id column and sequence left narrow is anote:; v1.4.0 skipped them silently;NOT NULLandDEFAULTreset on PgQueuer's own columns, reverting hand edits;- the retired
pgqueuer_statistics_statusenum dropped, andNOT NULLrelaxed on the retiredtime_in_queuecolumn (reported as anote:, never dropped).
- Notes also flag a missing primary key, or an id column whose kind (serial,
identity, plain) differs from the declared one, e.g. after its
nextvaldefault was dropped. Nothing is planned for either, so the summary still readsalready up to date. - Never changes durability. A mismatch with
PGQUEUER_DURABILITYis anote:that offers setting the variable to match, orpgq durability(which rewrites the table). - A column type it has no conversion for stops it with exit code
1and one line naming the column; alter it by hand and re-run. v1.4.0 ignored such a column. - Upgrades of one installation serialize on an advisory lock, which
pgq upgradeholds on its single connection. OverAsyncpgPoolDriverthe lock is released as soon as it is taken, so it serializes nothing. pgq sql upgradestill prints a script for operators who apply DDL themselves. It can't see the database, so it re-states every object behindIF NOT EXISTSand covers less than the connected upgrade. Compared with v1.4.0's script:- it no longer rewrites
pgqueuer_statisticson every apply, and drops a redefined index only when its definition differs; - it drops and recreates the
tg_pgqueuer_changedtrigger on every apply. Applied outside one transaction, changes made in between send no notification; - on PostgreSQL 14+, its first apply to a v1.4.0 database drops
pgqueuer_statistics_unique_countand then rebuilds it, not beside the old one. Applied outside one transaction, log aggregation fails until the build finishes. Every apply also re-runs the statistics fold, a full scan.
- it no longer rewrites
pgq upgrade --planprints the statements this database needs, headed by a comment, and applies nothing. Notes and the summary go to stderr.pgq stale: picked jobs whose heartbeat is older than--thresholdseconds (default 300).pgq workers: queue managers currently holding picked jobs.pgq backlog: count and age ofqueuedjobs per entrypoint.--jsononpgq failed,pgq stale,pgq workersandpgq backlog.pgq failed --jsonreports payloads by size (payload_bytes) only.Queries.plan_upgrade(),Queries.apply_upgrade(),Queries.schema_is_installed()andQueries.schema_lock()(the advisory lock above).Queries.upgrade()logs its notes as warnings.SchemaDriftErrorinpgqueuer.domain.errors(not re-exported frompgqueuer.errors): raised byQueries.upgrade(),plan_upgrade()andapply_upgrade()for a column type the planner can't convert.- Docs: CLI exit codes, and an onboarding guide for coding agents.
pgq upgradereportsApplied N statements.orPgQueuer schema is already up to date.instead ofUpgraded PgQueuer schema.Scripts matching the old line need updating.pgq upgrade --durability/-dis deprecated: hidden from--help, prints a warning, and is still ignored. It will be removed in v2.0; usepgq durability.pgq installrefuses a database that already has this installation, even a partial one, and exits1naming the prefix and schema. Previously a rawDuplicateObjectErrortraceback. Usepgq upgrade, which also installs into an empty database.pgq sql installoutput starts with a comment line naming the release, prefix, schema and durability, so checked-in install SQL changes with every release.pgq queueprints the duplicatededupe_keyerror on stderr, not stdout.enqueue()rejects one value next to a list of entrypoints, which v1.4.0 accepted:enqueue(["a", "b"], None, [0, 0])now raisesValueErrorforpayload. Pass one value per job, e.g.[None, None].- MCP
stale_jobsandqueue_agereturn numeric fields as JSON numbers, not strings, on PostgreSQL 14+. InsightsService.stale_jobs(threshold=timedelta(0))returns every picked job. Previously a zero threshold fell back to the 5-minute default.- Workers flush queued heartbeats every eighth of
heartbeat_timeout, not every quarter; each job still queues one every half. With latency under 0.2 of the timeout, one failed heartbeat write no longer lets a worker re-pick a running job. About twice as many batched heartbeatUPDATEs; rows written per job unchanged. Reasoning in the heartbeat guide.
- Upgrading a v0.18 database with statistics history now leaves log
aggregation working. Rows split on
time_in_queueare folded into one per bucket, summingcount, beforepgqueuer_statistics_unique_countis rebuilt, andNOT NULLontime_in_queueis relaxed. v1.4.0 left the pre-v0.19 index and theNOT NULLin place.pgq sql upgradedoes the same. AsyncpgPoolDriveraccepts a one-connection pool. The two-connection minimum is checked when it starts listening (which holds one connection for good), not at construction.- With
PGQUEUER_SCHEMAset, the dashboard's overview, entrypoints, jobs, job detail and system pages read that schema's tables. PreviouslyUndefinedTableErrorwhen the schema was not onsearch_path. - A worker no longer starts a second copy of a job it is still running when the job's heartbeat reaches the database late. Its own next dequeue re-picked the job as stale.
enqueue()raisesValueErrornaming the argument when batch lists differ in length, and inserts nothing. Previously PostgreSQL padded a short list withNULL(a shortpayloadlist queued jobs without a payload), and the in-memory adapter inserted the first jobs and then raisedIndexError.
- Internal
TTLCache(pgqueuer.core.cache). Drain shutdown probesqueued_workdirectly, without the 250ms TTL. Queries.qbe.build_widen_id_column_query()andbuild_widen_id_sequence_query().pgq upgradeplans the widening.QueryBuilderEnvironment.build_upgrade_queries(). UseQueries.plan_upgrade()/apply_upgrade(), orpgq sql upgradefor the script.- MCP server: the
Tail,TimePeriod,StaleThreshold,LimitandOffsettype aliases, andPgQueuerDatabase.settings/.qbs. Tool schemas are unchanged.
-
concurrency_limitis enforced by capacity slots instead of a row count, closing an overshoot when several workers dequeued concurrently (#761, #774, #777). -
Adds one column and one partial unique index:
ALTER TABLE pgqueuer ADD COLUMN IF NOT EXISTS slot BIGINT; CREATE UNIQUE INDEX IF NOT EXISTS pgqueuer_picked_slot_idx ON pgqueuer (entrypoint, slot) WHERE (status = 'picked' AND slot IS NOT NULL);
-
Migrate before deploying workers.
verify_structure()requires both, so workers on an unmigrated database raiseRuntimeErroron boot. -
The index build holds a
SHARElock that blocks writes to the queue table; on a large table run it in a low-traffic window. -
Jobs already
pickedduring the migration keepslot IS NULL, stay under the count gate, and drain out normally. -
Workers must agree on an entrypoint's limit. If they disagree the highest wins; nothing validates or rejects the mismatch.
- Dequeue no longer overshoots
concurrency_limit: candidates are windowed before locking, soSKIP LOCKEDcan't slide down the backlog past the cap (#774). --restart-on-failurerestarts. Each supervisor cycle owns its shutdown event, so a manager settingshutdownon exit no longer ends the loop. Workers that used to exit on failure now restart in place; review external restart or alerting that relied on the process dying.
Job.slot: the capacity seat a picked job holds underconcurrency_limit;Nonefor unlimited entrypoints and unpicked rows. Typed as the newSlot; a plainintat runtime.- Identity types, plain values at runtime:
QueueEntrypoint(str): entrypoint name onJob,Log, the statistics models andQueueManager.entrypoint_registry. Schedules keepCronEntrypoint.QueueManagerId(uuid.UUID): worker id onJob,StaleJob,ActiveWorkerandQueueManager.queue_manager_id.HealthCheckId(uuid.UUID):HealthCheckEvent.id, distinct fromQueueManagerIdat type level.
- All four live in
pgqueuer.domain.types, re-exported frompgqueuer.typesandpgqueuer.models.
- Type annotations only, no runtime change. Direct callers need the wrapper to
pass mypy:
dequeue(),queued_work(),eligible_queued_work(),next_deferred_eta()andQueryQueueBuilder.build_dequeue_query()takeQueueEntrypoint(QueueEntrypoint("name")), andQueueManager.entrypoint_registryis keyed by it.enqueue(),clear_queue()and@entrypointstill takestr.dequeue()andbuild_dequeue_query()takequeue_manager_id: QueueManagerId(QueueManagerId(uuid.uuid4())).notify_health_check()takesHealthCheckId.
- mypy runs in
strictmode withAnybanned inpgqueuer/. Tightened public signatures:Driver.fetch()/execute()take*args: object; rows arelist[dict[str, object]];Job.headersandTracebackRecord.additional_contextaredict[str, object];Context.resources/ScheduleContext.resourcesareMutableMapping[str, object], so readers narrow withisinstance;TracingProtocol.trace_publish()yieldsdict[str, object];load_factory()returns aFactoryprotocol;EventRoutertakes three typed handlers instead of aregister()decorator;ScheduleExecutorFactoryParametersusesCronEntrypoint/CronExpression.
Job.headersalso accepts an already-parsed dict, not only JSON text.load_factory()raisesTypeErrorfor a non-callablemodule:attr, andpgqcommands raiseTypeErrorwhen a factory yields something other thanQueries. Previously a laterAttributeError.QueryQueueBuilder.build_dequeue_query()andbuild_log_statistics_query()are keyword-only and return aComposedQuery(.sql,.args), not a SQL string. Internal, but reachable viaQueries.qbq.- Dequeue SQL is assembled per active gate by
SqlComposer; unused concurrency gates no longer render (ADR-0024). is_unique_violation()classifies driver errors by SQLSTATE instead of importing asyncpg and psycopg. Drivers without.sqlstateare no longer recognised.dequeue()treats a lost slot race (SQLSTATE23505or40P01) as an empty batch; the poll loop retries.
- Pin published Docker images to the released PgQueuer version (#717).
- Publish multi-arch Docker images to GHCR (#716).
- Built-in web dashboard for queue insights and management (#691).
- First-class Postgres schema support.
prefixis now aDBSettingsfield andadd_prefix()is deprecated (#703, #704). pgq sqlcommand group;--dry-rundeprecated (#705).- Periodic log→statistics aggregation, no cron required (#707).
connect_psycopg,ConnectionSettings, and shared pool factories. Single-connection factories are now context managers.
- Dequeue caps picks to the remaining per-entrypoint and per-worker slots, and drops the batch cap to the tightest entrypoint limit (#696, #697).
- Shutdown dispatches the remaining batch instead of stranding picked rows
(#693);
run()tears down lifecycle tasks on failure (#694). - Drain confirms an empty queue with an uncached count before exiting.
- Dequeue timeout probes eligible queued work before the deferred ETA (#695).
- Aggregation index rebuilt on upgrade (#670); the aggregation tick is skipped when the worker did no work (#713).
- In-memory adapter dequeues from priority heaps instead of a full scan.
enqueue(on_conflict=...)to control dedupe-key conflicts (#681).- Dequeue preserves priority when merging queued and stale jobs.
- Schema change. Ships the
BIGINTidwidening described under v1.0.0 (#676), lifting the ~2.1B lifetime-enqueue ceiling on the queue, statistics and schedules tables. pgq upgraderewrites each table under anACCESS EXCLUSIVElock. See the v1.0.0 warning, or runpgq upgrade --no-widen-idand widen out-of-band.
- Context injection is auto-detected from the handler signature (#635).
--heartbeat-timeoutexposed onpgq run(#675).- Schema change. Dequeue rewritten around per-entrypoint
LATERALlookups, backed by new entrypoint-leading indexes (#668). Applied bypgq upgrade. - Upgrade migration uses the configured status type rather than the default.
- Tracing: one header per entrypoint when the SDK is absent;
SentryTracing.trace_processre-raises instead of swallowing.
- Stale-job recovery no longer blocked once an entrypoint is at its concurrency limit (#633).
- Jobs canceled by
SIGTERMare recorded ascanceled(#631).
- Use the selector event loop on Windows for psycopg (#629).
- First stable release: a smaller API surface, an enforced hexagonal architecture, and deprecated code paths removed.
- Upgrading from v0.26.x is a one-time migration. The upgrade guide has before/after code for the common cases.
- Most breaks raise at decoration or startup, so a test run surfaces them.
-
Handlers must be
async def. A plaindefentrypoint raisesTypeErrorat decoration. Wrap blocking or CPU-bound calls inawait asyncio.to_thread(fn, ...). Handlers always run in an async context, so sync handlers that usedanyio.from_thread.run()to call async code can drop it.SyncEntrypointandSyncContextEntrypointare deleted;EntrypointisAsyncEntrypoint | AsyncContextEntrypoint. -
Factories must be async context managers.
pgq runrejects plainasync deffactories and sync@contextmanagerones with aTypeError. Add@asynccontextmanager,yield pgqinstead ofreturn, and put cleanup after theyield(runs on graceful shutdown).run_factoryis replaced byvalidate_factory_result, which validates but no longer converts. -
Removed exports.
Removed Replacement pgqueuer.executors.SyncEntrypointAsyncEntrypointpgqueuer.executors.SyncContextEntrypointAsyncContextEntrypointpgqueuer.factories.run_factory()validate_factory_result() -
Schema. Retry and hold need two additions, applied by
pgq installandpgq upgrade. If you manage DDL yourself, apply them before starting upgraded workers:ALTER TABLE pgqueuer ADD COLUMN IF NOT EXISTS attempts INT NOT NULL DEFAULT 0; ALTER TYPE pgqueuer_status ADD VALUE IF NOT EXISTS 'failed';
-
requests_per_secondremoved. Observed RPS diverged from real throughput under load, so throttling was unpredictable. Gone: the parameter on@pgq.entrypoint()/QueueManager.entrypoint(),QueueManager.observed_requests_per_second(),RequestsPerSecondEventand therequests_per_second_eventtype,RequestsPerSecondBuffer,notify_entrypoint_rps(), andEntrypointStatistics.samples. Useconcurrency_limit. -
Deprecated executor fields removed.
channel,connection,queries,shutdownonEntrypointExecutorParameters;connection,queries,shutdownonScheduleExecutorFactoryParameters. Built-in executors never used them; passing them now raisesTypeError. -
PGChannelremoved (it was= Channel) frompgqueuer.domain.types,pgqueuer.modelsandpgqueuer.types. UseChannel. -
DBSettings.statistics_table_status_typeremoved. Only thepgq uninstallteardown used it; that now callsadd_prefix(). -
AbstractScheduleExecutor.execute()takescontext: ScheduleContextas a second parameter. Ignore it if you don't need shared resources. -
Tracing singleton moved.
TracingConfig,TRACERandset_tracing_class()moved frompgqueuer.adapters.tracingtopgqueuer.ports.tracing(see 18). -
Package-root shim modules removed. Import from the canonical location:
Removed module Canonical import pgqueuer.bufferspgqueuer.core.bufferspgqueuer.cachepgqueuer.core.cachepgqueuer.clipgqueuer.adapters.cli.clipgqueuer.completionpgqueuer.core.completionpgqueuer.heartbeatpgqueuer.core.heartbeatpgqueuer.helpersRemoved (see Other changes) pgqueuer.listenerspgqueuer.core.listenerspgqueuer.logconfigpgqueuer.core.logconfigpgqueuer.qbpgqueuer.domain.settings/pgqueuer.adapters.persistence.qbpgqueuer.query_helperspgqueuer.adapters.persistence.query_helperspgqueuer.retriesRemoved (see 14) pgqueuer.supervisorpgqueuer.adapters.cli.supervisorpgqueuer.tmpgqueuer.core.tmpgqueuer.tracingpgqueuer.ports.tracing+pgqueuer.adapters.tracing.*Public API shims are unchanged:
pgqueuer.models,pgqueuer.queries,pgqueuer.executors,pgqueuer.errors,pgqueuer.db,pgqueuer.qm,pgqueuer.sm,pgqueuer.applications,pgqueuer.factories,pgqueuer.types. -
serialized_dispatchremoved from@pgq.entrypoint(),QueueManager.entrypoint(),PgQueuer.entrypoint()andEntrypointExecutorParameters. Useconcurrency_limit=1, enforced by the database. -
concurrency_limitis global. The dequeue query enforces it across all workers instead of per-process semaphores:concurrency_limit=5means 5 jobs fleet-wide, not 5 per worker. -
RetryManagerremoved (pgqueuer.core.retries, an internal buffer helper).TimedOverflowBufferre-queues on flush failure itself. -
Buffer callbacks replaced by port injection.
TimedOverflowBufferno longer takescallback; subclasses overrideflush_items()and inject a repository port. Only affects subclasses ofTimedOverflowBuffer,JobStatusLogBufferorHeartbeatBuffer. -
retry_timerreplaced by a globalheartbeat_timeout. Removed from@pgq.entrypoint(),QueueManager.entrypoint(),PgQueuer.entrypoint()andEntrypointExecutorParameters. Passheartbeat_timeouttoQueueManager.run()/PgQueuer.run()(default 30 seconds); with different per-entrypoint timers, use the maximum. Heartbeats go out at half the timeout. Stale-job retry is always on;retry_timer=0used to disable it. -
RetryWithBackoffEntrypointExecutorremoved, withMaxRetriesExceeded,MaxTimeExceededand theasync-timeoutdependency. UseDatabaseRetryEntrypointExecutor, which retries in the database and survives worker restarts. -
Tracing adapter re-exports removed. Import
TracingConfig,TRACER,set_tracing_class()andTracingProtocolfrompgqueuer.ports.tracing, notpgqueuer.adapters.tracing. -
log_statistics(tail=...)renamed tolimit=.... Positional calls are unaffected. -
CLI connection options and
dsn()removed. Gone:--pg-host,--pg-port,--pg-user,--pg-database,--pg-password,--pg-schema, the matchingAppConfigfields, anddsn()frompgqueuer.adapters.driversandpgqueuer.db. asyncpg and psycopg read the libpq variables (PGHOST,PGPORT,PGUSER,PGPASSWORD,PGDATABASE) themselves; set a schema withPGOPTIONS="-csearch_path=myschema", and in code callasyncpg.connect()orpsycopg.connect("")without a DSN. Kept:--pg-dsn/PGDSNand--prefix/PGQUEUER_PREFIX. -
QueueManagerandSchedulerManagertakequeries, not a connection. UseQueueManager(Queries(driver)),SchedulerManager(Queries(driver))andCompletionWatcher(driver, queries=Queries(driver)). Replaceqm.connection/sm.connectionwithqm.queries.driver/sm.queries.driver.PgQueuer(driver)is unchanged. -
Driver.tmreturnsTaskManagerPort(pgqueuer.ports.driver), notTaskManager. Only matters if a custom driver annotatedtmexplicitly; change the annotation or drop it. -
Async
Drivers must implementnotify(channel, payload).NOTIFYmoved from the queries layer to the drivers, which use their native, parameterized APIs;pgqueuer.adapters.persistence.qb.build_notify_query()is removed. Built-in async drivers are updated;SyncPsycopgDriveris unaffected. A psycopg implementation:async def notify(self, channel: str, payload: str) -> None: async with self.connection.cursor() as cur: await cur.execute("SELECT pg_notify(%s, %s)", (channel, payload))
RetryRequested(from pgqueuer import RetryRequested). Raise it from a handler to re-queue the job instead of failing it, e.g.raise RetryRequested(delay=timedelta(seconds=30), reason="rate limited"). The job keeps its row and id,attemptsgoes up by one, and it is eligible again after the optional delay.job.attempts(default0) lets handlers implement their own backoff or give-up logic.DatabaseRetryEntrypointExecutor(exported frompgqueuer). Automatic exponential backoff for any handler, attached withexecutor_factory=lambda p: DatabaseRetryEntrypointExecutor(parameters=p, ...)on the entrypoint. An unhandled exception becomes aRetryRequestedwith a computed delay (RetryRequesteditself passes through), and aftermax_attemptsconsecutive failures the original exception is terminal. Defaults:max_attempts=5,initial_delay=timedelta(seconds=1),max_delay=timedelta(minutes=5),backoff_multiplier=2.0.on_failure="hold"on an entrypoint keeps terminally failed jobs in the queue table asstatus='failed'instead of deleting them; dequeue skips them. With the retry executor, a job is held only after its retries run out. Invalid values raiseValueErrorat decoration. Inspect and re-queue withpgq failed(25 by default,-n 100for more),pgq requeue 42 43 44, orqueries.list_failed_jobs(limit=25)andqueries.requeue_jobs(ids).- Factory arguments.
pgq run myapp:factory -- --region us-east-1passes everything after--to the factory aslist[str]. Factories without arguments still work. ScheduleContext(pgqueuer.models). A scheduled task registered withaccepts_context=Trueis called as(schedule, ctx: ScheduleContext)and getsctx.resources, likeContext.resourcesfor job handlers. Previously the only way was a closure overpgq.resources. Handlers withoutaccepts_contextstill take just the schedule.- Read-only MCP server.
pip install pgqueuer[mcp], thenpython -m pgqueuer.adapters.mcp. Eleven tools:queue_size,queue_table_info,queue_stats,throughput_summary,failed_jobs,queue_log,schedules,stale_jobs,active_workers,queue_age,schema_info. Connects with the libpq variables orcreate_mcp_server(dsn="postgresql://..."). Works with Claude Desktop, Claude Code, Cursor and other MCP clients; see the MCP Server docs.
- Deferred (
execute_after) jobs wake up within ~100ms of becoming eligible instead of waiting up todequeue_timeout(default 30s); the manager shortens its wait to the next deferred job's ETA. - A job becoming eligible between the dequeue attempt and the ETA query no longer causes a full-timeout sleep. With queued work but no future deferred jobs, the manager polls every 100ms.
- The in-memory adapter releases the
dedupe_keyof a job held asfailed, and validates the'failed'enum value at startup. Queries.peak_schedule()andScheduleRepositoryPort.peak_schedule()are renamed topeek_schedule(), fixing the misspelling.idcolumns widened toBIGINT(#671). Theint4 SERIALkeys on the queue, statistics and schedules tables cap at 2,147,483,647, after which everyenqueuefails. Fresh installs useBIGSERIAL;pgq upgradewidens existing columns and their legacyAS INTEGERsequences in place.- This takes an
ACCESS EXCLUSIVElock. Rewriting the table and rebuilding its indexes blocks everything on it, plainSELECTincluded; on a large or bloated table that is seconds to minutes. While the migration waits for the lock, new queries queue behind it, so one long-running transaction can freeze the queue for the whole wait. Runpgq upgradein a maintenance window, or usepgq upgrade --no-widen-idand widen out-of-band. It is idempotent and a no-op once everything isBIGINT.
- This takes an
async-timeoutis no longer a runtime dependency (still a dev/test dependency until the tests move toasyncio.timeout).pgqueuer/core/helpers.pydeleted; its functions moved to their modules (listeners.py,executors.py,query_helpers.py, etc.).- Dead internals removed:
ExponentialBackoff,timer(),retry_timer_buffer_timeout(), andEntrypointStatisticsinpgqueuer.domain.models. - Added
has_function()andhas_trigger()toSchemaManagementPort, andOnFailureto thepgqueuer.typesre-exports. utc_now()lives in one place,pgqueuer.domain.models.- Scheduler heartbeat updates are batched.
asyncio.Futurestate transitions are guarded against races, and thelistener_healthytimeout raisesFailingListenerErrordirectly.PgQueuer.__post_init__is the only place concrete adapters are built;QueueManagerandSchedulerManagerno longer createQueries.- Import-linter checks the domain, ports, core and metrics layers separately.
Within core, only
core.applications(the composition root) keeps adapter import exceptions. - Docs: OpenTelemetry section in the tracing guide;
examples/callable_factory/removed (seeexamples/consumer.py). - Repo: PR titles linted for Conventional Commits, agent guidance consolidated
in
AGENTS.md, test suite trimmed from 695 to 599 tests, SVG logo, ASCII diagrams instead of Mermaid, docs CI runner label fixed.