Skip to content

Commit 79ffc9b

Browse files
bpamiriclaude
andcommitted
Rename _wheels_jobs to wheels_jobs for Oracle compatibility and fix BoxLang claim
Oracle requires unquoted identifiers to start with a letter, making _wheels_jobs invalid. Renamed to wheels_jobs across all SQL, tests, and docs. Removed transaction {} from $claimJob — auto-commit is sufficient and avoids failures on BoxLang + PostgreSQL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 12aa8fe commit 79ffc9b

File tree

9 files changed

+71
-73
lines changed

9 files changed

+71
-73
lines changed

.ai/wheels/jobs/overview.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Background Jobs
22

33
## Description
4-
Wheels provides a database-backed job queue system for running tasks asynchronously. Jobs are persisted to a `_wheels_jobs` table, processed in priority order, and automatically retried with exponential backoff on failure.
4+
Wheels provides a database-backed job queue system for running tasks asynchronously. Jobs are persisted to a `wheels_jobs` table, processed in priority order, and automatically retried with exponential backoff on failure.
55

66
## Key Points
77
- Jobs extend `wheels.Job` and implement a `perform()` method
@@ -17,7 +17,7 @@ Wheels provides a database-backed job queue system for running tasks asynchronou
1717
```bash
1818
wheels dbmigrate latest
1919
```
20-
This creates the `_wheels_jobs` table. The migration is at `app/migrator/migrations/20260221000001_create_wheels_jobs_table.cfc`.
20+
This creates the `wheels_jobs` table. The migration is at `app/migrator/migrations/20260221000001_createwheels_jobs_table.cfc`.
2121

2222
### 2. Create a Job
2323
```cfm
@@ -177,7 +177,7 @@ count = job.purgeCompleted(days=30, queue="reports");
177177

178178
## Database Schema
179179

180-
The `_wheels_jobs` table has these columns:
180+
The `wheels_jobs` table has these columns:
181181

182182
| Column | Type | Description |
183183
|--------|------|-------------|

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ wheels jobs monitor # live dashboard
368368

369369
**Configurable backoff**: `this.baseDelay = 2` and `this.maxDelay = 3600` in job `config()`. Formula: `Min(baseDelay * 2^attempt, maxDelay)`.
370370

371-
Requires migration: `20260221000001_create_wheels_jobs_table.cfc`. Run with `wheels dbmigrate latest`.
371+
Requires migration: `20260221000001_createwheels_jobs_table.cfc`. Run with `wheels dbmigrate latest`.
372372

373373
## Server-Sent Events (SSE) Quick Reference
374374

design_docs/FRAMEWORK-REVIEW.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ The native MCP server (`vendor/wheels/public/mcp/McpServer.cfc`) is a genuine di
137137

138138
**Impact:** Without a persistent job runner, background jobs cannot reliably execute. Email sending, report generation, and API syncs all require this.
139139

140-
**Recommendation:** Implement a database-backed job queue (following Rails' Solid Queue approach). Store jobs in a `_wheels_jobs` table with status, retry count, scheduled_at, and error tracking. A poll-based worker running as a CLI daemon (`wheels jobs work`) would complete the picture.
140+
**Recommendation:** Implement a database-backed job queue (following Rails' Solid Queue approach). Store jobs in a `wheels_jobs` table with status, retry count, scheduled_at, and error tracking. A poll-based worker running as a CLI daemon (`wheels jobs work`) would complete the picture.
141141

142142
#### Gap 3: No Nested Resource Routes
143143

@@ -234,7 +234,7 @@ Implement Server-Sent Events (SSE) as a lightweight first step:
234234
### Priority 2: Complete the Job System (High Impact)
235235

236236
Turn the existing `wheels.Job` skeleton into a full system:
237-
1. Database-backed queue table (`_wheels_jobs`)
237+
1. Database-backed queue table (`wheels_jobs`)
238238
2. CLI worker: `wheels jobs work --queue=default,high`
239239
3. Retry with exponential backoff, max attempts, dead letter storage
240240
4. `wheels jobs status` command for monitoring

design_docs/MODERNIZE-WHEELS-RIM.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ wheels jobs purge --completed --older-than=7d
473473
```
474474

475475
**Implementation steps:**
476-
1. Ensure `_wheels_jobs` table auto-creates on first use (already designed in `Job.cfc`)
476+
1. Ensure `wheels_jobs` table auto-creates on first use (already designed in `Job.cfc`)
477477
2. Implement `wheels.JobWorker` class that polls the job table
478478
3. Create CLI commands: `work`, `status`, `retry`, `purge`
479479
4. Add exponential backoff retry: delay = `min(baseDelay * 2^attempt, maxDelay)`

docs/src/command-line-tools/commands/jobs/jobs-purge.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: Purge old completed or failed jobs from the queue table.
44

55
# wheels jobs purge
66

7-
Delete old jobs from the `_wheels_jobs` table to prevent table bloat. By default, purges completed jobs older than 7 days.
7+
Delete old jobs from the `wheels_jobs` table to prevent table bloat. By default, purges completed jobs older than 7 days.
88

99
## Usage
1010

docs/src/working-with-wheels/background-jobs.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ First, create the jobs table by running the included migration:
2020
wheels dbmigrate latest
2121
```
2222

23-
This creates the `_wheels_jobs` table. The migration file is at `app/migrator/migrations/20260221000001_create_wheels_jobs_table.cfc`.
23+
This creates the `wheels_jobs` table. The migration file is at `app/migrator/migrations/20260221000001_createwheels_jobs_table.cfc`.
2424

2525
### 2. Create a Job
2626

@@ -228,7 +228,7 @@ Each job goes through these statuses:
228228

229229
## Database Schema
230230

231-
The `_wheels_jobs` table contains:
231+
The `wheels_jobs` table contains:
232232

233233
| Column | Type | Description |
234234
|--------|------|-------------|
@@ -355,4 +355,4 @@ The formula is `baseDelay * 2^attempt`, capped at `maxDelay`:
355355
5. **Monitor the queue**: Use `wheels jobs monitor` or `queueStats()` to track queue health and alert on growing `failed` counts.
356356
6. **Purge old jobs**: Run `wheels jobs purge` or `purgeCompleted()` on a schedule to prevent table bloat.
357357
7. **Run multiple workers**: For higher throughput, run multiple `wheels jobs work` processes — optimistic locking prevents duplicate processing.
358-
8. **Handle missing tables gracefully**: If the `_wheels_jobs` table doesn't exist yet, `enqueue()` will log a warning and return `persisted=false` instead of throwing an error.
358+
8. **Handle missing tables gracefully**: If the `wheels_jobs` table doesn't exist yet, `enqueue()` will log a warning and return `persisted=false` instead of throwing an error.

vendor/wheels/Job.cfc

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Provides background job processing with database-backed persistence,
44
* retry logic with exponential backoff, and priority queue support.
55
*
6-
* The _wheels_jobs table is auto-created on first use — no migration needed.
6+
* The wheels_jobs table is auto-created on first use — no migration needed.
77
*
88
* Usage:
99
* // In app/jobs/SendWelcomeEmailJob.cfc
@@ -136,7 +136,7 @@ component {
136136

137137
try {
138138
queryExecute(
139-
"INSERT INTO _wheels_jobs (id, jobClass, queue, data, priority, status, attempts, maxRetries, runAt, createdAt, updatedAt)
139+
"INSERT INTO wheels_jobs (id, jobClass, queue, data, priority, status, attempts, maxRetries, runAt, createdAt, updatedAt)
140140
VALUES (:id, :jobClass, :queue, :data, :priority, 'pending', 0, :maxRetries, :runAt, :createdAt, :updatedAt)",
141141
{
142142
id = {value = local.id, cfsqltype = "cf_sql_varchar"},
@@ -156,7 +156,7 @@ component {
156156
if ($ensureJobTable()) {
157157
try {
158158
queryExecute(
159-
"INSERT INTO _wheels_jobs (id, jobClass, queue, data, priority, status, attempts, maxRetries, runAt, createdAt, updatedAt)
159+
"INSERT INTO wheels_jobs (id, jobClass, queue, data, priority, status, attempts, maxRetries, runAt, createdAt, updatedAt)
160160
VALUES (:id, :jobClass, :queue, :data, :priority, 'pending', 0, :maxRetries, :runAt, :createdAt, :updatedAt)",
161161
{
162162
id = {value = local.id, cfsqltype = "cf_sql_varchar"},
@@ -202,7 +202,7 @@ component {
202202
};
203203

204204
local.sql = "SELECT id, jobClass, queue, data, attempts, maxRetries
205-
FROM _wheels_jobs
205+
FROM wheels_jobs
206206
WHERE status = 'pending' AND runAt <= :runAt";
207207

208208
if (Len(arguments.queue)) {
@@ -242,7 +242,7 @@ component {
242242
// Mark as processing
243243
try {
244244
queryExecute(
245-
"UPDATE _wheels_jobs
245+
"UPDATE wheels_jobs
246246
SET status = 'processing', attempts = attempts + 1, updatedAt = :updatedAt
247247
WHERE id = :id",
248248
{
@@ -264,7 +264,7 @@ component {
264264

265265
// Mark as completed
266266
queryExecute(
267-
"UPDATE _wheels_jobs
267+
"UPDATE wheels_jobs
268268
SET status = 'completed', completedAt = :completedAt, updatedAt = :updatedAt
269269
WHERE id = :id",
270270
{
@@ -294,7 +294,7 @@ component {
294294
local.nextRunAt = DateAdd("s", local.backoffSeconds, $now());
295295

296296
queryExecute(
297-
"UPDATE _wheels_jobs
297+
"UPDATE wheels_jobs
298298
SET status = 'pending',
299299
lastError = :lastError,
300300
runAt = :runAt,
@@ -317,7 +317,7 @@ component {
317317
} else {
318318
// Max retries exceeded — mark as failed (dead letter)
319319
queryExecute(
320-
"UPDATE _wheels_jobs
320+
"UPDATE wheels_jobs
321321
SET status = 'failed',
322322
failedAt = :failedAt,
323323
lastError = :lastError,
@@ -353,7 +353,7 @@ component {
353353
local.stats = {pending = 0, processing = 0, completed = 0, failed = 0, total = 0};
354354

355355
try {
356-
local.sql = "SELECT status, COUNT(*) as cnt FROM _wheels_jobs";
356+
local.sql = "SELECT status, COUNT(*) as cnt FROM wheels_jobs";
357357
local.params = {};
358358

359359
if (Len(arguments.queue)) {
@@ -383,7 +383,7 @@ component {
383383
* @queue Optional queue name to filter by.
384384
*/
385385
public numeric function retryFailed(string queue = "") {
386-
local.sql = "UPDATE _wheels_jobs
386+
local.sql = "UPDATE wheels_jobs
387387
SET status = 'pending', attempts = 0, lastError = NULL, failedAt = NULL,
388388
runAt = :runAt, updatedAt = :updatedAt
389389
WHERE status = 'failed'";
@@ -413,7 +413,7 @@ component {
413413
*/
414414
public numeric function purgeCompleted(numeric days = 7, string queue = "") {
415415
local.cutoff = DateAdd("d", -arguments.days, $now());
416-
local.sql = "DELETE FROM _wheels_jobs WHERE status = 'completed' AND completedAt < :cutoff";
416+
local.sql = "DELETE FROM wheels_jobs WHERE status = 'completed' AND completedAt < :cutoff";
417417
local.params = {
418418
cutoff = {value = local.cutoff, cfsqltype = "cf_sql_timestamp"}
419419
};
@@ -433,14 +433,14 @@ component {
433433
}
434434

435435
/**
436-
* Auto-create the _wheels_jobs table if it doesn't exist.
436+
* Auto-create the wheels_jobs table if it doesn't exist.
437437
* Uses database-agnostic SQL compatible with MySQL, PostgreSQL, SQL Server, H2, and SQLite.
438438
* Returns true if the table was created or already exists, false if creation failed.
439439
*/
440440
private boolean function $ensureJobTable() {
441441
try {
442442
// Check if table already exists by querying it
443-
queryExecute("SELECT COUNT(*) AS cnt FROM _wheels_jobs WHERE 1=0", {}, {datasource = variables.$datasource});
443+
queryExecute("SELECT COUNT(*) AS cnt FROM wheels_jobs WHERE 1=0", {}, {datasource = variables.$datasource});
444444
return true;
445445
} catch (any e) {
446446
// Table doesn't exist — create it
@@ -472,7 +472,7 @@ component {
472472
}
473473

474474
queryExecute("
475-
CREATE TABLE _wheels_jobs (
475+
CREATE TABLE wheels_jobs (
476476
id #local.varcharType#(36) NOT NULL PRIMARY KEY,
477477
jobClass #local.varcharType#(255) NOT NULL,
478478
queue #local.varcharType#(100) DEFAULT 'default' NOT NULL,
@@ -492,17 +492,17 @@ component {
492492

493493
// Add indexes for efficient queue processing
494494
try {
495-
queryExecute("CREATE INDEX idx_wheels_jobs_processing ON _wheels_jobs (status, runAt, priority)", {}, {datasource = variables.$datasource});
496-
queryExecute("CREATE INDEX idx_wheels_jobs_queue ON _wheels_jobs (queue, status)", {}, {datasource = variables.$datasource});
497-
queryExecute("CREATE INDEX idx_wheels_jobs_cleanup ON _wheels_jobs (status, completedAt)", {}, {datasource = variables.$datasource});
495+
queryExecute("CREATE INDEX idx_wjobs_processing ON wheels_jobs (status, runAt, priority)", {}, {datasource = variables.$datasource});
496+
queryExecute("CREATE INDEX idx_wjobs_queue ON wheels_jobs (queue, status)", {}, {datasource = variables.$datasource});
497+
queryExecute("CREATE INDEX idx_wjobs_cleanup ON wheels_jobs (status, completedAt)", {}, {datasource = variables.$datasource});
498498
} catch (any indexError) {
499499
// Indexes are optional — don't fail if they can't be created
500500
}
501501

502-
writeLog(text = "Auto-created _wheels_jobs table", type = "information", file = "wheels_jobs");
502+
writeLog(text = "Auto-created wheels_jobs table", type = "information", file = "wheels_jobs");
503503
return true;
504504
} catch (any createError) {
505-
writeLog(text = "Failed to auto-create _wheels_jobs table: #createError.message#", type = "error", file = "wheels_jobs");
505+
writeLog(text = "Failed to auto-create wheels_jobs table: #createError.message#", type = "error", file = "wheels_jobs");
506506
return false;
507507
}
508508
}

0 commit comments

Comments
 (0)