Skip to content

feat(m2): BullMQ Ride Queue & Worker App#14

Merged
codeRisshi25 merged 3 commits intomainfrom
feature/m2-bullmq-ride-queue
Feb 25, 2026
Merged

feat(m2): BullMQ Ride Queue & Worker App#14
codeRisshi25 merged 3 commits intomainfrom
feature/m2-bullmq-ride-queue

Conversation

@codeRisshi25
Copy link
Copy Markdown
Owner

Overview

Implements [M2] — BullMQ Ride Queue & Worker App (closes #7).

Creates the apps/ride-worker workspace as a separate BullMQ worker process and refactors ride creation to publish jobs to a queue instead of direct processing.


Changes

New: apps/ride-worker

  • ESM-only workspace with nodenext module resolution (same conventions as api-gateway)
  • src/index.ts — starts all three BullMQ worker processors, graceful SIGTERM/SIGINT shutdown
  • src/logger.ts — pino logger with pino-pretty in dev
  • src/config.ts — Redis config + queue name re-exports
  • src/utils/redis.ts — ioredis singleton with maxRetriesPerRequest: null (required by BullMQ)
  • src/utils/db.ts — Prisma client singleton

Workers

File Status
ride-request.worker.ts Processes new ride jobs, publishes to ride-matching queue
ride-matching.worker.ts Skeleton — fully implemented in M4 (nearest-first cascade)
ride-lifecycle.worker.ts Skeleton handling ACCEPT / VERIFY_OTP / START / COMPLETE / CANCEL actions

packages/common

  • New packages/common/queues/index.ts with QUEUE_NAMES constants (RIDE_REQUESTS, RIDE_MATCHING, RIDE_LIFECYCLE)
  • Exported from packages/common/index.ts

apps/api-gateway

  • Added bullmq dependency
  • ride.service.ts — after DB insert, publishes job to ride-requests queue with { tripId, riderId, pickupLng, pickupLat, dropoffLng, dropoffLat } and jobId: tripId (deduplication)
  • Uses inline connection options ({ host, port }) — avoids ioredis type mismatch with BullMQ ConnectionOptions

Docker / Infra

  • compose.yml — added ride-worker service
  • compose.override.yml — added ride-worker dev override with bind mount + pnpm dev
  • tsconfig.json (root) — added ride-worker project reference

Tests

  • Mocked bullmq Queue constructor in ride.service.test.ts
  • Added assertion verifying queue.add() is called with correct job payload
  • All 34 tests pass

Test Output

Test Files  4 passed (4)
Tests  34 passed (34)

- Create apps/ride-worker workspace with ESM TypeScript config
- Add src/index.ts entry point with graceful SIGTERM/SIGINT shutdown
- Add pino logger, config (REDIS_CONFIG, QUEUE_NAMES re-export), utils/redis, utils/db
- Implement ride-request.worker.ts: processes new ride jobs, publishes to ride-matching queue
- Implement ride-matching.worker.ts: skeleton processor (fully implemented in M4)
- Implement ride-lifecycle.worker.ts: skeleton handling ACCEPT/VERIFY_OTP/START/COMPLETE/CANCEL actions

- Create packages/common/queues/index.ts with QUEUE_NAMES constants
  (RIDE_REQUESTS, RIDE_MATCHING, RIDE_LIFECYCLE)
- Export queue constants from packages/common/index.ts

- Add bullmq dependency to api-gateway
- Refactor POST /rides/create: after DB insert, publish job to ride-requests queue
- Add connection options pattern for BullMQ (avoids ioredis type mismatch)

- Update compose.yml: add ride-worker service
- Update compose.override.yml: add ride-worker dev override with bind mount
- Update root tsconfig.json: add ride-worker project reference

- Update ride.service.test.ts: mock bullmq Queue constructor,
  assert queue.add() is called with correct job payload (tripId, riderId, coords)

All 34 tests pass.
Copilot AI review requested due to automatic review settings February 20, 2026 15:41
- Remove learn/ from .gitignore so learning docs are versioned
- Add learn/m1-redis-geo.md (retroactively tracked)
- Add learn/m2-bullmq-workers.md: covers BullMQ Queue/Worker concepts,
  connection options pattern, multi-queue architecture, delayed jobs,
  graceful shutdown, and mocking BullMQ in Vitest
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a BullMQ-based queue system for asynchronous ride processing by creating a dedicated worker application and refactoring the ride creation flow. The ride-worker app runs as a separate process, enabling horizontal scaling of job processing independently from the API gateway.

Changes:

  • Created new apps/ride-worker workspace with three BullMQ worker processors for ride-requests, ride-matching, and ride-lifecycle queues
  • Added shared queue constants to packages/common/queues/ for consistent queue naming across services
  • Refactored ride.service.ts to publish jobs to BullMQ instead of synchronous processing, with updated tests

Reviewed changes

Copilot reviewed 22 out of 25 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
tsconfig.json Added ride-worker project reference
pnpm-lock.yaml Added BullMQ and related dependencies for both api-gateway and ride-worker
packages/common/queues/index.ts New shared queue name constants (RIDE_REQUESTS, RIDE_MATCHING, RIDE_LIFECYCLE)
packages/common/package.json Added exports configuration for queues subpath
packages/common/index.ts Exported queue constants
compose.yml Added ride-worker service configuration
compose.override.yml Added ride-worker dev override with bind mount
apps/ride-worker/tsconfig.json TypeScript configuration matching api-gateway conventions
apps/ride-worker/package.json Package definition with ESM, BullMQ, and Prisma dependencies
apps/ride-worker/src/index.ts Worker entry point with graceful shutdown handlers
apps/ride-worker/src/logger.ts Pino logger matching api-gateway configuration
apps/ride-worker/src/config.ts Redis config and queue name exports
apps/ride-worker/src/utils/redis.ts ioredis singleton with maxRetriesPerRequest: null for BullMQ
apps/ride-worker/src/utils/db.ts Prisma client singleton matching api-gateway pattern
apps/ride-worker/src/workers/ride-request.worker.ts Processes new ride jobs and publishes to matching queue
apps/ride-worker/src/workers/ride-matching.worker.ts Skeleton processor for driver matching (M4)
apps/ride-worker/src/workers/ride-lifecycle.worker.ts Skeleton processor for ride state transitions (M4/M5)
apps/api-gateway/src/services/ride.service.ts Publishes ride jobs to BullMQ queue after DB insert
apps/api-gateway/src/services/tests/ride.service.test.ts Added BullMQ mock and queue assertions
apps/api-gateway/package.json Added BullMQ dependency
.github/copilot-instructions.md New Copilot configuration file for project conventions
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +5 to +8
const bullmqConnection = {
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
} as const;
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bullmqConnection object is duplicated here (lines 5-8) when it should use the REDIS_CONFIG constant from config.ts. Replace this with import { REDIS_CONFIG } from '../config.js' and use connection: REDIS_CONFIG at line 55.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +35
const { tripId, riderId, pickupLng, pickupLat } = job.data;

logger.info(
{ tripId, riderId, pickupLng, pickupLat },
'Processing ride request job',
);

// Publish a ride-matching job to initiate driver matching (fully implemented in M4)
await rideMatchingQueue.add(
'match-driver',
{ tripId, riderId, pickupLng, pickupLat },
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job data passed to the ride-matching queue is missing dropoffLng and dropoffLat fields (line 35). The ride-request job receives both pickup and dropoff coordinates (see RideRequestJobData interface lines 11-18), but only passes pickup coordinates to the matching worker. This will likely cause issues in M4 when the matching algorithm needs to calculate optimal driver-rider pairings based on both pickup and dropoff locations. Add dropoffLng: job.data.dropoffLng, dropoffLat: job.data.dropoffLat to the job data.

Suggested change
const { tripId, riderId, pickupLng, pickupLat } = job.data;
logger.info(
{ tripId, riderId, pickupLng, pickupLat },
'Processing ride request job',
);
// Publish a ride-matching job to initiate driver matching (fully implemented in M4)
await rideMatchingQueue.add(
'match-driver',
{ tripId, riderId, pickupLng, pickupLat },
const { tripId, riderId, pickupLng, pickupLat, dropoffLng, dropoffLat } = job.data;
logger.info(
{ tripId, riderId, pickupLng, pickupLat, dropoffLng, dropoffLat },
'Processing ride request job',
);
// Publish a ride-matching job to initiate driver matching (fully implemented in M4)
await rideMatchingQueue.add(
'match-driver',
{ tripId, riderId, pickupLng, pickupLat, dropoffLng, dropoffLat },

Copilot uses AI. Check for mistakes.
const rideRequestsQueue = new Queue(QUEUE_NAMES.RIDE_REQUESTS, {
connection: bullmqConnection,
});

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource leak: the rideRequestsQueue Queue instance is created at module level (lines 12-14) but is never closed during application shutdown. This can cause connection leaks when the api-gateway process terminates. Consider implementing a graceful shutdown handler that closes the queue, or use a lazy initialization pattern that allows proper cleanup.

Suggested change
let rideRequestsQueueShutdownHookRegistered = false;
const registerRideRequestsQueueShutdown = (queue: Queue): void => {
if (rideRequestsQueueShutdownHookRegistered) {
return;
}
rideRequestsQueueShutdownHookRegistered = true;
const shutdown = async (): Promise<void> => {
try {
await queue.close();
} catch (error) {
logger.error(error, 'Error closing rideRequestsQueue during shutdown');
}
};
process.once('beforeExit', () => {
void shutdown();
});
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
signals.forEach((signal) => {
process.once(signal, () => {
void shutdown();
});
});
};
registerRideRequestsQueueShutdown(rideRequestsQueue);

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +9
const bullmqConnection = {
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
} as const;
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bullmqConnection object is duplicated in this file (lines 6-9) when it should use the REDIS_CONFIG constant exported from config.ts. This violates the DRY principle and creates maintenance burden. Replace the local bullmqConnection definition with import { REDIS_CONFIG } from '../config.js' and use connection: REDIS_CONFIG instead. This same issue exists in ride-matching.worker.ts and ride-lifecycle.worker.ts.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +8
const bullmqConnection = {
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
} as const;
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bullmqConnection object is duplicated here (lines 5-8) when it should use the REDIS_CONFIG constant from config.ts. Replace this with import { REDIS_CONFIG } from '../config.js' and use connection: REDIS_CONFIG at line 33.

Copilot uses AI. Check for mistakes.
dcl # tail logs

# Build & typecheck
pnpm build # turbo build (common first, then api-gateway)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation is outdated: Line 62 states "turbo build (common first, then api-gateway)" but after this PR the build order should include ride-worker as well. Update to reflect the complete build order: "turbo build (common first, then api-gateway and ride-worker)".

Suggested change
pnpm build # turbo build (common first, then api-gateway)
pnpm build # turbo build (common first, then api-gateway and ride-worker)

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +18
const createRedisClient = (): Redis => {
const client = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
maxRetriesPerRequest: null, // required by BullMQ
retryStrategy(times: number) {
const delay = Math.min(times * 100, 3000);
logger.warn({ attempt: times, delayMs: delay }, 'Redis reconnecting');
return delay;
},
});
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent Redis configuration: The ride-worker Redis client does not use lazyConnect: true (unlike the api-gateway Redis client which does). While this won't cause functional issues, it means the Redis client connects immediately on module load rather than on first use. For consistency with api-gateway patterns and to defer connection establishment until actually needed, consider adding lazyConnect: true to the configuration.

Copilot uses AI. Check for mistakes.
```bash
# Setup & run (Docker-based, includes PostGIS + Redis)
source activate.sh # loads aliases: dcu, dcd, dcr, dcl, dc
dcu # docker-compose up -d (postgres + redis + api-gateway)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation is outdated: Line 58 mentions "docker-compose up -d (postgres + redis + api-gateway)" but after this PR the docker-compose also starts the ride-worker service. Update this to include ride-worker in the list of services.

Suggested change
dcu # docker-compose up -d (postgres + redis + api-gateway)
dcu # docker-compose up -d (postgres + redis + api-gateway + ride-worker)

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23
await Promise.all(workers.map((w) => w.close()));
await redis.quit();

logger.info('ride-worker shutdown complete');
process.exit(0);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling in shutdown: If worker.close() or redis.quit() fails (lines 19-20), the error will be uncaught and the process will exit with code 0 (success) anyway. Wrap the shutdown logic in a try-catch block and call process.exit(1) on failure to properly signal shutdown errors.

Suggested change
await Promise.all(workers.map((w) => w.close()));
await redis.quit();
logger.info('ride-worker shutdown complete');
process.exit(0);
try {
await Promise.all(workers.map((w) => w.close()));
await redis.quit();
logger.info('ride-worker shutdown complete');
process.exit(0);
} catch (err) {
logger.error({ err, signal }, 'Error during ride-worker shutdown');
process.exit(1);
}

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +9
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx watch src/index.ts",
"start": "node ./dist/index.js"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage: The ride-worker app has no test files or test scripts in package.json. The api-gateway follows a pattern of comprehensive test coverage for all services (auth.service.test.ts, driver.service.test.ts, ride.service.test.ts). The ride-worker should follow the same pattern with tests for worker processors. At minimum, add tests for: 1) successful job processing, 2) job failure scenarios, 3) queue connection errors, and 4) graceful shutdown behavior. While the workers are skeleton implementations for M4, the test infrastructure should be established now.

Copilot uses AI. Check for mistakes.
- Merge redundant bullmq imports in ride-request.worker.ts
- Remove duplicated bullmqConnection in all 3 workers; use REDIS_CONFIG from config.ts (DRY)
- Pass dropoffLng/dropoffLat to ride-matching queue (data integrity fix for M4)
- Add dropoffLng/dropoffLat fields to RideMatchingJobData interface
- Return rideMatchingQueue from createRideRequestWorker for proper cleanup
- Wrap shutdown logic in try-catch with process.exit(1) on failure (index.ts)
- Add graceful SIGINT/SIGTERM shutdown hook for rideRequestsQueue in ride.service.ts
- Update copilot-instructions.md: 3 workspaces, dcu services, build order

All 34 tests pass.
@codeRisshi25 codeRisshi25 merged commit c80c3d9 into main Feb 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[M2] BullMQ Ride Queue & Worker App

2 participants