@pgpm/database-jobs 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Makefile CHANGED
@@ -1,5 +1,5 @@
1
1
  EXTENSION = launchql-database-jobs
2
- DATA = sql/launchql-database-jobs--0.4.6.sql
2
+ DATA = sql/launchql-database-jobs--0.4.0.sql
3
3
 
4
4
  PG_CONFIG = pg_config
5
5
  PGXS := $(shell $(PG_CONFIG) --pgxs)
package/README.md CHANGED
@@ -2,4 +2,418 @@
2
2
 
3
3
  Database-specific job handling and queue management.
4
4
 
5
- Extends the core jobs system with database-specific functionality for managing background tasks and job processing workflows.
5
+ ## Overview
6
+
7
+ `@pgpm/database-jobs` provides a complete PostgreSQL-based background job processing system with persistent queues, scheduled jobs, and worker management. This package implements a robust job queue system entirely within PostgreSQL, enabling reliable background task processing with features like job locking, retries, priorities, and cron-style scheduling.
8
+
9
+ ## Features
10
+
11
+ - **Persistent Job Queue**: Store jobs in PostgreSQL with ACID guarantees
12
+ - **Job Scheduling**: Cron-style and rule-based job scheduling
13
+ - **Worker Management**: Multiple workers with job locking and expiry
14
+ - **Priority Queue**: Process jobs by priority and run time
15
+ - **Automatic Retries**: Configurable retry attempts with exponential backoff
16
+ - **Job Keys**: Upsert semantics for idempotent job creation
17
+ - **Queue Management**: Named queues with independent locking
18
+ - **Notifications**: PostgreSQL LISTEN/NOTIFY for real-time job processing
19
+
20
+ ## Installation
21
+
22
+ If you have `pgpm` installed:
23
+
24
+ ```bash
25
+ pgpm install @pgpm/database-jobs
26
+ pgpm deploy
27
+ ```
28
+
29
+ This is a quick way to get started. The sections below provide more detailed installation options.
30
+
31
+ ### Prerequisites
32
+
33
+ ```bash
34
+ # Install pgpm globally
35
+ npm install -g pgpm
36
+
37
+ # Start PostgreSQL
38
+ pgpm docker start
39
+
40
+ # Set environment variables
41
+ eval "$(pgpm env)"
42
+ ```
43
+
44
+ ### Deploy
45
+
46
+ #### Option 1: Deploy by installing with pgpm
47
+
48
+ ```bash
49
+ pgpm install @pgpm/database-jobs
50
+ pgpm deploy
51
+ ```
52
+
53
+ #### Option 2: Deploy from Package Directory
54
+
55
+ ```bash
56
+ cd packages/jobs/database-jobs
57
+ pgpm deploy --createdb
58
+ ```
59
+
60
+ #### Option 3: Deploy from Workspace Root
61
+
62
+ ```bash
63
+ # Install workspace dependencies
64
+ pgpm install
65
+
66
+ # Deploy with dependencies
67
+ pgpm deploy mydb1 --yes --createdb
68
+ ```
69
+
70
+ ## Core Concepts
71
+
72
+ ### Jobs Table
73
+
74
+ The `app_jobs.jobs` table stores active jobs with the following key fields:
75
+ - `id`: Unique job identifier
76
+ - `database_id`: Database/tenant identifier
77
+ - `task_identifier`: Job type/handler name
78
+ - `payload`: JSON data for the job
79
+ - `priority`: Lower numbers = higher priority (default: 0)
80
+ - `run_at`: When the job should run
81
+ - `attempts`: Current attempt count
82
+ - `max_attempts`: Maximum retry attempts (default: 25)
83
+ - `locked_by`: Worker ID that locked this job
84
+ - `locked_at`: When the job was locked
85
+ - `key`: Optional unique key for upsert semantics
86
+
87
+ ### Scheduled Jobs Table
88
+
89
+ The `app_jobs.scheduled_jobs` table stores recurring jobs with cron-style or rule-based scheduling.
90
+
91
+ ### Job Queues Table
92
+
93
+ The `app_jobs.job_queues` table tracks queue statistics and locking state.
94
+
95
+ ## Usage
96
+
97
+ ### Adding Jobs
98
+
99
+ ```sql
100
+ -- Add a simple job
101
+ SELECT app_jobs.add_job(
102
+ db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid,
103
+ identifier := 'send_email',
104
+ payload := '{"to": "user@example.com", "subject": "Hello"}'::json
105
+ );
106
+
107
+ -- Add a job with priority and delayed execution
108
+ SELECT app_jobs.add_job(
109
+ db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid,
110
+ identifier := 'generate_report',
111
+ payload := '{"report_id": 123}'::json,
112
+ run_at := now() + interval '1 hour',
113
+ priority := 10,
114
+ max_attempts := 5
115
+ );
116
+
117
+ -- Add a job with a unique key (upsert semantics)
118
+ SELECT app_jobs.add_job(
119
+ db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid,
120
+ identifier := 'daily_summary',
121
+ payload := '{"date": "2025-01-15"}'::json,
122
+ job_key := 'daily_summary_2025_01_15',
123
+ queue_name := 'reports'
124
+ );
125
+ ```
126
+
127
+ ### Getting Jobs (Worker Side)
128
+
129
+ ```sql
130
+ -- Worker fetches next available job
131
+ SELECT * FROM app_jobs.get_job(
132
+ worker_id := 'worker-1',
133
+ task_identifiers := ARRAY['send_email', 'generate_report'],
134
+ job_expiry := interval '4 hours'
135
+ );
136
+
137
+ -- Returns NULL if no jobs available
138
+ -- Returns job row if job was successfully locked
139
+ ```
140
+
141
+ ### Completing Jobs
142
+
143
+ ```sql
144
+ -- Mark job as complete
145
+ SELECT app_jobs.complete_job(
146
+ worker_id := 'worker-1',
147
+ job_id := 123
148
+ );
149
+ ```
150
+
151
+ ### Failing Jobs
152
+
153
+ ```sql
154
+ -- Mark job as failed (will retry if attempts < max_attempts)
155
+ SELECT app_jobs.fail_job(
156
+ worker_id := 'worker-1',
157
+ job_id := 123,
158
+ error_message := 'Connection timeout'
159
+ );
160
+ ```
161
+
162
+ ### Scheduled Jobs
163
+
164
+ ```sql
165
+ -- Schedule a job with cron-style timing
166
+ INSERT INTO app_jobs.scheduled_jobs (
167
+ database_id,
168
+ task_identifier,
169
+ payload,
170
+ schedule_info
171
+ ) VALUES (
172
+ '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid,
173
+ 'cleanup_old_data',
174
+ '{"days": 30}'::json,
175
+ '{
176
+ "hour": [2],
177
+ "minute": [0],
178
+ "dayOfWeek": [0, 1, 2, 3, 4, 5, 6]
179
+ }'::json
180
+ );
181
+
182
+ -- Schedule a job with a rule (every minute for 3 minutes)
183
+ SELECT app_jobs.add_scheduled_job(
184
+ db_id := '5b720132-17d5-424d-9bcb-ee7b17c13d43'::uuid,
185
+ identifier := 'heartbeat',
186
+ payload := '{}'::json,
187
+ schedule_info := json_build_object(
188
+ 'start', now() + interval '10 seconds',
189
+ 'end', now() + interval '3 minutes',
190
+ 'rule', '*/1 * * * *'
191
+ )
192
+ );
193
+
194
+ -- Run a scheduled job (creates a job in the jobs table)
195
+ SELECT * FROM app_jobs.run_scheduled_job(scheduled_job_id := 1);
196
+ ```
197
+
198
+ ## Functions Reference
199
+
200
+ ### app_jobs.add_job(...)
201
+
202
+ Adds a new job to the queue or updates an existing job if a key is provided.
203
+
204
+ **Parameters:**
205
+ - `db_id` (uuid): Database/tenant identifier
206
+ - `identifier` (text): Job type/handler name
207
+ - `payload` (json): Job data (default: `{}`)
208
+ - `job_key` (text): Optional unique key for upsert (default: NULL)
209
+ - `queue_name` (text): Optional queue name (default: random UUID)
210
+ - `run_at` (timestamptz): When to run (default: now())
211
+ - `max_attempts` (integer): Maximum retries (default: 25)
212
+ - `priority` (integer): Job priority (default: 0)
213
+
214
+ **Returns:** `app_jobs.jobs` row
215
+
216
+ **Behavior:**
217
+ - If `job_key` is provided and exists, updates the job (if not locked)
218
+ - If job is locked, removes the key and creates a new job
219
+ - Triggers notifications for workers
220
+
221
+ ### app_jobs.get_job(...)
222
+
223
+ Fetches and locks the next available job for a worker.
224
+
225
+ **Parameters:**
226
+ - `worker_id` (text): Unique worker identifier
227
+ - `task_identifiers` (text[]): Optional filter for job types (default: NULL = all)
228
+ - `job_expiry` (interval): How long before locked jobs expire (default: 4 hours)
229
+
230
+ **Returns:** `app_jobs.jobs` row or NULL
231
+
232
+ **Behavior:**
233
+ - Selects jobs by priority, run_at, and id
234
+ - Locks the job and its queue
235
+ - Increments attempt counter
236
+ - Uses `FOR UPDATE SKIP LOCKED` for concurrency
237
+
238
+ ### app_jobs.complete_job(...)
239
+
240
+ Marks a job as successfully completed and removes it from the queue.
241
+
242
+ **Parameters:**
243
+ - `worker_id` (text): Worker that processed the job
244
+ - `job_id` (bigint): Job identifier
245
+
246
+ **Returns:** `app_jobs.jobs` row
247
+
248
+ ### app_jobs.fail_job(...)
249
+
250
+ Marks a job as failed and schedules retry if attempts remain.
251
+
252
+ **Parameters:**
253
+ - `worker_id` (text): Worker that processed the job
254
+ - `job_id` (bigint): Job identifier
255
+ - `error_message` (text): Error description (default: NULL)
256
+
257
+ **Returns:** `app_jobs.jobs` row
258
+
259
+ **Behavior:**
260
+ - Records error message
261
+ - Unlocks the job for retry if attempts < max_attempts
262
+ - Permanently fails if max_attempts reached
263
+
264
+ ### app_jobs.add_scheduled_job(...)
265
+
266
+ Creates a scheduled job with cron-style or rule-based timing.
267
+
268
+ **Parameters:**
269
+ - `db_id` (uuid): Database/tenant identifier
270
+ - `identifier` (text): Job type/handler name
271
+ - `payload` (json): Job data
272
+ - `schedule_info` (json): Scheduling configuration
273
+ - `job_key` (text): Optional unique key
274
+ - `queue_name` (text): Optional queue name
275
+ - `max_attempts` (integer): Maximum retries
276
+ - `priority` (integer): Job priority
277
+
278
+ **Returns:** `app_jobs.scheduled_jobs` row
279
+
280
+ ### app_jobs.run_scheduled_job(...)
281
+
282
+ Executes a scheduled job by creating a job in the jobs table.
283
+
284
+ **Parameters:**
285
+ - `scheduled_job_id` (bigint): Scheduled job identifier
286
+
287
+ **Returns:** `app_jobs.jobs` row
288
+
289
+ ## Job Processing Pattern
290
+
291
+ ```sql
292
+ -- Worker loop (simplified)
293
+ LOOP
294
+ -- 1. Get next job
295
+ SELECT * FROM app_jobs.get_job('worker-1', ARRAY['my_task']);
296
+
297
+ -- 2. Process job
298
+ -- ... application logic ...
299
+
300
+ -- 3. Mark as complete or failed
301
+ IF success THEN
302
+ SELECT app_jobs.complete_job('worker-1', job_id);
303
+ ELSE
304
+ SELECT app_jobs.fail_job('worker-1', job_id, error_msg);
305
+ END IF;
306
+ END LOOP;
307
+ ```
308
+
309
+ ## Triggers and Automation
310
+
311
+ The package includes several triggers for automatic management:
312
+
313
+ - **timestamps**: Automatically sets created_at/updated_at
314
+ - **notify_worker**: Sends LISTEN/NOTIFY events when jobs are added
315
+ - **increase_job_queue_count**: Updates queue statistics on insert
316
+ - **decrease_job_queue_count**: Updates queue statistics on delete/update
317
+
318
+ ## Dependencies
319
+
320
+ - `@pgpm/default-roles`: Role-based access control definitions
321
+ - `@pgpm/verify`: Verification utilities for database objects
322
+
323
+ ## Testing
324
+
325
+ ```bash
326
+ pnpm test
327
+ ```
328
+
329
+ The test suite validates:
330
+ - Job creation and retrieval
331
+ - Scheduled job creation with cron and rule-based timing
332
+ - Job key upsert semantics
333
+ - Worker locking and concurrency
334
+
335
+ ## Development
336
+
337
+ See the [Development](#development) section below for information on working with this package.
338
+
339
+ ---
340
+
341
+ ## Development
342
+
343
+ ### **Before You Begin**
344
+
345
+ ```bash
346
+ # 1. Install pgpm
347
+ npm install -g pgpm
348
+
349
+ # 2. Start Postgres (Docker or local)
350
+ pgpm docker start
351
+
352
+ # 3. Load PG* environment variables (PGHOST, PGUSER, ...)
353
+ eval "$(pgpm env)"
354
+ ```
355
+
356
+ ---
357
+
358
+ ### **Starting a New Project**
359
+
360
+ ```bash
361
+ # 1. Create a workspace
362
+ pgpm init --workspace
363
+ cd my-app
364
+
365
+ # 2. Create your first module
366
+ pgpm init
367
+
368
+ # 3. Add a migration
369
+ pgpm add some_change
370
+
371
+ # 4. Deploy (auto-creates database)
372
+ pgpm deploy --createdb
373
+ ```
374
+
375
+ ---
376
+
377
+ ### **Working With an Existing Project**
378
+
379
+ ```bash
380
+ # 1. Clone and enter the project
381
+ git clone <repo> && cd <project>
382
+
383
+ # 2. Install dependencies
384
+ pnpm install
385
+
386
+ # 3. Deploy locally
387
+ pgpm deploy --createdb
388
+ ```
389
+
390
+ ---
391
+
392
+ ### **Testing a Module Inside a Workspace**
393
+
394
+ ```bash
395
+ # 1. Install workspace deps
396
+ pnpm install
397
+
398
+ # 2. Enter the module directory
399
+ cd packages/<some-module>
400
+
401
+ # 3. Run tests in watch mode
402
+ pnpm test:watch
403
+ ```
404
+
405
+ ## Related Tooling
406
+
407
+ * [pgpm](https://github.com/launchql/launchql/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
408
+ * [pgsql-test](https://github.com/launchql/launchql/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
409
+ * [supabase-test](https://github.com/launchql/launchql/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
410
+ * [graphile-test](https://github.com/launchql/launchql/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
411
+ * [pgsql-parser](https://github.com/launchql/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
412
+ * [libpg-query-node](https://github.com/launchql/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
413
+ * [pg-proto-parser](https://github.com/launchql/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
414
+
415
+ ## Disclaimer
416
+
417
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
418
+
419
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
@@ -1,6 +1,6 @@
1
1
  # launchql-database-jobs extension
2
2
  comment = 'launchql-database-jobs extension'
3
- default_version = '0.4.6'
3
+ default_version = '0.4.0'
4
4
  module_pathname = '$libdir/launchql-database-jobs'
5
5
  requires = 'plpgsql,uuid-ossp,pgcrypto,launchql-default-roles,launchql-verify'
6
6
  relocatable = false
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@pgpm/database-jobs",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Database-specific job handling and queue management",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "scripts": {
9
- "bundle": "lql package",
9
+ "bundle": "pgpm package",
10
10
  "test": "jest",
11
11
  "test:watch": "jest --watch"
12
12
  },
13
13
  "devDependencies": {
14
- "@launchql/cli": "^4.9.0"
14
+ "pgpm": "^0.2.0"
15
15
  },
16
16
  "dependencies": {
17
- "@pgpm/default-roles": "0.4.0",
18
- "@pgpm/verify": "0.4.0"
17
+ "@pgpm/default-roles": "0.5.0",
18
+ "@pgpm/verify": "0.5.0"
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
@@ -25,5 +25,5 @@
25
25
  "bugs": {
26
26
  "url": "https://github.com/launchql/extensions/issues"
27
27
  },
28
- "gitHead": "cc9f52a335caa6e21ee7751b04b77c84ce6cb809"
28
+ "gitHead": "d8eedbb24ad22a106634bc3b919bfb8d41976c16"
29
29
  }