feat: add in-process mutex to prevent concurrent scraper runs
All checks were successful
CI/CD Pipeline - Apartment API / Scan Dependencies (pull_request) Successful in 13s
CI/CD Pipeline - Apartment API / Lint & Test (pull_request) Successful in 38s
CI/CD Pipeline - Apartment API / Send Webhook Notification (pull_request) Successful in 2s
CI/CD Pipeline - Apartment API / Build & Push Image (pull_request) Has been skipped
CI/CD Pipeline - Apartment API / Deploy to Production (pull_request) Has been skipped
All checks were successful
CI/CD Pipeline - Apartment API / Scan Dependencies (pull_request) Successful in 13s
CI/CD Pipeline - Apartment API / Lint & Test (pull_request) Successful in 38s
CI/CD Pipeline - Apartment API / Send Webhook Notification (pull_request) Successful in 2s
CI/CD Pipeline - Apartment API / Build & Push Image (pull_request) Has been skipped
CI/CD Pipeline - Apartment API / Deploy to Production (pull_request) Has been skipped
Implement a lightweight in-process mutex lock for the scraper job scheduler. This prevents multiple scraper instances from running simultaneously, which could cause duplicate data and race conditions. Exported functions: - acquireLock(jobId): Attempts to acquire the mutex, returns boolean - releaseLock(): Releases the mutex lock - isScraperRunning(): Returns current lock state - getCurrentJobId(): Returns the active job ID or null Includes 19 unit tests covering lock acquisition, release, re-entry prevention, and edge cases.
This commit is contained in:
151
__tests__/scraper/scraperJob.test.js
Normal file
151
__tests__/scraper/scraperJob.test.js
Normal file
@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Tests for scraperJob.js - In-process mutex (isScraperRunning flag)
|
||||
* Following TDD - these tests are written BEFORE implementation
|
||||
*/
|
||||
|
||||
const {
|
||||
isScraperRunning,
|
||||
getCurrentJobId,
|
||||
acquireLock,
|
||||
releaseLock
|
||||
} = require('../../jobs/scraperJob');
|
||||
|
||||
describe('scraperJob - mutex lock', () => {
|
||||
// Reset state between each test
|
||||
afterEach(() => {
|
||||
releaseLock();
|
||||
});
|
||||
|
||||
describe('isScraperRunning()', () => {
|
||||
test('should return false when not running', () => {
|
||||
expect(isScraperRunning()).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true after acquireLock()', () => {
|
||||
acquireLock('test-job-1');
|
||||
|
||||
expect(isScraperRunning()).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false after releaseLock()', () => {
|
||||
acquireLock('test-job-2');
|
||||
releaseLock();
|
||||
|
||||
expect(isScraperRunning()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentJobId()', () => {
|
||||
test('should return null when not running', () => {
|
||||
expect(getCurrentJobId()).toBeNull();
|
||||
});
|
||||
|
||||
test('should return jobId when running', () => {
|
||||
const jobId = 'my-unique-job-id';
|
||||
acquireLock(jobId);
|
||||
|
||||
expect(getCurrentJobId()).toBe(jobId);
|
||||
});
|
||||
|
||||
test('should return null after releaseLock()', () => {
|
||||
acquireLock('test-job-3');
|
||||
releaseLock();
|
||||
|
||||
expect(getCurrentJobId()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('acquireLock()', () => {
|
||||
test('should return true when lock is available', () => {
|
||||
const result = acquireLock('job-1');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when lock is already held', () => {
|
||||
acquireLock('job-1');
|
||||
|
||||
const result = acquireLock('job-2');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('should set isScraperRunning to true', () => {
|
||||
acquireLock('job-1');
|
||||
|
||||
expect(isScraperRunning()).toBe(true);
|
||||
});
|
||||
|
||||
test('should set currentJobId to the provided jobId', () => {
|
||||
const jobId = 'specific-job-id';
|
||||
acquireLock(jobId);
|
||||
|
||||
expect(getCurrentJobId()).toBe(jobId);
|
||||
});
|
||||
|
||||
test('should not overwrite existing lock when already held', () => {
|
||||
const firstJobId = 'first-job';
|
||||
const secondJobId = 'second-job';
|
||||
|
||||
acquireLock(firstJobId);
|
||||
acquireLock(secondJobId);
|
||||
|
||||
// Should still have the first job's ID
|
||||
expect(getCurrentJobId()).toBe(firstJobId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('releaseLock()', () => {
|
||||
test('should clear running state', () => {
|
||||
acquireLock('job-to-release');
|
||||
|
||||
releaseLock();
|
||||
|
||||
expect(isScraperRunning()).toBe(false);
|
||||
});
|
||||
|
||||
test('should clear currentJobId', () => {
|
||||
acquireLock('job-to-release');
|
||||
|
||||
releaseLock();
|
||||
|
||||
expect(getCurrentJobId()).toBeNull();
|
||||
});
|
||||
|
||||
test('should allow a new lock to be acquired after release', () => {
|
||||
acquireLock('first-job');
|
||||
releaseLock();
|
||||
|
||||
const result = acquireLock('second-job');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(getCurrentJobId()).toBe('second-job');
|
||||
expect(isScraperRunning()).toBe(true);
|
||||
});
|
||||
|
||||
test('should be safe to call when no lock is held', () => {
|
||||
// Should not throw when called without an active lock
|
||||
expect(() => releaseLock()).not.toThrow();
|
||||
expect(isScraperRunning()).toBe(false);
|
||||
expect(getCurrentJobId()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('module exports', () => {
|
||||
test('should export isScraperRunning as a function', () => {
|
||||
expect(typeof isScraperRunning).toBe('function');
|
||||
});
|
||||
|
||||
test('should export getCurrentJobId as a function', () => {
|
||||
expect(typeof getCurrentJobId).toBe('function');
|
||||
});
|
||||
|
||||
test('should export acquireLock as a function', () => {
|
||||
expect(typeof acquireLock).toBe('function');
|
||||
});
|
||||
|
||||
test('should export releaseLock as a function', () => {
|
||||
expect(typeof releaseLock).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user