Skip to content

Commit 10a3282

Browse files
committed
major jobs refactor
1 parent b80b030 commit 10a3282

35 files changed

Lines changed: 2992 additions & 7644 deletions

File tree

DEVELOPMENT_JOBS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ This ensures all subsequent `pgpm` and `psql` commands point at the same local d
5656

5757
Make sure `pgpm` is installed and up to date.
5858

59-
From the `constructive/` directory (with `pgenv` applied):
59+
From the `constructive-db/` directory (with `pgenv` applied):
6060

6161
1. Bootstrap admin users:
6262

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ COPY . .
2323

2424
# Install and build all workspaces
2525
RUN set -eux; \
26-
pnpm install --frozen-lockfile; \
26+
CI=true pnpm install --frozen-lockfile; \
2727
pnpm run build
2828

2929
################################################################################

docker-compose.jobs.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ services:
22
# LaunchQL API server (GraphQL)
33
launchql-server:
44
container_name: launchql-server
5-
image: ghcr.io/constructive-io/launchql:b88e3d1
5+
image: constructive-launchql:dev
6+
build:
7+
context: .
8+
dockerfile: ./Dockerfile
69
# The image entrypoint already runs the LaunchQL CLI (`lql`).
710
# We only need to provide the subcommand and flags here.
811
entrypoint: ["lql", "server", "--port", "3000", "--origin", "*", "--strictAuth", "false"]
@@ -34,7 +37,7 @@ services:
3437
# Simple email function (Knative-style HTTP function)
3538
simple-email:
3639
container_name: simple-email
37-
image: ghcr.io/constructive-io/launchql:b88e3d1
40+
image: constructive-launchql:dev
3841
# Override the image entrypoint (LaunchQL CLI) and run the Node function directly.
3942
entrypoint: ["node", "functions/simple-email/dist/index.js"]
4043
environment:
@@ -43,8 +46,10 @@ services:
4346
# Mailgun / email provider configuration for @launchql/postmaster
4447
# Replace with real credentials for local testing.
4548
MAILGUN_API_KEY: "change-me-mailgun-api-key"
49+
MAILGUN_KEY: "change-me-mailgun-api-key"
4650
MAILGUN_DOMAIN: "mg.constructive.io"
4751
MAILGUN_FROM: "no-reply@mg.constructive.io"
52+
MAILGUN_REPLY: "info@mg.constructive.io"
4853
ports:
4954
# Expose function locally (optional)
5055
- "8081:8080"
@@ -54,7 +59,7 @@ services:
5459
# Jobs runtime: callback server + worker + scheduler
5560
knative-job-service:
5661
container_name: knative-job-service
57-
image: ghcr.io/constructive-io/launchql:b88e3d1
62+
image: constructive-launchql:dev
5863
# Override the image entrypoint and run the jobs runtime directly.
5964
entrypoint: ["node", "jobs/knative-job-service/dist/run.js"]
6065
depends_on:
@@ -77,12 +82,11 @@ services:
7782

7883
# Callback HTTP server (job completion callbacks)
7984
INTERNAL_JOBS_CALLBACK_PORT: "8080"
80-
INTERNAL_JOBS_CALLBACK_URL: "http://knative-job-service:8080"
85+
# Hostname used by job-utils.getCallbackBaseUrl for callbacks
86+
JOBS_CALLBACK_HOST: "knative-job-service"
8187

8288
# Function gateway base URL (used by worker when no dev map is present)
83-
# Not used in dev when INTERNAL_GATEWAY_DEVELOPMENT_MAP is set, but required for validation.
8489
KNATIVE_SERVICE_URL: "dev.internal"
85-
INTERNAL_GATEWAY_URL: "http://simple-email:8080"
8690

8791
# Development-only map from task identifier -> function URL
8892
# Used by @launchql/knative-job-worker when NODE_ENV !== 'production'.

jobs/job-pg/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"url": "https://github.com/launchql/jobs/issues"
2929
},
3030
"dependencies": {
31-
"envalid": "6.0.2",
31+
"@launchql/job-utils": "workspace:^",
3232
"pg": "8.16.3"
3333
}
3434
}

jobs/job-pg/src/env.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import { cleanEnv, str, port } from 'envalid';
2-
3-
export default cleanEnv(
4-
process.env,
5-
{
6-
PGUSER: str({ default: 'postgres' }),
7-
PGHOST: str({ default: 'localhost' }),
8-
PGPASSWORD: str({ default: 'password' }),
9-
PGPORT: port({ default: 5432 }),
10-
PGDATABASE: str({ default: 'jobs' })
11-
},
12-
{ dotEnvPath: null }
13-
);
1+
// Legacy env module (no longer used in code).
2+
// PG configuration is now resolved via @launchql/job-utils.getJobPgConfig().
3+
export default {
4+
PGUSER: process.env.PGUSER || 'postgres',
5+
PGHOST: process.env.PGHOST || 'localhost',
6+
PGPASSWORD: process.env.PGPASSWORD || 'password',
7+
PGPORT: Number(process.env.PGPORT) || 5432,
8+
PGDATABASE: process.env.PGDATABASE || 'jobs'
9+
};

