Make Safelink event replay idempotent (#11524)
This commit is contained in:
Vendored
+1
@@ -137,6 +137,7 @@ export class SafelinkClient {
|
|||||||
action: rule.action,
|
action: rule.action,
|
||||||
createdAt: rule.createdAt,
|
createdAt: rule.createdAt,
|
||||||
})
|
})
|
||||||
|
.onConflict(oc => oc.column('id').doNothing())
|
||||||
.execute()
|
.execute()
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
redirectLogger.error(
|
redirectLogger.error(
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ const createClient = (getAgent: () => Promise<unknown>) => {
|
|||||||
const client: SafelinkClient = Object.create(SafelinkClient.prototype)
|
const client: SafelinkClient = Object.create(SafelinkClient.prototype)
|
||||||
Reflect.set(client, 'stopped', false)
|
Reflect.set(client, 'stopped', false)
|
||||||
Reflect.set(client, 'ozoneAgent', {getAgent})
|
Reflect.set(client, 'ozoneAgent', {getAgent})
|
||||||
|
Reflect.set(client, 'domainCache', {delete: () => {}})
|
||||||
|
Reflect.set(client, 'urlCache', {delete: () => {}})
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Safelink shutdown', () => {
|
void describe('Safelink shutdown', () => {
|
||||||
it('clears a scheduled retry and cannot restart after stop', async () => {
|
void it('clears a scheduled retry and cannot restart after stop', async () => {
|
||||||
const client = createClient(async () => {
|
const client = createClient(() =>
|
||||||
throw new Error('Ozone unavailable')
|
Promise.reject(new Error('Ozone unavailable')),
|
||||||
})
|
)
|
||||||
|
|
||||||
await client.runFetchEvents()
|
await client.runFetchEvents()
|
||||||
assert.ok(Reflect.get(client, 'fetchEventsTimeout'))
|
assert.ok(Reflect.get(client, 'fetchEventsTimeout'))
|
||||||
@@ -27,7 +29,7 @@ describe('Safelink shutdown', () => {
|
|||||||
assert.strictEqual(Reflect.get(client, 'fetchEventsTimeout'), undefined)
|
assert.strictEqual(Reflect.get(client, 'fetchEventsTimeout'), undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('waits for an active poll to finish before stopping', async () => {
|
void it('waits for an active poll to finish before stopping', async () => {
|
||||||
let pollStarted = () => {}
|
let pollStarted = () => {}
|
||||||
const started = new Promise<void>(resolve => {
|
const started = new Promise<void>(resolve => {
|
||||||
pollStarted = () => resolve(undefined)
|
pollStarted = () => resolve(undefined)
|
||||||
@@ -57,7 +59,7 @@ describe('Safelink shutdown', () => {
|
|||||||
assert.strictEqual(stopped, true)
|
assert.strictEqual(stopped, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it(
|
void it(
|
||||||
'bounds the wait for a poll that never finishes',
|
'bounds the wait for a poll that never finishes',
|
||||||
{timeout: 1_000},
|
{timeout: 1_000},
|
||||||
async () => {
|
async () => {
|
||||||
@@ -70,12 +72,14 @@ describe('Safelink shutdown', () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
it('retries a failed rule write without advancing the cursor', async () => {
|
void it('retries a failed rule write without advancing the cursor', async () => {
|
||||||
const client = createClient(async () => ({
|
const client = createClient(() =>
|
||||||
|
Promise.resolve({
|
||||||
tools: {
|
tools: {
|
||||||
ozone: {
|
ozone: {
|
||||||
safelink: {
|
safelink: {
|
||||||
queryEvents: async () => ({
|
queryEvents: () =>
|
||||||
|
Promise.resolve({
|
||||||
data: {
|
data: {
|
||||||
cursor: 'next',
|
cursor: 'next',
|
||||||
events: [
|
events: [
|
||||||
@@ -93,17 +97,19 @@ describe('Safelink shutdown', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
Reflect.set(client, 'cursor', 'current')
|
Reflect.set(client, 'cursor', 'current')
|
||||||
Reflect.set(client, 'db', {
|
Reflect.set(client, 'db', {
|
||||||
transaction: async (run: (db: unknown) => Promise<void>) =>
|
transaction: (run: (db: unknown) => Promise<void>) =>
|
||||||
run({
|
run({
|
||||||
db: {
|
db: {
|
||||||
insertInto: () => ({
|
insertInto: () => ({
|
||||||
values: () => ({
|
values: () => ({
|
||||||
execute: async () => {
|
onConflict: () => ({
|
||||||
throw new Error('database unavailable')
|
execute: () =>
|
||||||
},
|
Promise.reject(new Error('database unavailable')),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -115,4 +121,69 @@ describe('Safelink shutdown', () => {
|
|||||||
assert.strictEqual(Reflect.get(client, 'cursor'), 'current')
|
assert.strictEqual(Reflect.get(client, 'cursor'), 'current')
|
||||||
await client.stop(1_000)
|
await client.stop(1_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
void it('advances the cursor after replaying an existing rule event', async () => {
|
||||||
|
const client = createClient(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
tools: {
|
||||||
|
ozone: {
|
||||||
|
safelink: {
|
||||||
|
queryEvents: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: {
|
||||||
|
cursor: 'next',
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
action: 'block',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
eventType: 'addRule',
|
||||||
|
id: 1,
|
||||||
|
pattern: 'domain',
|
||||||
|
url: 'example.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
Reflect.set(client, 'cursor', 'current')
|
||||||
|
let storedCursor = 'current'
|
||||||
|
Reflect.set(client, 'db', {
|
||||||
|
transaction: (run: (db: unknown) => Promise<void>) =>
|
||||||
|
run({
|
||||||
|
db: {
|
||||||
|
insertInto: () => ({
|
||||||
|
values: () => ({
|
||||||
|
onConflict: () => ({
|
||||||
|
execute: () => Promise.resolve(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
db: {
|
||||||
|
insertInto: () => ({
|
||||||
|
values: ({cursor}: {cursor: string}) => ({
|
||||||
|
onConflict: () => ({
|
||||||
|
execute: () => {
|
||||||
|
storedCursor = cursor
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.runFetchEvents()
|
||||||
|
try {
|
||||||
|
assert.strictEqual(storedCursor, 'next')
|
||||||
|
assert.strictEqual(Reflect.get(client, 'cursor'), 'next')
|
||||||
|
} finally {
|
||||||
|
await client.stop(1_000)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user