SCRAPE-12: Implement in-process mutex (isScraperRunning flag) (#17)
Co-authored-by: Stephen Minakian <stephenminakian@gmail.com> Co-committed-by: Stephen Minakian <stephenminakian@gmail.com>
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
48
jobs/scraperJob.js
Normal file
48
jobs/scraperJob.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// In-process mutex state
|
||||||
|
let isRunning = false;
|
||||||
|
let currentJobId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if scraper is currently running
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isScraperRunning() {
|
||||||
|
return isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current job ID if running
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function getCurrentJobId() {
|
||||||
|
return currentJobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire the scraper lock
|
||||||
|
* @param {string} jobId - Job ID to set
|
||||||
|
* @returns {boolean} True if lock acquired
|
||||||
|
*/
|
||||||
|
function acquireLock(jobId) {
|
||||||
|
if (isRunning) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isRunning = true;
|
||||||
|
currentJobId = jobId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release the scraper lock
|
||||||
|
*/
|
||||||
|
function releaseLock() {
|
||||||
|
isRunning = false;
|
||||||
|
currentJobId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isScraperRunning,
|
||||||
|
getCurrentJobId,
|
||||||
|
acquireLock,
|
||||||
|
releaseLock
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user