jobs/job-pg/src/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable no-console */
22

33
import { Pool, PoolConfig } from 'pg';
4-
import env from './env';
4+
import { getJobPgConfig } from '@launchql/job-utils';
55

66
// k8s only does SIGINT
77
// other events are bad for babel-watch
@@ -31,12 +31,7 @@ function once<T extends (...args: unknown[]) => unknown>(
3131
};
3232
}
3333

34-
const getDbString = (): string =>
35-
`postgres://${env.PGUSER}:${env.PGPASSWORD}@${env.PGHOST}:${env.PGPORT}/${env.PGDATABASE}`;
36-
37-
const pgPoolConfig: PoolConfig = {
38-
connectionString: getDbString()
39-
};
34+
const pgPoolConfig: PoolConfig = getJobPgConfig() as PoolConfig;
4035

4136
const end = (pool: Pool): void => {
4237
try {

jobs/job-scheduler/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"dependencies": {
3131
"@launchql/job-pg": "workspace:^",
3232
"@launchql/job-utils": "workspace:^",
33-
"envalid": "6.0.2",
33+
"@pgpmjs/logger": "workspace:^",
3434
"node-schedule": "1.3.2"
3535
}
3636
}

jobs/job-scheduler/src/env.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
1-
import { cleanEnv, str, bool, port, makeValidator } from 'envalid';
2-
3-
const array = makeValidator<string[]>((x) => x.split(',').filter((i) => i), '');
4-
5-
export default cleanEnv(
6-
process.env,
7-
{
8-
PGUSER: str({ default: 'postgres' }),
9-
PGHOST: str({ default: 'localhost' }),
10-
PGPASSWORD: str({ default: 'password' }),
11-
PGPORT: port({ default: 5432 }),
12-
PGDATABASE: str({ default: 'jobs' }),
13-
JOBS_SCHEMA: str({ default: 'app_jobs' }),
14-
JOBS_SUPPORT_ANY: bool({ default: true }),
15-
JOBS_SUPPORTED: array({ default: '' as unknown as string[] }),
16-
HOSTNAME: str({
17-
default: 'scheduler-0'
18-
})
19-
},
20-
{ dotEnvPath: null }
21-
);
1+
// Legacy env module kept for compatibility.
2+
// Scheduler configuration now flows through @launchql/job-utils runtime helpers.
3+
export default {
4+
PGUSER: process.env.PGUSER || 'postgres',
5+
PGHOST: process.env.PGHOST || 'localhost',
6+
PGPASSWORD: process.env.PGPASSWORD || 'password',
7+
PGPORT: Number(process.env.PGPORT) || 5432,
8+
PGDATABASE: process.env.PGDATABASE || 'jobs',
9+
JOBS_SCHEMA: process.env.JOBS_SCHEMA || 'app_jobs',
10+
JOBS_SUPPORT_ANY: process.env.JOBS_SUPPORT_ANY !== 'false',
11+
JOBS_SUPPORTED: (process.env.JOBS_SUPPORTED || '')
12+
.split(',')
13+
.map(s => s.trim())
14+
.filter(Boolean),
15+
HOSTNAME: process.env.HOSTNAME || 'scheduler-0'
16+
};

jobs/job-scheduler/src/index.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import env from './env';
21
import * as jobs from '@launchql/job-utils';
32
import type { PgClientLike } from '@launchql/job-utils';
43
import schedule from 'node-schedule';
54
import poolManager from '@launchql/job-pg';
65
import type { Pool, PoolClient } from 'pg';
7-
8-
/* eslint-disable no-console */
6+
import { Logger } from '@pgpmjs/logger';
97

108
export interface ScheduledJobRow {
119
id: number | string;
@@ -17,6 +15,8 @@ interface SchedulerJobHandle {
1715
cancel(): void;
1816
}
1917

18+
const log = new Logger('jobs:scheduler');
19+
2020
export default class Scheduler {
2121
idleDelay: number;
2222
supportedTaskNames: string[];
@@ -77,10 +77,8 @@ export default class Scheduler {
7777
}: { err?: Error; fatalError: unknown; jobId: ScheduledJobRow['id'] }
7878
) {
7979
const when = err ? `after failure '${err.message}'` : 'after success';
80-
console.error(
81-
`Failed to release job '${jobId}' ${when}; committing seppuku`
82-
);
83-
console.error(fatalError);
80+
log.error(`Failed to release job '${jobId}' ${when}; committing seppuku`);
81+
log.error(String(fatalError));
8482
await poolManager.close();
8583
process.exit(1);
8684
}
@@ -92,9 +90,8 @@ export default class Scheduler {
9290
duration
9391
}: { err: Error; job: ScheduledJobRow; duration: string }
9492
) {
95-
console.error(
96-
`scheduler: Failed to initialize scheduler for ${job.id} (${job.task_identifier}) with error ${err.message} (${duration}ms)`,
97-
{ err, stack: err.stack }
93+
log.error(
94+
`Failed to initialize scheduler for ${job.id} (${job.task_identifier}) with error ${err.message} (${duration}ms)`
9895
);
9996
const j = this.jobs[job.id];
10097
if (j) j.cancel();
@@ -107,8 +104,8 @@ export default class Scheduler {
107104
client: PgClientLike,
108105
{ job, duration }: { job: ScheduledJobRow; duration: string }
109106
) {
110-
console.log(
111-
`scheduler: initialized ${job.id} (${job.task_identifier}) with success (${duration}ms)`
107+
log.info(
108+
`initialized ${job.id} (${job.task_identifier}) with success (${duration}ms)`
112109
);
113110
}
114111
async scheduleJob(client: PgClientLike, job: ScheduledJobRow) {
@@ -120,18 +117,18 @@ export default class Scheduler {
120117

121118
if (newjob) {
122119
if (newjob.id) {
123-
console.log(`spinning up job[${newjob.task_identifier}]`);
120+
log.info(`spinning up job[${newjob.task_identifier}]`);
124121
} else {
125122
// this means the scheduled_job has been deleted from db, so cancel it
126123
console.log(
127-
`scheduler: attempted job[${job.task_identifier}] but it's probably non existent, unscheduling...`
124+
`attempted job[${job.task_identifier}] but it's probably non existent, unscheduling...`
128125
);
129126
const scheduledJob = this.jobs[job.id];
130127
if (scheduledJob) scheduledJob.cancel();
131128
}
132129
} else {
133-
console.log(
134-
`scheduler: job already scheduled but not yet run or complete: [${job.task_identifier}]`
130+
log.info(
131+
`job already scheduled but not yet run or complete: [${job.task_identifier}]`
135132
);
136133
}
137134
});
@@ -149,7 +146,7 @@ export default class Scheduler {
149146
try {
150147
const job = await jobs.getScheduledJob<ScheduledJobRow>(client, {
151148
workerId: this.workerId,
152-
supportedTaskNames: env.JOBS_SUPPORT_ANY
149+
supportedTaskNames: jobs.getJobSupportAny()
153150
? null
154151
: this.supportedTaskNames
155152
});
@@ -189,33 +186,33 @@ export default class Scheduler {
189186
}
190187
}
191188
listen() {
192-
const listenForChanges = (
189+
const listenForChanges = (
193190
err: Error | null,
194191
client: PoolClient,
195192
release: () => void
196193
) => {
197194
if (err) {
198-
console.error('scheduler: Error connecting with notify listener', err);
195+
log.error('Error connecting with notify listener');
199196
// Try again in 5 seconds
200197
// should this really be done in the node process?
201198
setTimeout(this.listen, 5000);
202199
return;
203200
}
204201
client.on('notification', () => {
205-
console.log('scheduler: a NEW scheduled JOB!');
202+
log.info('a NEW scheduled JOB!');
206203
if (this.doNextTimer) {
207204
// Must be idle, do something!
208205
this.doNext(client);
209206
}
210207
});
211208
client.query('LISTEN "scheduled_jobs:insert"');
212209
client.on('error', (e: unknown) => {
213-
console.error('scheduler: Error with database notify listener', e);
210+
log.error('Error with database notify listener');
214211
release();
215212
this.listen();
216213
});
217-
console.log(
218-
`scheduler: ${this.workerId} connected and looking for scheduled jobs...`
214+
log.info(
215+
`${this.workerId} connected and looking for scheduled jobs...`
219216
);
220217
this.doNext(client);
221218
};

jobs/job-scheduler/src/run.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
#!/usr/bin/env node
22

33
import Scheduler from './index';
4-
import env from './env';
54
import poolManager from '@launchql/job-pg';
5+
import {
6+
getSchedulerHostname,
7+
getJobSupported
8+
} from '@launchql/job-utils';
69

710
const pgPool = poolManager.getPool();
811

912
const scheduler = new Scheduler({
1013
pgPool,
11-
workerId: env.HOSTNAME,
12-
tasks: env.JOBS_SUPPORTED
14+
workerId: getSchedulerHostname(),
15+
tasks: getJobSupported()
1316
});
1417

1518
scheduler.listen();

0 commit comments

Comments
 (0)