mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
36722c19a3
Grist has needed a job queue for some time. This adds one, using BullMQ. BullMQ however requires Redis, meaning we couldn't use jobs for the large subset of Grist that needs to be runnable without Redis (e.g. for use on desktop, or on simple self-hosted sites). So simple immediate, delayed, and repeated jobs are supported also in a crude single-process form when Redis is not available. This code isn't ready for actual use since an important issue remains to be worked out, specifically how to handle draining the queue during deployments to avoid mixing versions (or - if allowing mixed versions - thinking through any extra support needed for the developer to avoid introducing hard-to-test code paths).
151 lines
4.1 KiB
TypeScript
151 lines
4.1 KiB
TypeScript
import { delay } from 'app/common/delay';
|
|
import { GristBullMQJobs, GristJobs } from 'app/server/lib/GristJobs';
|
|
import { assert } from 'chai';
|
|
|
|
describe('GristJobs', function() {
|
|
this.timeout(20000);
|
|
|
|
// Clean up any jobs left over from previous round of tests,
|
|
// if external queues are in use (Redis).
|
|
beforeEach(async function() {
|
|
const jobs = new GristBullMQJobs();
|
|
const q = jobs.queue();
|
|
await q.stop({obliterate: true});
|
|
});
|
|
|
|
it('can run immediate jobs', async function() {
|
|
const jobs: GristJobs = new GristBullMQJobs();
|
|
const q = jobs.queue();
|
|
try {
|
|
let ct = 0;
|
|
let defaultCt = 0;
|
|
q.handleName('add', async (job) => {
|
|
ct += job.data.delta;
|
|
});
|
|
q.handleDefault(async (job) => {
|
|
defaultCt++;
|
|
});
|
|
await q.add('add', {delta: 2});
|
|
await waitToPass(async () => {
|
|
assert.equal(ct, 2);
|
|
assert.equal(defaultCt, 0);
|
|
});
|
|
await q.add('add', {delta: 3});
|
|
await waitToPass(async () => {
|
|
assert.equal(ct, 5);
|
|
assert.equal(defaultCt, 0);
|
|
});
|
|
await q.add('badd', {delta: 4});
|
|
await waitToPass(async () => {
|
|
assert.equal(ct, 5);
|
|
assert.equal(defaultCt, 1);
|
|
});
|
|
} finally {
|
|
await jobs.stop({obliterate: true});
|
|
}
|
|
});
|
|
|
|
it('can run delayed jobs', async function() {
|
|
const jobs: GristJobs = new GristBullMQJobs();
|
|
const q = jobs.queue();
|
|
try {
|
|
let ct = 0;
|
|
let defaultCt = 0;
|
|
q.handleName('add', async (job) => {
|
|
ct += job.data.delta;
|
|
});
|
|
q.handleDefault(async () => {
|
|
defaultCt++;
|
|
});
|
|
await q.add('add', {delta: 2}, {delay: 500});
|
|
assert.equal(ct, 0);
|
|
assert.equal(defaultCt, 0);
|
|
// We need to wait long enough to see the effect.
|
|
await delay(100);
|
|
assert.equal(ct, 0);
|
|
assert.equal(defaultCt, 0);
|
|
await delay(900);
|
|
assert.equal(ct, 2);
|
|
assert.equal(defaultCt, 0);
|
|
} finally {
|
|
await jobs.stop({obliterate: true});
|
|
}
|
|
});
|
|
|
|
it('can run repeated jobs', async function() {
|
|
const jobs: GristJobs = new GristBullMQJobs();
|
|
const q = jobs.queue();
|
|
try {
|
|
let ct = 0;
|
|
let defaultCt = 0;
|
|
q.handleName('add', async (job) => {
|
|
ct += job.data.delta;
|
|
});
|
|
q.handleDefault(async () => {
|
|
defaultCt++;
|
|
});
|
|
await q.add('add', {delta: 2}, {repeat: {every: 250}});
|
|
await q.add('badd', {delta: 2}, {repeat: {every: 100}});
|
|
assert.equal(ct, 0);
|
|
assert.equal(defaultCt, 0);
|
|
await delay(1000);
|
|
// allow for a lot of slop on CI
|
|
assert.isAtLeast(ct, 8 - 4);
|
|
assert.isAtMost(ct, 8 + 4);
|
|
assert.isAtLeast(defaultCt, 10 - 3);
|
|
assert.isAtMost(defaultCt, 10 + 3);
|
|
} finally {
|
|
await jobs.stop({obliterate: true});
|
|
}
|
|
});
|
|
|
|
it('can pick up jobs again', async function() {
|
|
// this test is only appropriate if we have an external queue.
|
|
if (!process.env.REDIS_URL) { this.skip(); }
|
|
const jobs1: GristJobs = new GristBullMQJobs();
|
|
const q = jobs1.queue();
|
|
try {
|
|
let ct = 0;
|
|
q.handleName('add', async (job) => {
|
|
ct += job.data.delta;
|
|
});
|
|
q.handleDefault(async () => {});
|
|
await q.add('add', {delta: 1}, {delay: 250});
|
|
await q.add('add', {delta: 1}, {delay: 1000});
|
|
await delay(500);
|
|
assert.equal(ct, 1);
|
|
await jobs1.stop();
|
|
const jobs2: GristJobs = new GristBullMQJobs();
|
|
const q2 = jobs2.queue();
|
|
try {
|
|
q2.handleName('add', async (job) => {
|
|
ct += job.data.delta * 2;
|
|
});
|
|
q2.handleDefault(async () => {});
|
|
await delay(1000);
|
|
assert.equal(ct, 3);
|
|
} finally {
|
|
await jobs2.stop({obliterate: true});
|
|
}
|
|
} finally {
|
|
await jobs1.stop({obliterate: true});
|
|
}
|
|
});
|
|
});
|
|
|
|
async function waitToPass(fn: () => Promise<void>,
|
|
maxWaitMs: number = 2000) {
|
|
const start = Date.now();
|
|
while (Date.now() - start < maxWaitMs) {
|
|
try {
|
|
await fn();
|
|
return true;
|
|
} catch (e) {
|
|
// continue after a small delay.
|
|
await delay(10);
|
|
}
|
|
}
|
|
await fn();
|
|
return true;
|
|
}
|