Skip to content

fix(job): report a missing job schema from inside a PostgreSQL transaction - #4527

Merged
bpamiri merged 2 commits into
developfrom
peter/jobs-pre-cut-polish
Oct 9, 2026
Merged

bpamiri merged 2 commits into
developfrom
peter/jobs-pre-cut-polish

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Pre-4.2.0 polish for background jobs: one fix, two specs and one doc line.

Fix: a missing schema inside a PostgreSQL transaction

With jobsAutoCreateTables = false, Job.$ensureJobTable() probed wheels_jobs with SELECT … WHERE 1=0 and asked the catalog only after the probe failed.

On PostgreSQL and CockroachDB, a failed statement aborts the transaction. So inside one, the catalog check then failed too: the caller saw ERROR: current transaction is aborted, commands ignored until end of transaction block instead of Wheels.Job.SchemaMissing (with its pointer to wheels jobs install).

On those databases the catalog is now asked first. JobSchema.hasTable() is a plain read, and the database type comes from the cached JobSchema, so there's no extra cfdbinfo call. Other databases don't abort a transaction on a failed statement and are unchanged.

New spec in JobSchemaSpec: "with jobsAutoCreateTables = false, says the schema is missing from inside a transaction too". Against the old Job.cfc on Lucee 7 + PostgreSQL it fails with exactly that aborted-transaction error.

Specs and docs

  • JobHeartbeatGraceSpec: "reclaims a job idle past the 30-second minimum when the setting is below it". The setting is 5, the last heartbeat was 45 s ago, and the job is reaped and requeued. The existing spec covers the other side: 15 s ago, inside the minimum, the job is left alone.
  • JobLifecycleHooksSpec: "stores NULL when perform() returns an empty string, as for nothing". HookProbeJob gets an empty mode for it.
  • Guide ("The stored result"): returning nothing or an empty string stores NULL, because Oracle can't tell the two apart.

Checked and already in place, so no change

  • $markInterrupted logs only when a row changed: the writeLog is guarded by local.changed > 0, the update's recordCount. See vendor/wheels/JobWorker.cfc, $markInterrupted.
  • SQL Server result column:
    • It is NVARCHAR(4000): JobSchema declares it unicodeString.
    • Auto-create, add-column and the wheels jobs install migration all emit NVARCHAR on SQL Server.
    • JobSchemaSpec asserts the type is nvarchar after running the migration.

Tests

The head includes a merge of develop (with #4526); the guide page builds, and the jobs suite passes on Lucee 7 + SQLite at the merged head (293).

Before the merge, jobs suite:

Engine Databases Result
Lucee 7 PostgreSQL, CockroachDB, SQLite, MySQL 291/0/0 each
BoxLang PostgreSQL, SQLite 291/0/0 each
Adobe 2025 SQLite 291/0/0
Adobe 2025 SQL Server 292/0/0

Also:

  • RustCFML (tools/rustcfml/run-suite.sh, TZ=UTC): exit 0, no new failures.
  • Complexity and docblock gates pass.
  • verify-docs on the guide page: 13/13.

A changelog fragment (fixed) covers the user-visible fix.

🤖 Generated with Claude Code

bpamiri and others added 2 commits October 9, 2026 02:06
…ction

With jobsAutoCreateTables = false, $ensureJobTable() probed wheels_jobs and asked the catalog
only after the probe failed. On PostgreSQL and CockroachDB a failed statement aborts the
transaction, so inside one the catalog check then failed with "current transaction is
aborted" instead of reporting Wheels.Job.SchemaMissing. There it now asks the catalog first.

Also, before the 4.2.0 cut:
- a spec that a job idle past the 30-second heartbeat-grace minimum is reclaimed when the
  setting is below it (the existing spec covers a job inside the minimum);
- a spec, and a guide line, that an empty-string perform() return is stored as NULL, like a
  return of nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri added this to the v4.2.0 milestone Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Wheels Test Results

111 585 tests   108 729 ✅  56m 11s ⏱️
  8 955 suites    2 856 💤
     15 files          0 ❌

Results for commit fca97c9.

@bpamiri

bpamiri commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review: head fca97c9. APPROVE (ready). One INFO on the floor spec.

(b) SchemaMissing inside a transaction on PostgreSQL / CockroachDB

  • Correct catalog query. JobSchema.hasTable() on PostgreSQL uses information_schema.tables WHERE table_schema = current_schema() AND LOWER(table_name) = 'wheels_jobs'. That follows the session's search_path and is case-insensitive on the stored name. CockroachDB takes the same branch ($detectDatabaseType() → postgresql).
  • No extra round trip on the normal path. The pre-check is !autoCreateEnabled() && databaseType() == "postgresql" && !hasTable(...). autoCreateEnabled() reads the app scope, so with auto-create on (the default) it short-circuits and makes no database call. databaseType() is cached per JobSchema and $jobSchema() per Job instance. With auto-create off on PostgreSQL, a call costs one catalog query before the probe; $ensureJobTable() runs once per worker ($tableVerified), so that's negligible.
  • Scoped to PostgreSQL on purpose. The other databases don't abort a transaction on a failed statement, so the existing probe-then-catalog order is fine there.
  • The RED is genuine. On the old code, inside transaction {} on PostgreSQL the probe fails and aborts the transaction, then the catalog check in the catch fails with "current transaction is aborted". The caller sees that, not SchemaMissing. The new spec asserts the type inside a rolled-back transaction and passes on all 10 PostgreSQL/CockroachDB legs (and on SQLite, where it was already right).

(a) Heartbeat-grace floor

  • What the new spec proves. Setting 5, last beat 45 s ago. claimTimeout = 3600 on the row means only the heartbeat grace can reap it, and it's reaped.
  • INFO. On its own this would also pass with no floor (45 > 5). It only pins the floor together with the existing "raises a grace below 30 seconds to 30" spec: 5 s setting, 15 s ago, not reaped, plus $heartbeatGraceSeconds() == 30. Between them the effective grace is bracketed to 15 < grace ≤ 45, which is adequate. A 31 s / 29 s pair would pin it exactly if you want it tighter.

(e) Empty result → NULL

  • $resultAssignment() binds null = !Len(text), so an empty string and "returned nothing" both store NULL.
  • HookProbeJob gains mode = "empty", and the spec asserts resultIsNull.
  • The guide sentence matches, including the Oracle reason.

(c) and (d), no change needed (spot-checked)

  • (c) JobWorker.$markInterrupted() logs only when local.changed > 0 (the recordCount of its guarded UPDATE).
  • (d) The SQL Server result column is NVARCHAR(4000). It's unicodeString in JobSchema.columnType(), used by both auto-create and $resultAlterSql(), and bound as cf_sql_nvarchar.

Evidence

  • Matrix 37909382316 (all engines × sqlite / postgres / cockroachdb):
    • 15/15 legs green, 0 fail / 0 error, 597 bundles;
    • JobSchemaSpec 14/14 on PostgreSQL/CockroachDB and 13 + 1 skip on SQLite (the skip is the pre-4.2 conversion spec, which doesn't apply to SQLite by design);
    • JobHeartbeatGraceSpec 8/8 and JobLifecycleHooksSpec 16/16 everywhere;
    • RustCFML 7261/0/0.
  • PR CI: 27 pass, 0 fail, 3 skipped.

@bpamiri
bpamiri merged commit 915a96c into develop Oct 9, 2026
51 checks passed
@bpamiri
bpamiri deleted the peter/jobs-pre-cut-polish branch October 9, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant