SCRAPE-12: Implement in-process mutex (isScraperRunning flag) #17

Merged
stephen merged 1 commits from scraper/mutex into dev 2026-02-06 16:27:29 -07:00
2 changed files with 199 additions and 0 deletions

View 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
View 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
};