|
1 | 1 | import { mkdtemp, rm } from 'node:fs/promises' |
2 | 2 | import { tmpdir } from 'node:os' |
3 | 3 | import { join } from 'node:path' |
| 4 | +import { createServer, type AddressInfo } from 'node:net' |
4 | 5 | import Knex from 'knex' |
5 | 6 | import type { Knex as KnexTypes } from 'knex' |
6 | 7 | import { test } from '@japa/runner' |
@@ -215,6 +216,25 @@ test.group('Adapter | Redis', (group) => { |
215 | 216 | assert.closeTo(Date.now() - start, 200, 100) |
216 | 217 | }) |
217 | 218 |
|
| 219 | + test('aborts a blocking connection stuck in its handshake', async ({ assert, cleanup }) => { |
| 220 | + // Accepts connections and never answers. |
| 221 | + const blackhole = createServer(() => {}) |
| 222 | + await new Promise<void>((resolve) => blackhole.listen(0, '127.0.0.1', resolve)) |
| 223 | + cleanup(() => void blackhole.close()) |
| 224 | + const port = (blackhole.address() as AddressInfo).port |
| 225 | + const direct = new Redis(connection.options) |
| 226 | + direct.duplicate = (override?: Partial<RedisOptions>) => |
| 227 | + new Redis({ ...connection.options, host: '127.0.0.1', port, ...override }) |
| 228 | + cleanup(() => direct.disconnect()) |
| 229 | + |
| 230 | + const controller = new AbortController() |
| 231 | + const adapter = new RedisAdapter(direct, false, new MemoryLogger()) |
| 232 | + const watching = adapter.watch(['default'], controller.signal) |
| 233 | + setTimeout(() => controller.abort(), 100) |
| 234 | + |
| 235 | + await assert.rejects(() => watching) |
| 236 | + }).timeout(2000) |
| 237 | + |
218 | 238 | test('reconnects a blocking connection that stops answering', async ({ assert, cleanup }) => { |
219 | 239 | shortenHeartbeat(cleanup) |
220 | 240 | const target = { |
@@ -2691,6 +2711,60 @@ test.group('Adapter | Knex (PostgreSQL)', (group) => { |
2691 | 2711 | assert.isTrue(created[0].ended) |
2692 | 2712 | }) |
2693 | 2713 |
|
| 2714 | + test('rejects a LISTEN probe whose connection stalls', async ({ assert, cleanup }) => { |
| 2715 | + // The LISTEN connection works, the probe never completes its handshake. |
| 2716 | + fakeListenClients(cleanup, (index) => |
| 2717 | + index === 1 ? 'real' : { connect: () => new Promise(() => {}) } |
| 2718 | + ) |
| 2719 | + const watching = new KnexAdapter({ |
| 2720 | + connection, |
| 2721 | + tableName, |
| 2722 | + schedulesTableName, |
| 2723 | + notifications: true, |
| 2724 | + }) |
| 2725 | + |
| 2726 | + await assert.rejects(() => watching.watch!(['default']), /do not enable notifications/) |
| 2727 | + }).timeout(5000) |
| 2728 | + |
| 2729 | + test('ignores a notification that is not a wake message', async ({ assert, cleanup }) => { |
| 2730 | + const options = { connection, tableName, schedulesTableName, notifications: true } |
| 2731 | + const watcher = await new KnexAdapter(options).watch!(['default']) |
| 2732 | + cleanup(() => watcher.close()) |
| 2733 | + watcher.reset() |
| 2734 | + const waiting = watcher.wait(2000, new AbortController().signal, 0) |
| 2735 | + |
| 2736 | + await connection.raw(`select pg_notify(?, 'null')`, [tableName]) |
| 2737 | + const job = { id: 'after-null', name: 'TestJob', payload: {}, attempts: 0 } |
| 2738 | + await new KnexAdapter(options).pushOn('default', job) |
| 2739 | + assert.isTrue(await waiting) |
| 2740 | + }) |
| 2741 | + |
| 2742 | + test('watches tables found through the search path', async ({ assert, cleanup }) => { |
| 2743 | + const schema = 'queue_search_path' |
| 2744 | + await connection.raw('create schema if not exists ??', [schema]) |
| 2745 | + cleanup(() => connection.raw('drop schema ?? cascade', [schema])) |
| 2746 | + // Tables only the search path finds, unlike those of the other tests. |
| 2747 | + const tables = { tableName: 'tenant_jobs', schedulesTableName: 'tenant_schedules' } |
| 2748 | + for (const [table, like] of [ |
| 2749 | + [tables.tableName, tableName], |
| 2750 | + [tables.schedulesTableName, schedulesTableName], |
| 2751 | + ]) { |
| 2752 | + await connection.raw('create table ?? (like ?? including all)', [`${schema}.${table}`, like]) |
| 2753 | + } |
| 2754 | + const tenant = Knex({ ...connection.client.config, searchPath: [schema] }) |
| 2755 | + cleanup(() => tenant.destroy()) |
| 2756 | + const options = { connection: tenant, ...tables, notifications: true } |
| 2757 | + |
| 2758 | + const watcher = await new KnexAdapter(options).watch!(['default']) |
| 2759 | + cleanup(() => watcher.close()) |
| 2760 | + watcher.reset() |
| 2761 | + const waiting = watcher.wait(2000, new AbortController().signal, 0) |
| 2762 | + |
| 2763 | + const job = { id: 'tenant-job', name: 'TestJob', payload: {}, attempts: 0 } |
| 2764 | + await new KnexAdapter(options).pushOn('default', job) |
| 2765 | + assert.isTrue(await waiting) |
| 2766 | + }) |
| 2767 | + |
2694 | 2768 | test('does not wake again for a due schedule a concurrent claim holds', async ({ |
2695 | 2769 | assert, |
2696 | 2770 | cleanup, |
|
0 commit comments