|
@@ -22,6 +22,9 @@ const pg = vi.hoisted(() => {
|
|
|
/** Kept across tests: the bridge connects once for the life of a process. */
|
|
/** Kept across tests: the bridge connects once for the life of a process. */
|
|
|
const listening: string[] = []
|
|
const listening: string[] = []
|
|
|
let failNotify = false
|
|
let failNotify = false
|
|
|
|
|
+ /** How many queries the connection has been handed and not yet answered. */
|
|
|
|
|
+ let inFlight = 0
|
|
|
|
|
+ let mostInFlight = 0
|
|
|
class Client {
|
|
class Client {
|
|
|
on(event: string, handler: (arg: unknown) => void) {
|
|
on(event: string, handler: (arg: unknown) => void) {
|
|
|
if (event === 'notification') {
|
|
if (event === 'notification') {
|
|
@@ -33,6 +36,18 @@ const pg = vi.hoisted(() => {
|
|
|
}
|
|
}
|
|
|
async query(text: string, values?: unknown[]) {
|
|
async query(text: string, values?: unknown[]) {
|
|
|
queries.push(text)
|
|
queries.push(text)
|
|
|
|
|
+ inFlight++
|
|
|
|
|
+ mostInFlight = Math.max(mostInFlight, inFlight)
|
|
|
|
|
+ try {
|
|
|
|
|
+ // A round trip takes time, and it is during that time that a second
|
|
|
|
|
+ // query can arrive on the same connection.
|
|
|
|
|
+ await new Promise((resolve) => setImmediate(resolve))
|
|
|
|
|
+ return this.answer(text, values)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ inFlight--
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ answer(text: string, values?: unknown[]) {
|
|
|
if (text.startsWith('LISTEN ')) listening.push(text)
|
|
if (text.startsWith('LISTEN ')) listening.push(text)
|
|
|
if (text.startsWith('SELECT pg_notify')) {
|
|
if (text.startsWith('SELECT pg_notify')) {
|
|
|
if (failNotify) throw new Error('connection lost')
|
|
if (failNotify) throw new Error('connection lost')
|
|
@@ -57,6 +72,8 @@ const pg = vi.hoisted(() => {
|
|
|
setFailNotify(value: boolean) {
|
|
setFailNotify(value: boolean) {
|
|
|
failNotify = value
|
|
failNotify = value
|
|
|
},
|
|
},
|
|
|
|
|
+ /** The most queries the connection ever held at one moment. */
|
|
|
|
|
+ mostInFlight: () => mostInFlight,
|
|
|
/**
|
|
/**
|
|
|
* Between tests, not between processes: the connection and its LISTEN
|
|
* Between tests, not between processes: the connection and its LISTEN
|
|
|
* belong to the instance and are made once, exactly as in production.
|
|
* belong to the instance and are made once, exactly as in production.
|
|
@@ -65,6 +82,7 @@ const pg = vi.hoisted(() => {
|
|
|
notifications.length = 0
|
|
notifications.length = 0
|
|
|
queries.length = 0
|
|
queries.length = 0
|
|
|
failNotify = false
|
|
failNotify = false
|
|
|
|
|
+ mostInFlight = 0
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
@@ -96,7 +114,7 @@ afterEach(() => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('publishing', () => {
|
|
describe('publishing', () => {
|
|
|
- it('delivers here at once, without waiting for the database', () => {
|
|
|
|
|
|
|
+ it('delivers here at once, without waiting for the database', async () => {
|
|
|
const heard: unknown[] = []
|
|
const heard: unknown[] = []
|
|
|
const stop = onBusMessage((message) => heard.push(message))
|
|
const stop = onBusMessage((message) => heard.push(message))
|
|
|
|
|
|
|
@@ -106,6 +124,8 @@ describe('publishing', () => {
|
|
|
// round trip to see a change made in this process.
|
|
// round trip to see a change made in this process.
|
|
|
expect(heard).toEqual([{ t: 'record', change }])
|
|
expect(heard).toEqual([{ t: 'record', change }])
|
|
|
stop()
|
|
stop()
|
|
|
|
|
+ // Let the relay finish, so its notification is not counted by the next test.
|
|
|
|
|
+ await settle()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('sends it on to the other instances, stamped with who sent it', async () => {
|
|
it('sends it on to the other instances, stamped with who sent it', async () => {
|
|
@@ -126,6 +146,40 @@ describe('publishing', () => {
|
|
|
expect(pg.listening).toContain('LISTEN torqvoice_realtime')
|
|
expect(pg.listening).toContain('LISTEN torqvoice_realtime')
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ it('sends one at a time and in order, however many are published at once', async () => {
|
|
|
|
|
+ // A save announces several things in the same breath. The connection
|
|
|
|
|
+ // takes one query at a time: pg queues the rest for now, warns that it
|
|
|
|
|
+ // will stop, and from version 9 refuses them, which would lose the event
|
|
|
|
|
+ // for every other instance.
|
|
|
|
|
+ for (let id = 1; id <= 5; id++) {
|
|
|
|
|
+ publishToBus({ t: 'record', change: { ...change, id: `job-${id}` } })
|
|
|
|
|
+ }
|
|
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 50))
|
|
|
|
|
+
|
|
|
|
|
+ expect(pg.notifications.map((n) => JSON.parse(n.payload).change.id)).toEqual([
|
|
|
|
|
+ 'job-1',
|
|
|
|
|
+ 'job-2',
|
|
|
|
|
+ 'job-3',
|
|
|
|
|
+ 'job-4',
|
|
|
|
|
+ 'job-5',
|
|
|
|
|
+ ])
|
|
|
|
|
+ expect(pg.mostInFlight()).toBe(1)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('keeps sending after one that could not be sent', async () => {
|
|
|
|
|
+ const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
|
|
|
+ pg.setFailNotify(true)
|
|
|
|
|
+ publishToBus({ t: 'record', change })
|
|
|
|
|
+ await settle()
|
|
|
|
|
+ pg.setFailNotify(false)
|
|
|
|
|
+
|
|
|
|
|
+ publishToBus({ t: 'record', change: { ...change, id: 'job-2' } })
|
|
|
|
|
+ await settle()
|
|
|
|
|
+
|
|
|
|
|
+ expect(errors).toHaveBeenCalled()
|
|
|
|
|
+ expect(pg.notifications.map((n) => JSON.parse(n.payload).change.id)).toEqual(['job-2'])
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
it('does not fail the write when the relay cannot send', async () => {
|
|
it('does not fail the write when the relay cannot send', async () => {
|
|
|
pg.setFailNotify(true)
|
|
pg.setFailNotify(true)
|
|
|
const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|