remove the lock functionality
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -8,25 +8,8 @@ import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { Duration } from 'luxon';
|
||||
import { Logger as Logger_2 } from 'winston';
|
||||
|
||||
// @public
|
||||
export interface LockOptions {
|
||||
timeout: Duration;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface PluginTaskManager {
|
||||
acquireLock(
|
||||
id: string,
|
||||
options: LockOptions,
|
||||
): Promise<
|
||||
| {
|
||||
acquired: false;
|
||||
}
|
||||
| {
|
||||
acquired: true;
|
||||
release(): Promise<void>;
|
||||
}
|
||||
>;
|
||||
scheduleTask(
|
||||
id: string,
|
||||
options: TaskOptions,
|
||||
|
||||
@@ -20,30 +20,6 @@
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
//
|
||||
// mutexes
|
||||
//
|
||||
await knex.schema.createTable('backstage_backend_tasks__mutexes', table => {
|
||||
table.comment('Locks used for mutual exclusion among multiple workers');
|
||||
table
|
||||
.text('id')
|
||||
.primary()
|
||||
.notNullable()
|
||||
.comment('The unique ID of this particular mutex');
|
||||
table
|
||||
.text('current_lock_ticket')
|
||||
.notNullable()
|
||||
.comment('A unique ticket for the current mutex lock');
|
||||
table
|
||||
.dateTime('current_lock_acquired_at')
|
||||
.nullable()
|
||||
.comment('The time when the mutex was locked');
|
||||
table
|
||||
.dateTime('current_lock_expires_at')
|
||||
.nullable()
|
||||
.comment('The time when a locked mutex will time out and auto-release');
|
||||
table.index(['id'], 'backstage_backend_tasks__mutexes__id_idx');
|
||||
});
|
||||
//
|
||||
// tasks
|
||||
//
|
||||
@@ -89,11 +65,4 @@ exports.down = async function down(knex) {
|
||||
table.dropIndex([], 'backstage_backend_tasks__tasks__id_idx');
|
||||
});
|
||||
await knex.schema.dropTable('backstage_backend_tasks__tasks');
|
||||
//
|
||||
// locks
|
||||
//
|
||||
await knex.schema.alterTable('backstage_backend_tasks__task_locks', table => {
|
||||
table.dropIndex([], 'backstage_backend_tasks__task_locks__id_idx');
|
||||
});
|
||||
await knex.schema.dropTable('backstage_backend_tasks__task_locks');
|
||||
};
|
||||
|
||||
@@ -15,16 +15,8 @@
|
||||
*/
|
||||
|
||||
export const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations';
|
||||
export const DB_MUTEXES_TABLE = 'backstage_backend_tasks__mutexes';
|
||||
export const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks';
|
||||
|
||||
export type DbMutexesRow = {
|
||||
id: string;
|
||||
current_lock_ticket: string;
|
||||
current_lock_acquired_at?: Date | string;
|
||||
current_lock_expires_at?: Date | string;
|
||||
};
|
||||
|
||||
export type DbTasksRow = {
|
||||
id: string;
|
||||
settings_json: string;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { Duration } from 'luxon';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
import { migrateBackendTasks } from '../database/migrateBackendTasks';
|
||||
import { PluginTaskManagerImpl } from './PluginTaskManagerImpl';
|
||||
@@ -36,86 +35,6 @@ describe('PluginTaskManagerImpl', () => {
|
||||
return { knex, manager };
|
||||
}
|
||||
|
||||
describe('acquireLock', () => {
|
||||
it.each(databases.eachSupportedId())(
|
||||
'can run the happy path, %p',
|
||||
async databaseId => {
|
||||
const { manager } = await init(databaseId);
|
||||
|
||||
const lock1 = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
const lock2 = await manager.acquireLock('lock2', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
|
||||
expect(lock1.acquired).toBe(true);
|
||||
expect(lock2.acquired).toBe(true);
|
||||
|
||||
await expect(
|
||||
manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
}),
|
||||
).resolves.toEqual({ acquired: false });
|
||||
|
||||
await (lock1 as any).release();
|
||||
await (lock2 as any).release();
|
||||
|
||||
const lock1Again = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
expect(lock1Again.acquired).toBe(true);
|
||||
await (lock1Again as any).release();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'rejects double lock attempts, %p',
|
||||
async databaseId => {
|
||||
const { manager } = await init(databaseId);
|
||||
|
||||
const lock1 = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
const lock2 = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
|
||||
expect(lock1.acquired).toBe(true);
|
||||
expect(lock2.acquired).toBe(false);
|
||||
|
||||
await (lock1 as any).release();
|
||||
|
||||
const lock1Again = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
expect(lock1Again.acquired).toBe(true);
|
||||
await (lock1Again as any).release();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'times out locks, %p',
|
||||
async databaseId => {
|
||||
const { manager } = await init(databaseId);
|
||||
|
||||
const lock1 = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(200),
|
||||
});
|
||||
|
||||
expect(lock1.acquired).toBe(true);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const lock2 = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
expect(lock2.acquired).toBe(true);
|
||||
await (lock2 as any).release();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// This is just to test the wrapper code; most of the actual tests are in
|
||||
// TaskWorker.test.ts
|
||||
describe('scheduleTask', () => {
|
||||
|
||||
@@ -14,14 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isDatabaseConflictError } from '@backstage/backend-common';
|
||||
import { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import { DbMutexesRow, DB_MUTEXES_TABLE } from '../database/tables';
|
||||
import { TaskWorker } from './TaskWorker';
|
||||
import { LockOptions, PluginTaskManager, TaskOptions } from './types';
|
||||
import { nowPlus, validateId } from './util';
|
||||
import { PluginTaskManager, TaskOptions } from './types';
|
||||
import { validateId } from './util';
|
||||
|
||||
/**
|
||||
* Implements the actual task management.
|
||||
@@ -32,57 +29,6 @@ export class PluginTaskManagerImpl implements PluginTaskManager {
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async acquireLock(
|
||||
id: string,
|
||||
options: LockOptions,
|
||||
): Promise<
|
||||
{ acquired: false } | { acquired: true; release(): Promise<void> }
|
||||
> {
|
||||
validateId(id);
|
||||
|
||||
const knex = await this.databaseFactory();
|
||||
const ticket = uuid();
|
||||
|
||||
const release = async () => {
|
||||
try {
|
||||
await knex<DbMutexesRow>(DB_MUTEXES_TABLE)
|
||||
.where('id', '=', id)
|
||||
.where('current_lock_ticket', '=', ticket)
|
||||
.delete();
|
||||
} catch (e) {
|
||||
this.logger.warn(`Failed to release lock, ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
const record: Knex.DbRecord<DbMutexesRow> = {
|
||||
current_lock_ticket: ticket,
|
||||
current_lock_acquired_at: knex.fn.now(),
|
||||
current_lock_expires_at: options.timeout
|
||||
? nowPlus(options.timeout, knex)
|
||||
: knex.raw('null'),
|
||||
};
|
||||
|
||||
// First try to overwrite an existing lock, that has timed out
|
||||
const stolen = await knex<DbMutexesRow>(DB_MUTEXES_TABLE)
|
||||
.where('id', '=', id)
|
||||
.where('current_lock_expires_at', '<', knex.fn.now())
|
||||
.update(record);
|
||||
|
||||
if (stolen) {
|
||||
return { acquired: true, release };
|
||||
}
|
||||
|
||||
try {
|
||||
await knex<DbMutexesRow>(DB_MUTEXES_TABLE).insert({ id, ...record });
|
||||
return { acquired: true, release };
|
||||
} catch (e) {
|
||||
if (!isDatabaseConflictError(e)) {
|
||||
this.logger.warn(`Failed to acquire lock, ${e}`);
|
||||
}
|
||||
return { acquired: false };
|
||||
}
|
||||
}
|
||||
|
||||
async scheduleTask(
|
||||
id: string,
|
||||
options: TaskOptions,
|
||||
|
||||
@@ -17,12 +17,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Duration } from 'luxon';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
DbMutexesRow,
|
||||
DbTasksRow,
|
||||
DB_MUTEXES_TABLE,
|
||||
DB_TASKS_TABLE,
|
||||
} from '../database/tables';
|
||||
import { DbTasksRow, DB_TASKS_TABLE } from '../database/tables';
|
||||
import { CancelToken } from './CancelToken';
|
||||
|
||||
/**
|
||||
@@ -70,22 +65,6 @@ export class PluginTaskManagerJanitor {
|
||||
// https://github.com/knex/knex/issues/4370
|
||||
// https://github.com/mapbox/node-sqlite3/issues/1453
|
||||
|
||||
const mutexesQuery = this.knex<DbMutexesRow>(DB_MUTEXES_TABLE)
|
||||
.where('current_lock_expires_at', '<', this.knex.fn.now())
|
||||
.delete();
|
||||
|
||||
if (this.knex.client.config.client === 'sqlite3') {
|
||||
const mutexes = await mutexesQuery;
|
||||
if (mutexes > 0) {
|
||||
this.logger.warn(`${mutexes} mutex locks timed out and were lost`);
|
||||
}
|
||||
} else {
|
||||
const mutexes = await mutexesQuery.returning(['id']);
|
||||
for (const { id } of mutexes) {
|
||||
this.logger.warn(`Mutex lock timed out and was lost: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const tasksQuery = this.knex<DbTasksRow>(DB_TASKS_TABLE)
|
||||
.where('current_run_expires_at', '<', this.knex.fn.now())
|
||||
.delete();
|
||||
|
||||
@@ -43,10 +43,14 @@ describe('TaskManager', () => {
|
||||
const database = await createDatabase(databaseId);
|
||||
const manager = new TaskManager(database, logger).forPlugin('test');
|
||||
|
||||
const lock = await manager.acquireLock('lock1', {
|
||||
timeout: Duration.fromMillis(5000),
|
||||
});
|
||||
expect(lock.acquired).toBe(true);
|
||||
const task = await manager.scheduleTask(
|
||||
'task1',
|
||||
{
|
||||
timeout: Duration.fromMillis(5000),
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
expect(task.unschedule).toBeDefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
|
||||
export { TaskManager } from './TaskManager';
|
||||
export type { LockOptions, PluginTaskManager, TaskOptions } from './types';
|
||||
export type { PluginTaskManager, TaskOptions } from './types';
|
||||
|
||||
@@ -17,19 +17,6 @@
|
||||
import { Duration } from 'luxon';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Options that apply to the acquiral of a given lock.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface LockOptions {
|
||||
/**
|
||||
* The maximum amount of time that the lock can be held, before it's
|
||||
* considered timed out and gets auto-released by the framework.
|
||||
*/
|
||||
timeout: Duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that apply to the invocation of a given task.
|
||||
*
|
||||
@@ -82,26 +69,6 @@ export interface TaskOptions {
|
||||
* @public
|
||||
*/
|
||||
export interface PluginTaskManager {
|
||||
/**
|
||||
* Attempts to acquire an exclusive lock.
|
||||
*
|
||||
* A lock can only be held by one party at a time. Any subsequent attempts to
|
||||
* acquire the lock will fail, unless the timeout period has been exceeded or
|
||||
* the lock was released by the previous holder.
|
||||
*
|
||||
* @param id - A unique ID (within the scope of the plugin) for a lock
|
||||
* @param options - Options for the lock
|
||||
* @returns The result of the lock attempt. If it was successfully acquired,
|
||||
* you should remember to call its `release` method as soon as you
|
||||
* are done with the lock.
|
||||
*/
|
||||
acquireLock(
|
||||
id: string,
|
||||
options: LockOptions,
|
||||
): Promise<
|
||||
{ acquired: false } | { acquired: true; release(): Promise<void> }
|
||||
>;
|
||||
|
||||
/**
|
||||
* Schedules a task function for coordinated exclusive invocation across
|
||||
* workers.
|
||||
|
||||
Reference in New Issue
Block a user