Skip to content

Support latest bull board #19

Description

@sharq88

Until it gets proper support I thought to share some work here in case others wants to also use latest bull board with groupMQ.

Since It was giving all sorts of errors, I've run through it with Claude so that AT LEAST it works in a read-only manner.. :/ I wanted to display both bullmq and groupmq queues in the same admin UI. (using groupMQ for group based jobs and bullmq when we need priority queues and other features incompatible)

So I'm sharing it here to save some tokens for those who are in the same boat:

Summary

groupmq's BullBoardGroupMQAdapter was built against @bull-board/api@^6.13.0 (its own devDependency), but this project has @bull-board/api@9.3.2 installed. Between v6 and v9, BaseAdapter gained new abstract members that groupmq never implemented: getGlobalConcurrency,
setGlobalConcurrency, getJobSchedulers, getJobSchedulersCount, removeJobScheduler, updateJobScheduler, getMetrics, obliterate.

Two consequences, one of them severe:

  1. Runtime crash of the whole board — handlers/queues.js in @bull-board/api calls queue.getGlobalConcurrency() and queue.getJobSchedulersCount() unconditionally for every queue when building /api/queues, inside a single Promise.all with no per-queue error handling. Since those
    methods don't exist on the groupmq adapter, this throws and breaks the entire dashboard, including your working BullMQAdapter queue — not just the GroupMQ tab.
  2. It doesn't even type-check — I confirmed tsc fails on groupmq's own shipped .d.ts (TS2655: BullBoardGroupMQAdapter is missing 8 BaseAdapter members) as soon as @bull-board/api@9.x types are in node_modules, independent of any code you write.

Fix applied:

  • Added /groupMQAdapter.ts — a GroupMQAdapter subclass of BullBoardGroupMQAdapter implementing the 8 missing methods as honest stubs (groupmq has no queue-level concurrency setting or job-scheduler registry, so these
    return null/0/[]/false/'not-found' rather than faking support; getMetrics returns an empty series; obliterate throws a clear "not supported" error).
  • Updated ui.ts to use GroupMQAdapter instead of BullBoardGroupMQAdapter directly.
  • Verified with tsc --noEmit that this compiles cleanly with skipLibCheck: true (you'll need that flag in any tsconfig you use here, since groupmq's own declarations don't satisfy v9's BaseAdapter regardless of the fix above).

One minor cosmetic note (not fixed, low priority): groupmq passes its Redis namespace as the adapter's type field, where bull-board expects 'bull' | 'bullmq'. This only gates BullMQ Job-Flow features (correctly falls through to "unsupported" for GroupMQ), so it's harmless but
worth knowing if you see an unexpected type value in the API response.

import { BullBoardGroupMQAdapter } from 'groupmq';
import type {
    JobSchedulerRepeatOptions,
    JobSchedulerUpdateResult,
    MetricsType,
    ObliterateOptions,
    QueueDefaultJobOptions,
    QueueJob,
    QueueMetrics,
    QueueWorker,
    RedisStats,
} from '@bull-board/api/typings/app';

/**
 * groupmq's BullBoardGroupMQAdapter was built against @bull-board/api ^6.13.0.
 * Its bundler vendored that version's BaseAdapter directly into groupmq's dist
 * output instead of importing the one actually installed here (9.x), so this
 * adapter is missing everything BaseAdapter grew since v6 - both the abstract
 * members (getGlobalConcurrency/getJobSchedulersCount etc, patched below) and
 * the non-abstract ones that used to just come from the base class default
 * (getWorkers, getDatastoreStats, getArmedJobSchedulerId, getQueueDefaultJobOptions).
 * getWorkers() in particular is called unconditionally for every queue on every
 * board load, so without it the whole board 500s, not just the GroupMQ queue.
 * GroupMQ has no queue-level concurrency setting, job-scheduler registry, or
 * worker/connection introspection, so these are honest stubs rather than real
 * mappings.
 */
export class GroupMQAdapter extends BullBoardGroupMQAdapter {
    async getWorkers(): Promise<QueueWorker[] | null> {
        return null;
    }

    getJobDataSchema(): Record<string, any> | undefined {
        return undefined;
    }

    async getDatastoreStats(): Promise<RedisStats | null> {
        return null;
    }

    async getArmedJobSchedulerId(_job: QueueJob): Promise<string | null> {
        return null;
    }

    getQueueDefaultJobOptions(): QueueDefaultJobOptions {
        return {};
    }

    get supportsJobSchedulerUpdate(): boolean {
        return false;
    }

    async getMetrics(_type: MetricsType, _start?: number, _end?: number): Promise<QueueMetrics> {
        return { meta: { count: 0, prevTS: 0, prevCount: 0 }, data: [], count: 0 };
    }

    async obliterate(_opts?: ObliterateOptions): Promise<void> {
        if (this.readOnlyMode) {
            throw new Error('This adapter is in read-only mode. Mutations are disabled.');
        }
        throw new Error('GroupMQ does not support obliterating a queue.');
    }

    async getGlobalConcurrency(): Promise<number | null> {
        return null;
    }

    async setGlobalConcurrency(_concurrency: number): Promise<void> {
        throw new Error('GroupMQ does not support setting a global concurrency limit.');
    }

    async getJobSchedulers() {
        return [];
    }

    async getJobSchedulersCount(): Promise<number> {
        return 0;
    }

    async removeJobScheduler(_id: string): Promise<boolean> {
        return false;
    }

    async updateJobScheduler(
        _id: string,
        _repeat: JobSchedulerRepeatOptions
    ): Promise<JobSchedulerUpdateResult> {
        return 'not-found';
    }
}

with the above the latest bull board admin UI works again - using the example from the home page as a baseline:

// @ts-ignore
import express from 'express';
import { createBullBoard } from '@bull-board/api';
import { ExpressAdapter } from '@bull-board/express';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { Queue as GroupMQQueue } from 'groupmq';
import { Queue as BullMQQueue } from 'bullmq';
import Redis from "ioredis";
import { GroupMQAdapter } from './groupMQAdapter';

// GroupMQ queue configuration
const app = express();
const serverAdapter = new ExpressAdapter();
const redis = new Redis("redis://127.0.0.1:6379");
const groupMQQueue = new GroupMQQueue({ redis, namespace: 'group' });
const groupMQAdapter = new GroupMQAdapter(groupMQQueue, { displayName: 'group (groupMQ)' });

// BullMQ queue configuration
const redisOptions = { host: 'localhost', port: 6379, password: undefined, };
const bullMQQueue = new BullMQQueue ( 'bull', { connection: redisOptions } );
const bullMQAdapter = new BullMQAdapter ( bullMQQueue, { displayName: 'bull (bullMQ)' } );

createBullBoard({
    queues: [
        groupMQAdapter,
        bullMQAdapter
    ],
    serverAdapter,
});

serverAdapter.setBasePath('/admin/queues');
app.use('/admin/queues', serverAdapter.getRouter());
app.listen(4000);

console.log( 'UI started on http://localhost:4000/admin/queues' );

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions