Skip to content

Commit 21d1df6

Browse files
committed
chore: sort out sql files and sql tests
Squashed commit of the following: commit d25a50a Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 13:54:05 2025 +0100 test(queue): refactor test utility and uncomment archive test cases Renamed readAllMessages to peekAllMessages with visibility timeout adjustment Uncommented and activated previously commented archive test scenarios for single and batch message archiving Updated test steps to use new peek function and verify message archiving behavior commit a8b9bc0 Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 13:46:31 2025 +0100 test(queue): Refactor queue test suite and improve message handling Enhance Queue test suite with improved type definitions, utility functions, and test coverage. Key changes include: - Renamed TestMessage to TestPayload with explicit type - Added readAllMessages and clearDb utility functions - Simplified test steps and improved message reading approach - Commented out some archive-related test steps - Imported additional assert functions commit f455688 Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 11:27:59 2025 +0100 test(queue): update readWithPoll method parameters in tests Adjust test parameters for queue read operations to match expected method signature commit 4a87e70 Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 11:24:15 2025 +0100 refactor(worker): Add generic type support for WorkerLifecycle Introduce generic type parameter for WorkerLifecycle and Queue to improve type safety and flexibility. Update Worker and test files to support generic message payload type. - Add generic type MessagePayload to WorkerLifecycle class - Update Queue type to support generic message types - Modify constructor signatures to use generic types - Remove unnecessary queueName parameter in test cases commit f996c1b Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 10:52:09 2025 +0100 test(sql): improve test utility functions and add transaction rollback support Add withRollback utility for transactional testing and update SQL test helpers to consistently truncate workers table before tests commit bf71192 Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 10:42:46 2025 +0100 test(Queue): update test cases with simplified read message polling Modify queue read message tests by: - Simplifying readWithPoll method call - Commenting out message content verification - Removing unnecessary parameters in test steps commit f5bcc19 Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 10:40:53 2025 +0100 feat(queue): Add queue management methods and comprehensive tests Implement new queue operations including safeCreate, archive, and batch processing methods. Add extensive unit tests covering queue creation, message sending, reading, and archiving scenarios with different parameters. Adds methods: - safeCreate: Safely create queue if not exists - archive: Archive individual messages - archiveBatch: Archive multiple messages - Comprehensive test coverage for queue operations commit d7005ee Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 10:22:05 2025 +0100 refactor(worker): Simplify worker lifecycle and queue constructor Refactored WorkerLifecycle and Queue class constructors to improve dependency injection and reduce complexity. Updated constructor signatures and added a queueName getter to WorkerLifecycle. Changes include: - Simplified Queue constructor parameters - Modified WorkerLifecycle constructor to accept Queue directly - Added queueName getter in WorkerLifecycle - Updated Worker initialization to pass Queue to WorkerLifecycle commit 837829a Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 10:20:54 2025 +0100 chore(migrations): Update migration file naming and project configuration Rename SQL migration files to ensure correct ordering and update project.json to include additional migration files commit ecf4f95 Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 10:12:03 2025 +0100 refactor(database): Move worker views from migration to dedicated SQL file Extracted active and inactive worker views from migration script to a separate views.sql file for better organization and maintainability commit b636f7c Author: Wojtek Majewski <[email protected]> Date: Tue Jan 21 10:09:45 2025 +0100 refactor(migration): relocate spawn_worker SQL file to sql directory Move migration utility SQL file to a more appropriate project structure
1 parent 4b67995 commit 21d1df6

File tree

11 files changed

+287
-103
lines changed

11 files changed

+287
-103
lines changed

pkgs/edge-worker/migrations/000_schema.sql

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,3 @@ create table if not exists edge_worker.workers (
1616
stopped_at TIMESTAMPTZ,
1717
last_heartbeat_at TIMESTAMPTZ not null default now()
1818
);
19-
20-
-------------------------------------------------------------------------------
21-
-- Active Workers View --------------------------------------------------------
22-
-------------------------------------------------------------------------------
23-
create or replace view edge_worker.active_workers as
24-
select
25-
worker_id,
26-
queue_name,
27-
function_name,
28-
started_at,
29-
stopped_at,
30-
last_heartbeat_at
31-
from edge_worker.workers
32-
where
33-
stopped_at is null
34-
and last_heartbeat_at > now() - make_interval(secs => 6);
35-
36-
-------------------------------------------------------------------------------
37-
-- Inactive Workers View ------------------------------------------------------
38-
-------------------------------------------------------------------------------
39-
create or replace view edge_worker.inactive_workers as
40-
select
41-
worker_id,
42-
queue_name,
43-
function_name,
44-
started_at,
45-
stopped_at,
46-
last_heartbeat_at
47-
from edge_worker.workers
48-
where
49-
stopped_at is null
50-
and last_heartbeat_at < now() - make_interval(secs => 6);

pkgs/edge-worker/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"commands": [
6262
"rm supabase/migrations/*.sql",
6363
"cp migrations/*.sql supabase/migrations/",
64+
"cp sql/*_*.sql supabase/migrations/",
6465
"supabase db reset"
6566
],
6667
"parallel": false

pkgs/edge-worker/sql/995_views.sql

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- Active workers are workers that have not stopped
2+
-- and have not sent a heartbeat in the last 6 seconds
3+
create or replace view edge_worker.active_workers as
4+
select
5+
worker_id,
6+
queue_name,
7+
function_name,
8+
started_at,
9+
stopped_at,
10+
last_heartbeat_at
11+
from edge_worker.workers
12+
where
13+
stopped_at is null
14+
and last_heartbeat_at > now() - make_interval(secs => 6);
15+
16+
-- Inactive workers are workers that have stopped
17+
-- or have not sent a heartbeat in the last 6 seconds
18+
create or replace view edge_worker.inactive_workers as
19+
select
20+
worker_id,
21+
queue_name,
22+
function_name,
23+
started_at,
24+
stopped_at,
25+
last_heartbeat_at
26+
from edge_worker.workers
27+
where
28+
stopped_at is null
29+
and last_heartbeat_at < now() - make_interval(secs => 6);

pkgs/edge-worker/src/Queue.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@ import { type Json } from './types.ts';
33
import { MessageRecord } from './types.ts';
44

