Service locking and process coordination utilities for managing concurrent Gatsby processes, preventing conflicts, and enabling inter-process communication during builds and development.
Creates lockfiles for coordinating between multiple Gatsby processes and preventing conflicts.
/**
* Creates a lockfile for service coordination between Gatsby processes
* @param programPath - Path to the Gatsby site directory
* @param serviceName - Name of the service to lock (e.g., "build", "develop")
* @param content - Data to store with the lock (process info, configuration, etc.)
* @returns Promise resolving to unlock function or null if lock failed
*/
function createServiceLock(
programPath: string,
serviceName: string,
content: Record<string, any>
): Promise<UnlockFn | null>;
/**
* Function type for unlocking services
*/
type UnlockFn = () => Promise<void>;Usage Examples:
import { createServiceLock } from "gatsby-core-utils";
// Lock build service to prevent concurrent builds
const unlockBuild = await createServiceLock(
process.cwd(),
"build",
{
pid: process.pid,
startedAt: Date.now(),
nodeVersion: process.version,
command: process.argv.join(" ")
}
);
if (unlockBuild) {
try {
console.log("Build lock acquired, starting build...");
await runBuild();
} finally {
// Always unlock when done
await unlockBuild();
console.log("Build lock released");
}
} else {
console.error("Could not acquire build lock - another build may be running");
process.exit(1);
}
// Lock development server
const unlockDevelop = await createServiceLock(
"/path/to/gatsby-site",
"develop",
{
pid: process.pid,
port: 8000,
host: "localhost",
startedAt: Date.now()
}
);Retrieves information about locked services and their current state.
/**
* Retrieves service data if the service is currently locked
* @param programPath - Path to the Gatsby site directory
* @param serviceName - Name of the service to query
* @param ignoreLockfile - Whether to ignore lock status and return data anyway
* @returns Promise resolving to service data or null if not locked/found
*/
function getService<T>(
programPath: string,
serviceName: string,
ignoreLockfile?: boolean
): Promise<T | null>;Usage Examples:
import { getService } from "gatsby-core-utils";
// Check if build is currently running
const buildService = await getService(process.cwd(), "build");
if (buildService) {
console.log(`Build running since: ${new Date(buildService.startedAt)}`);
console.log(`Build PID: ${buildService.pid}`);
console.log(`Node version: ${buildService.nodeVersion}`);
} else {
console.log("No build currently running");
}
// Get development server info
interface DevelopService {
pid: number;
port: number;
host: string;
startedAt: number;
}
const developService = await getService<DevelopService>(
"/path/to/site",
"develop"
);
if (developService) {
console.log(`Dev server running on http://${developService.host}:${developService.port}`);
console.log(`Started: ${new Date(developService.startedAt)}`);
}
// Force retrieve service data (ignore lock status)
const serviceData = await getService(process.cwd(), "build", true);Gets information about all services for a Gatsby site.
/**
* Gets all services for a Gatsby site
* @param programPath - Path to the Gatsby site directory
* @returns Promise resolving to object containing all services data
*/
function getServices(programPath: string): Promise<any>;Usage Examples:
import { getServices } from "gatsby-core-utils";
// Get overview of all running services
const services = await getServices(process.cwd());
console.log("Active services:", Object.keys(services));
// Display service status dashboard
for (const [serviceName, serviceData] of Object.entries(services)) {
if (serviceData) {
console.log(`${serviceName}: Running (PID ${serviceData.pid})`);
}
}
// Check for conflicts before starting new service
const allServices = await getServices("/path/to/site");
const hasConflicts = Object.values(allServices).some(service =>
service && service.port === desiredPort
);
if (hasConflicts) {
console.error(`Port ${desiredPort} already in use by another service`);
}Generic resource locking for coordinating access to shared resources.
/**
* Creates a lock for the specified resource
* @param resources - Resource identifier to lock
* @returns Promise resolving to unlock function
*/
function lock(resources: string): Promise<() => void>;Usage Examples:
import { lock } from "gatsby-core-utils";
// Lock shared cache directory
const unlockCache = await lock("cache-directory");
try {
// Perform cache operations that need exclusive access
await cleanupCacheFiles();
await rebuildCacheIndex();
} finally {
unlockCache();
}
// Lock database or file resources
const unlockDatabase = await lock("site-database");
try {
await performDatabaseMigration();
} finally {
unlockDatabase();
}
// Multiple processes coordinating file access
const unlockConfigFile = await lock("gatsby-config");
try {
const config = await readConfigFile();
const updatedConfig = await processConfig(config);
await writeConfigFile(updatedConfig);
} finally {
unlockConfigFile();
}import { createServiceLock, getService } from "gatsby-core-utils";
class GatsbyBuildManager {
private unlockFn: UnlockFn | null = null;
async startBuild() {
// Check if build already running
const existingBuild = await getService(this.sitePath, "build");
if (existingBuild) {
throw new Error(`Build already running (PID: ${existingBuild.pid})`);
}
// Acquire lock
this.unlockFn = await createServiceLock(this.sitePath, "build", {
pid: process.pid,
startedAt: Date.now(),
buildId: this.generateBuildId(),
environment: process.env.NODE_ENV
});
if (!this.unlockFn) {
throw new Error("Failed to acquire build lock");
}
// Register cleanup handlers
process.on('SIGINT', () => this.cleanup());
process.on('SIGTERM', () => this.cleanup());
process.on('uncaughtException', () => this.cleanup());
}
async cleanup() {
if (this.unlockFn) {
await this.unlockFn();
this.unlockFn = null;
}
}
}import { getServices, getService } from "gatsby-core-utils";
async function checkServiceHealth(sitePath: string) {
const services = await getServices(sitePath);
const healthReport = {};
for (const [serviceName, serviceData] of Object.entries(services)) {
if (serviceData && serviceData.pid) {
try {
// Check if process is still running
process.kill(serviceData.pid, 0);
healthReport[serviceName] = {
status: 'healthy',
pid: serviceData.pid,
uptime: Date.now() - serviceData.startedAt
};
} catch (error) {
// Process not found - stale lock
healthReport[serviceName] = {
status: 'stale',
pid: serviceData.pid,
error: 'Process not found'
};
}
}
}
return healthReport;
}
// Usage
const health = await checkServiceHealth(process.cwd());
console.log("Service health:", health);import { createServiceLock, getService } from "gatsby-core-utils";
// Producer process
async function startDataProcessor() {
const unlock = await createServiceLock(process.cwd(), "data-processor", {
pid: process.pid,
status: "initializing",
progress: 0,
lastUpdate: Date.now()
});
// Update progress periodically
setInterval(async () => {
await unlock(); // Release old lock
const unlock2 = await createServiceLock(process.cwd(), "data-processor", {
pid: process.pid,
status: "processing",
progress: getCurrentProgress(),
lastUpdate: Date.now()
});
unlock = unlock2;
}, 5000);
}
// Consumer process
async function monitorDataProcessing() {
const processorInfo = await getService(process.cwd(), "data-processor");
if (processorInfo) {
console.log(`Processing ${processorInfo.progress}% complete`);
console.log(`Status: ${processorInfo.status}`);
console.log(`Last update: ${new Date(processorInfo.lastUpdate)}`);
}
}import { createServiceLock } from "gatsby-core-utils";
async function safeLockAcquisition(programPath: string, serviceName: string) {
const maxRetries = 3;
const retryDelay = 1000;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const unlock = await createServiceLock(programPath, serviceName, {
pid: process.pid,
attempt: attempt,
timestamp: Date.now()
});
if (unlock) {
return unlock;
}
console.log(`Lock acquisition failed (attempt ${attempt}/${maxRetries})`);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
}
} catch (error) {
console.error(`Lock error on attempt ${attempt}:`, error.message);
}
}
throw new Error(`Failed to acquire lock after ${maxRetries} attempts`);
}import { getServices } from "gatsby-core-utils";
async function cleanupStaleLocks(programPath: string) {
const services = await getServices(programPath);
const staleServices = [];
for (const [serviceName, serviceData] of Object.entries(services)) {
if (serviceData && serviceData.pid) {
try {
// Test if process exists
process.kill(serviceData.pid, 0);
} catch (error) {
// Process doesn't exist - stale lock
staleServices.push(serviceName);
}
}
}
console.log(`Found ${staleServices.length} stale locks:`, staleServices);
// Manual cleanup would involve removing lock files
}Process coordination is critical for Gatsby's multi-process architecture:
These utilities ensure that Gatsby's complex build system operates reliably even with multiple concurrent processes and shared resources.