Problem
The ILockingService interface in v0.8.0 lacks lifecycle management methods for lock storage initialization. This forces adapters to handle setup implicitly during first lock acquisition, which:
- Violates fail-fast principle - Setup errors surface during migration, not during handler initialization
- Creates unclear lifecycle - When is lock storage created? First use? Constructor?
- Leads to inconsistent implementations - Each adapter handles setup differently
- Makes confusing error messages - Is it a permission issue or a lock conflict?
- Reduces testability - Can't test initialization separately from locking logic
Evidence
The Firebase adapter already recognized this gap and implemented these methods as custom extensions beyond the interface:
// FirebaseLockingService.ts (lines 262-304)
async initLockStorage(): Promise<void> {
// Validates path access
}
async ensureLockStorageAccessible(): Promise<boolean> {
// Verifies connection and permissions
}
Both methods include this comment:
"This method extends beyond the ILockingService interface. See issue #67 for proposal to add this to MSR Core."
Proposed Solution
Add two optional methods to ILockingService<DB> for backwards compatibility:
interface ILockingService<DB extends IDB> {
// Existing methods (v0.8.0)
acquireLock(executorId: string, timeout: number): Promise<boolean>;
verifyLockOwnership(executorId: string): Promise<boolean>;
releaseLock(executorId: string): Promise<void>;
forceReleaseLock(): Promise<void>;
checkAndReleaseExpiredLock(): Promise<void>;
getLockStatus(): Promise<ILockStatus | null>;
// New optional methods (v0.8.1) - NON-BREAKING
/**
* Initialize lock storage (create tables, indexes, paths, etc.).
*
* Called during handler initialization to set up lock storage.
* Implementations should create necessary database structures and
* verify required permissions.
*
* @throws Error if storage cannot be created (permissions, network, etc.)
* @example
* ```typescript
* // PostgreSQL
* async initLockStorage() {
* await this.db.query(`
* CREATE TABLE IF NOT EXISTS migration_locks (
* id SERIAL PRIMARY KEY,
* executor_id VARCHAR(255) UNIQUE NOT NULL,
* locked_at TIMESTAMP NOT NULL,
* expires_at TIMESTAMP NOT NULL
* )
* `);
* }
*
* // Firebase
* async initLockStorage() {
* const ref = this.db.database.ref(this.lockPath);
* await ref.once('value'); // Verify read access
* }
*
* // MongoDB
* async initLockStorage() {
* await this.db.createCollection('migration_locks');
* await this.db.collection('migration_locks').createIndex(
* { executor_id: 1 },
* { unique: true }
* );
* }
* ```
*/
initLockStorage?(): Promise<void>;
/**
* Verify lock storage is accessible.
*
* Used for pre-flight checks before migrations to ensure database
* connection is active and lock storage has proper read/write permissions.
*
* @returns true if accessible, false otherwise
* @note Does NOT throw - returns false for any access issues
* @example
* ```typescript
* // Pre-flight check before migration
* if (handler.lockingService?.ensureLockStorageAccessible) {
* const accessible = await handler.lockingService.ensureLockStorageAccessible();
* if (\!accessible) {
* throw new Error('Lock storage is not accessible');
* }
* }
* ```
*/
ensureLockStorageAccessible?(): Promise<boolean>;
}
Benefits
- ✅ Fail-Fast - Setup errors caught during initialization, not migration
- ✅ Explicit Lifecycle - Clear when storage is created
- ✅ Standard Pattern - Consistent across all adapters
- ✅ Better Errors - Setup vs runtime failures are distinct
- ✅ Testability - Can test initialization separately
- ✅ Backwards Compatible - Optional methods, no breaking changes
Implementation Notes
When to Call
Option 1: Handler initialization (recommended)
class MyDatabaseHandler implements IDatabaseMigrationHandler<DB> {
constructor(db: DB) {
this.db = db;
this.lockingService = new MyLockingService(db);
}
async initialize(): Promise<void> {
await this.schemaVersion.initTable();
if (this.lockingService?.initLockStorage) {
await this.lockingService.initLockStorage();
}
}
}
const handler = new MyDatabaseHandler(db);
await handler.initialize();
Option 2: MigrationWorkflowOrchestrator (automatic)
// In executeBeforeMigrate() or similar
if (this.handler.lockingService?.initLockStorage) {
await this.handler.lockingService.initLockStorage();
}
Error Handling
initLockStorage() → Throws on failure (fail-fast setup)
ensureLockStorageAccessible() → Returns boolean (pre-flight check)
Databases That Don't Need Setup
class InMemoryLockingService implements ILockingService<IDB> {
async initLockStorage(): Promise<void> {
// No-op - nothing to initialize
}
async ensureLockStorageAccessible(): Promise<boolean> {
return true; // Always accessible
}
}
Tasks
Related
Future Considerations
In v1.0.0, consider making these methods required (breaking change) for cleaner API and enforced consistency across all adapters.
Problem
The
ILockingServiceinterface in v0.8.0 lacks lifecycle management methods for lock storage initialization. This forces adapters to handle setup implicitly during first lock acquisition, which:Evidence
The Firebase adapter already recognized this gap and implemented these methods as custom extensions beyond the interface:
Both methods include this comment:
Proposed Solution
Add two optional methods to
ILockingService<DB>for backwards compatibility:Benefits
Implementation Notes
When to Call
Option 1: Handler initialization (recommended)
Option 2: MigrationWorkflowOrchestrator (automatic)
Error Handling
initLockStorage()→ Throws on failure (fail-fast setup)ensureLockStorageAccessible()→ Returns boolean (pre-flight check)Databases That Don't Need Setup
Tasks
ILockingService<DB>interfaceRelated
Future Considerations
In v1.0.0, consider making these methods required (breaking change) for cleaner API and enforced consistency across all adapters.