Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/jobs-schema-missing-in-transaction.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- With `jobsAutoCreateTables = false` on PostgreSQL or CockroachDB, a job call made inside a database transaction while the job tables are missing now reports `Wheels.Job.SchemaMissing`, pointing at `wheels jobs install`. Before, the failed table probe aborted the transaction, and the caller saw "current transaction is aborted" instead.
6 changes: 6 additions & 0 deletions vendor/wheels/Job.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,12 @@ component {
* Public with $ prefix so JobWorker can bootstrap the table on a fresh database.
*/
public boolean function $ensureJobTable() {
// PostgreSQL and CockroachDB abort a transaction on a failed statement, so inside one the
// probe below would turn a missing table into "current transaction is aborted" for the
// catalog check after it. With auto-create off, ask the catalog first there.
if (!$jobSchema().autoCreateEnabled() && $jobSchema().databaseType() == "postgresql" && !$jobSchema().hasTable("wheels_jobs")) {
$throwJobSchemaMissing("The wheels_jobs table");
}
try {
// Check if table already exists by querying it
$jobsQuery("SELECT COUNT(*) AS cnt FROM wheels_jobs WHERE 1=0", {}, {datasource = variables.$datasource});
Expand Down
6 changes: 5 additions & 1 deletion vendor/wheels/tests/_assets/jobs/HookProbeJob.cfc
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* A job with every lifecycle hook (core-suite fixture). Each hook appends to a list in the
* application scope so the hook specs can check what ran, in what order, with what.
* data.mode picks what perform() does: "text" (default), "struct", "void", "long" or "fail".
* data.mode picks what perform() does: "text" (default), "struct", "void", "empty" (returns ""),
* "long", "unicode", "unicodeLong" or "fail".
*/
component extends="wheels.Job" {

Expand Down Expand Up @@ -38,6 +39,9 @@ component extends="wheels.Job" {
if (local.mode == "void") {
return;
}
if (local.mode == "empty") {
return "";
}
return "done";
}

Expand Down
8 changes: 8 additions & 0 deletions vendor/wheels/tests/specs/jobs/JobHeartbeatGraceSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ component extends="wheels.WheelsTest" {
expect($status(id)).toBe("processing", "15 seconds is inside the 30-second minimum");
});

it("reclaims a job idle past the 30-second minimum when the setting is below it", function() {
application.wheels.jobsHeartbeatGraceSeconds = 5;
var id = $insertRunning(queue = "test_grace_floor_past", heartbeatSecondsAgo = 45);
var reaped = new wheels.JobWorker().checkTimeouts(timeout = 300, queues = "test_grace_floor_past");
expect(reaped).toBe(1);
expect($status(id)).toBe("pending", "45 seconds is past the 30-second minimum, so the job is retried");
});

it("renews an exclusive job's lease by the heartbeat grace window", function() {
application.wheels.jobsHeartbeatGraceSeconds = 100;
var job = new wheels.Job();
Expand Down
8 changes: 8 additions & 0 deletions vendor/wheels/tests/specs/jobs/JobLifecycleHooksSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ component extends="wheels.WheelsTest" {
expect(ListLast(application["$hookSpecLog"], "|")).toBe("success:none");
});

it("stores NULL when perform() returns an empty string, as for nothing", function() {
var id = $insertJob(queue = "test_hooks_empty", data = {mode = "empty"});
new wheels.JobWorker().processNext(queues = "test_hooks_empty", timeout = 300);
var row = $row(id);
expect(row.status).toBe("completed");
expect(row.resultIsNull).toBeTrue("Oracle can't tell an empty string from NULL, so every database stores NULL");
});

it("cuts a long result short with a visible marker", function() {
var id = $insertJob(queue = "test_hooks_long", data = {mode = "long"});
new wheels.JobWorker().processNext(queues = "test_hooks_long", timeout = 300);
Expand Down
16 changes: 16 additions & 0 deletions vendor/wheels/tests/specs/jobs/JobSchemaSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ component extends="wheels.WheelsTest" {
expect(new wheels.JobSchema().hasTable("wheels_jobs")).toBeFalse();
});

it("with jobsAutoCreateTables = false, says the schema is missing from inside a transaction too", function() {
$dropJobTable();
application.wheels.jobsAutoCreateTables = false;
var ensured = {type = "", message = ""};
transaction {
try {
new wheels.Job().$ensureJobTable();
} catch (any e) {
ensured.type = e.type;
ensured.message = e.message;
}
transaction action="rollback";
}
expect(ensured.type).toBe("Wheels.Job.SchemaMissing", "not a database error from an aborted transaction: " & ensured.message);
});

it("with jobsAutoCreateTables = false, reports a probe failure as itself when the table exists", function() {
// The catalog says the table is there but the probe fails (here: a datasource that
// doesn't exist, as for a lost connection): that error surfaces, not a misleading
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ The `event` struct deliberately carries **no job `data`** (that can hold secrets

### The stored result

`perform()`'s return value is stored on the row, in a `wheels_jobs.result` column, and handed to `onSuccess(result)`. A simple value is stored as-is; a struct or array is stored as JSON. It's capped — 4000 characters on SQL Server (`NVARCHAR`), 4000 **UTF-8 bytes** on the other databases (a safe bound; Oracle's `VARCHAR2` counts bytes) — truncated on a character boundary with a visible marker if it's longer, so return a small summary, not a large payload. The `result` column is added by the same guarded auto-`ALTER` as the other 4.2 columns; without it, jobs run exactly as before and nothing is stored.
`perform()`'s return value is stored on the row, in a `wheels_jobs.result` column, and handed to `onSuccess(result)`. A simple value is stored as-is; a struct or array is stored as JSON. Returning nothing stores `NULL`, and so does returning an empty string (Oracle can't tell the two apart, so no database does). It's capped — 4000 characters on SQL Server (`NVARCHAR`), 4000 **UTF-8 bytes** on the other databases (a safe bound; Oracle's `VARCHAR2` counts bytes) — truncated on a character boundary with a visible marker if it's longer, so return a small summary, not a large payload. The `result` column is added by the same guarded auto-`ALTER` as the other 4.2 columns; without it, jobs run exactly as before and nothing is stored.

## Cap how many jobs a server runs at once

Expand Down
Loading