55
export class Queue<MessagePayload extends Json> {
6-
constructor(
7-
private readonly sql: postgres.Sql,
8-
private readonly queueName: string
9-
) {}
6+
constructor(private readonly sql: postgres.Sql, readonly queueName: string) {}
7+
8+
/**
9+
* Creates a queue if it doesn't exist.
10+
* If the queue already exists, this method does nothing.
11+
*/
12+
async safeCreate(): Promise<void> {
13+
await this.sql`
14+
select * from pgmq.create(${this.queueName});
15+
`;
16+
}
1017

1118
async archive(msgId: number): Promise<void> {
1219
await this.sql`

pkgs/edge-worker/src/Worker.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@ export class Worker<MessagePayload extends Json> {
6161
const queue = new Queue<MessagePayload>(this.sql, this.config.queueName);
6262
const queries = new Queries(this.sql);
6363

64-
this.lifecycle = new WorkerLifecycle(queries, this.logger, {
65-
queueName: this.config.queueName,
66-
});
64+
this.lifecycle = new WorkerLifecycle<MessagePayload>(
65+
queries,
66+
queue,
67+
this.logger
68+
);
6769

6870
this.executionController = new ExecutionController<MessagePayload>(
6971
queue,

pkgs/edge-worker/src/WorkerLifecycle.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import { Heartbeat } from './Heartbeat.ts';
22
import { Logger } from './Logger.ts';
33
import { Queries } from './Queries.ts';
4-
import { WorkerBootstrap, WorkerRow } from './types.ts';
4+
import { Queue } from './Queue.ts';
5+
import { Json, WorkerBootstrap, WorkerRow } from './types.ts';
56
import { States, WorkerState } from './WorkerState.ts';
67

78
export interface LifecycleConfig {
89
queueName: string;
910
}
1011

11-
export class WorkerLifecycle {
12+
export class WorkerLifecycle<MessagePayload extends Json> {
1213
private workerState: WorkerState = new WorkerState();
1314
private heartbeat?: Heartbeat;
1415
private logger: Logger;
1516
private queries: Queries;
16-
private readonly queueName: string;
17+
private queue: Queue<MessagePayload>;
1718
private workerRow?: WorkerRow;
1819

19-
constructor(queries: Queries, logger: Logger, config: LifecycleConfig) {
20+
constructor(queries: Queries, queue: Queue<MessagePayload>, logger: Logger) {
2021
this.queries = queries;
2122
this.logger = logger;
22-
this.queueName = config.queueName;
23+
this.queue = queue;
2324
}
2425

2526
async acknowledgeStart(workerBootstrap: WorkerBootstrap): Promise<void> {
@@ -70,6 +71,10 @@ export class WorkerLifecycle {
7071
return this.workerRow?.function_name;
7172
}
7273

74+
get queueName() {
75+
return this.queue.queueName;
76+
}
77+
7378
async sendHeartbeat() {
7479
await this.heartbeat?.send();
7580
}

pkgs/edge-worker/tests/sql.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,36 @@ const DB_URL = 'postgresql://postgres:[email protected]:50322/postgres';
55
export function createSql() {
66
return postgres(DB_URL, {
77
prepare: true,
8-
onnotice(_) {
8+
onnotice(_: unknown) {
99
// no-op to silence notices
1010
},
1111
});
1212
}
1313

14+
export async function withRollback<T>(
15+
callback: (sql: postgres.Sql) => Promise<T>
16+
): Promise<T> {
17+
const sql = createSql();
18+
try {
19+
const result = (await sql.begin(
20+
'read write',
21+
async (sqlTx: postgres.Sql) => {
22+
const callbackResult = await callback(sqlTx);
23+
await sqlTx`ROLLBACK`;
24+
return callbackResult;
25+
}
26+
)) as T;
27+
return result;
28+
} finally {
29+
await sql.end();
30+
}
31+
}
32+
1433
export async function withSql<T>(
1534
callback: (sql: postgres.Sql) => Promise<T>
1635
): Promise<T> {
1736
const sql = createSql();
1837
try {
19-
await sql`TRUNCATE edge_worker.workers CASCADE`;
2038
return await callback(sql);
2139
} finally {
2240
await sql.end();

pkgs/edge-worker/tests/unit/Queries.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
22
import { Queries } from '../../src/Queries.ts';
3-
import { withSql } from '../sql.ts';
3+
import { withSql, withRollback } from '../sql.ts';
44
import { WorkerRow } from '../../src/types.ts';
55

66
const FAKE_UUID = '123e4567-e89b-12d3-a456-426614174000';
77

88
Deno.test('Queries.onWorkerStarted integration test', async () => {
99
await withSql(async (sql) => {
10+
await sql`TRUNCATE edge_worker.workers CASCADE`;
1011
const queries = new Queries(sql);
1112
// Test data
1213
const queueName = 'test_queue';
@@ -32,6 +33,7 @@ Deno.test('Queries.onWorkerStarted integration test', async () => {
3233

3334
Deno.test('Queries.onWorkerStarted throws on duplicate worker', async () => {
3435
await withSql(async (sql) => {
36+
await sql`TRUNCATE edge_worker.workers CASCADE`;
3537
const queries = new Queries(sql);
3638

3739
const params = {
@@ -59,6 +61,7 @@ Deno.test(
5961
'Queries.sendHeartbeat updates last_heartbeat_at for started worker',
6062
() =>
6163
withSql(async (sql) => {
64+
await sql`TRUNCATE edge_worker.workers CASCADE`;
6265
const queries = new Queries(sql);
6366

6467
// First create a worker
@@ -95,6 +98,7 @@ Deno.test(
9598

9699
Deno.test('Queries operations fail gracefully for non-existent worker', (t) =>
97100
withSql(async (sql) => {
101+
await sql`TRUNCATE edge_worker.workers CASCADE`;
98102
return; // TODO: decide if we really want to throw for non-existent worker
99103

100104
const queries = new Queries(sql);
@@ -130,6 +134,7 @@ Deno.test(
130134
'Queries.onWorkerStopped updates stopped_at and last_heartbeat_at',
131135
() =>
132136
withSql(async (sql) => {
137+
await sql`TRUNCATE edge_worker.workers CASCADE`;
133138
const queries = new Queries(sql);
134139

135140
// First create a worker
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { assertEquals, assertExists } from '@std/assert';
2+
import { Queue } from '../../src/Queue.ts';
3+
import { type postgres, withSql } from '../sql.ts';
4+
import { MessageRecord } from '../../src/types.ts';
5+
6+
// Test message type
7+
type TestPayload = {
8+
id: string;
9+
data: string;
10+
};
11+
12+
async function peekAllMessages(sql: postgres.Sql, queueName: string) {
13+
return await sql<MessageRecord<TestPayload>[]>`
14+
SELECT * FROM pgmq.read(
15+
queue_name => ${queueName},
16+
vt => 0,
17+
qty => 9999
18+
)
19+
`;
20+
}
21+
22+
async function clearDb(sql: postgres.Sql, queueName: string) {
23+
await sql`SELECT * FROM pgmq.purge_queue(${queueName})`;
24+
await sql`DELETE FROM edge_worker.workers`;
25+
}
26+
27+
Deno.test(
28+
'Queue#safeCreate creates queue and handles duplicate creation',
29+
async () => {
30+
await withSql(async (sql) => {
31+
const queue = new Queue<TestPayload>(sql, 'test_queue_safe_create');
32+
33+
// First creation should succeed
34+
await queue.safeCreate();
35+
36+
// Second creation should not throw
37+
await queue.safeCreate();
38+
39+
// Verify queue exists using pgmq.metrics()
40+
const metrics = await sql`
41+
SELECT * FROM pgmq.metrics('test_queue_safe_create')
42+
`;
43+
44+
assertEquals(metrics.length, 1);
45+
assertEquals(metrics[0].queue_name, 'test_queue_safe_create');
46+
});
47+
}
48+
);
49+
50+
Deno.test('Queue operations integration test', async (t) => {
51+
await withSql(async (sql) => {
52+
await clearDb(sql, 'test_queue');
53+
const queue = new Queue<TestPayload>(sql, 'test_queue');
54+
await queue.safeCreate();
55+
const testMessage: TestPayload = {
56+
id: '123',
57+
data: 'test data',
58+
};
59+
60+
await queue.send(testMessage);
61+
const messages = await peekAllMessages(sql, 'test_queue');
62+
63+
assertEquals(messages.length, 1);
64+
assertExists(messages[0].msg_id);
65+
66+
await t.step('set visibility timeout', async () => {
67+
const message = messages[0];
68+
const updatedMessage = await queue.setVt(message.msg_id, 10);
69+
assertExists(updatedMessage);
70+
assertEquals(updatedMessage.message, message.message);
71+
});
72+
73+
await t.step('archive single message', async () => {
74+
await queue.send(testMessage);
75+
const [message] = await peekAllMessages(sql, 'test_queue');
76+
await queue.archive(message.msg_id);
77+
78+
// Verify message is no longer available
79+
const newMessages = await peekAllMessages(sql, 'test_queue');
80+
assertEquals(newMessages.length, 0);
81+
});
82+
});
83+
});
84+
85+
Deno.test('Queue batch operations', async (t) => {
86+
await withSql(async (sql) => {
87+
await clearDb(sql, 'test_queue_batch');
88+
const queue = new Queue<TestPayload>(sql, 'test_queue_batch');
89+
await queue.safeCreate();
90+
const testMessages: TestPayload[] = [
91+
{ id: '1', data: 'test 1' },
92+
{ id: '2', data: 'test 2' },
93+
{ id: '3', data: 'test 3' },
94+
];
95+
96+
await t.step('send multiple messages', async () => {
97+
for (const msg of testMessages) {
98+
await queue.send(msg);
99+
}
100+
});
101+
102+
await t.step('read multiple messages', async () => {
103+
const messages = await peekAllMessages(sql, 'test_queue_batch');
104+
assertEquals(messages.length, 3);
105+
});
106+
107+
await t.step('archive batch', async () => {
108+
const messages = await peekAllMessages(sql, 'test_queue');
109+
const msgIds = messages.map((m) => m.msg_id);
110+
await queue.archiveBatch(msgIds);
111+
112+
// Verify messages are no longer available
113+
const newMessages = await peekAllMessages(sql, 'test_queue');
114+
assertEquals(newMessages.length, 0);
115+
});
116+
});
117+
});
118+
119+
// Deno.test('Queue readWithPoll with different parameters', async () => {
120+
// await withSql(async (sql) => {
121+
// const queue = new Queue<TestPayload>(sql, 'test_queue_params');
122+
// await queue.safeCreate();
123+
// const testMessage: TestPayload = {
124+
// id: '123',
125+
// data: 'test data',
126+
// };
127+
//
128+
// // Send a message
129+
// await queue.send(testMessage);
130+
//
131+
// // Test different read parameters
132+
// const messages = await queue.readWithPoll(
133+
// 5, // batch size
134+
// 30, // visibility timeout
135+
// 2, // max poll seconds
136+
// 500 // poll interval ms
137+
// );
138+
//
139+
// assertEquals(messages.length, 1);
140+
// assertEquals(messages[0].message, testMessage);
141+
// });
142+
// });

0 commit comments

Comments
 (0)