mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
Change the backups so they stop using their own task system, and use the same system as XO Tasks. For the moment, it shouldn't affect the users, but when the previous backup logs will be old enough to be erased, this will allow us to have shorter loading times for backup logs. XO-52
264 lines
6.1 KiB
JavaScript
264 lines
6.1 KiB
JavaScript
'use strict'
|
|
|
|
const assert = require('node:assert').strict
|
|
const { AsyncLocalStorage } = require('node:async_hooks')
|
|
|
|
function alreadyEnded() {
|
|
throw new Error('task has already ended')
|
|
}
|
|
|
|
const serializeErrors = errors => (Array.isArray(errors) ? errors.map(serializeError) : errors)
|
|
|
|
// Create a serializable object from an error.
|
|
//
|
|
// Otherwise some fields might be non-enumerable and missing from logs.
|
|
const serializeError = error => {
|
|
return error instanceof Error
|
|
? {
|
|
...error, // Copy enumerable properties.
|
|
cause: error.cause,
|
|
code: error.code,
|
|
errors: serializeErrors(error.errors), // supports AggregateError
|
|
message: error.message,
|
|
name: error.name,
|
|
stack: error.stack,
|
|
}
|
|
: error
|
|
}
|
|
|
|
exports.serializeError = serializeError
|
|
|
|
// define a read-only, non-enumerable, non-configurable property
|
|
function define(object, property, value) {
|
|
Object.defineProperty(object, property, { value })
|
|
}
|
|
|
|
const noop = Function.prototype
|
|
|
|
const FAILURE = 'failure'
|
|
const PENDING = 'pending'
|
|
const SUCCESS = 'success'
|
|
exports.STATUS = { FAILURE, PENDING, SUCCESS }
|
|
|
|
// stored in the global context so that various versions of the library can interact.
|
|
const asyncStorageKey = '@vates/task@0'
|
|
const asyncStorage = global[asyncStorageKey] ?? (global[asyncStorageKey] = new AsyncLocalStorage())
|
|
|
|
const getTask = () => asyncStorage.getStore()
|
|
|
|
class Task {
|
|
static get abortSignal() {
|
|
const task = getTask()
|
|
if (task !== undefined) {
|
|
return task._abortController.signal
|
|
}
|
|
}
|
|
|
|
static run(...args) {
|
|
let opts = args[0]
|
|
let fn
|
|
if (typeof opts === 'object') {
|
|
args.shift()
|
|
fn = args[0]
|
|
} else {
|
|
fn = opts
|
|
opts = undefined
|
|
}
|
|
args.shift()
|
|
|
|
const thisArg = this !== Task ? this : undefined
|
|
|
|
if (typeof fn !== 'function') {
|
|
fn = this[fn]
|
|
}
|
|
|
|
return new Task(opts).run(() => fn.apply(thisArg, args))
|
|
}
|
|
|
|
static wrap(opts, fn) {
|
|
// compatibility with @decorateWith
|
|
if (typeof fn !== 'function') {
|
|
;[fn, opts] = [opts, fn]
|
|
}
|
|
|
|
return function taskRun() {
|
|
return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
|
|
}
|
|
}
|
|
|
|
// These two properties should not be used outside the class
|
|
//
|
|
// To ensure compatibility between multiple versions of this lib, they are kept public for now
|
|
_abortController = new AbortController()
|
|
_onProgress
|
|
|
|
get id() {
|
|
// Use a compact, sortable, string representation of the creation date,
|
|
// and add a random part to differentiate tasks created at the same time
|
|
//
|
|
// Due to the padding, dates are sortable up to 5188-04-22T11:04:28.415Z
|
|
return (this.id = Date.now().toString(36).padStart(9, '0') + '-' + Math.random().toString(36).slice(2))
|
|
}
|
|
set id(value) {
|
|
define(this, 'id', value)
|
|
}
|
|
|
|
#startData
|
|
|
|
#status = PENDING
|
|
get status() {
|
|
return this.#status
|
|
}
|
|
|
|
constructor({ properties, onProgress } = {}) {
|
|
this.#startData = { properties }
|
|
|
|
if (onProgress !== undefined) {
|
|
this._onProgress = onProgress
|
|
} else {
|
|
const parent = getTask()
|
|
if (parent !== undefined) {
|
|
const { signal } = parent._abortController
|
|
signal.addEventListener('abort', () => {
|
|
this._abortController.abort(signal.reason)
|
|
})
|
|
|
|
this._onProgress = parent._onProgress
|
|
this.#startData.parentId = parent.id
|
|
} else {
|
|
this._onProgress = noop
|
|
}
|
|
}
|
|
|
|
const { signal } = this._abortController
|
|
signal.addEventListener('abort', () => {
|
|
if (this.status === PENDING) {
|
|
this.#maybeStart()
|
|
|
|
this.#emit('abortionRequested', { reason: signal.reason })
|
|
|
|
if (!this.#running) {
|
|
this.#end(FAILURE, signal.reason)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
abort(reason) {
|
|
this._abortController.abort(reason)
|
|
}
|
|
|
|
#emit(type, data) {
|
|
assert.equal(this.#startData, undefined, 'task has not started yet')
|
|
|
|
data.id = this.id
|
|
data.timestamp = Date.now()
|
|
data.type = type
|
|
this._onProgress(data)
|
|
}
|
|
|
|
#end(status, result) {
|
|
assert.equal(this.#status, PENDING)
|
|
assert.equal(this.#running, false)
|
|
|
|
if (result instanceof Error && result.toJSON === undefined) {
|
|
result = serializeError(result)
|
|
}
|
|
|
|
this.#emit('end', { status, result })
|
|
this._onProgress = alreadyEnded
|
|
this.#status = status
|
|
}
|
|
|
|
failure(error) {
|
|
this.#end(FAILURE, error)
|
|
}
|
|
|
|
info(message, data) {
|
|
this.#emit('info', { data, message })
|
|
}
|
|
|
|
#maybeStart() {
|
|
if (this.#startData !== undefined) {
|
|
this.start()
|
|
}
|
|
}
|
|
|
|
async run(fn) {
|
|
const result = await this.runInside(fn)
|
|
if (this.status === PENDING) {
|
|
this.#end(SUCCESS, result)
|
|
}
|
|
return result
|
|
}
|
|
|
|
#running = false
|
|
async runInside(fn) {
|
|
assert.equal(this.status, PENDING)
|
|
assert.equal(this.#running, false)
|
|
this.#running = true
|
|
|
|
this.#maybeStart()
|
|
|
|
try {
|
|
const result = await asyncStorage.run(this, fn, this)
|
|
this.#running = false
|
|
return result
|
|
} catch (result) {
|
|
this.#running = false
|
|
this.#end(FAILURE, result)
|
|
throw result
|
|
}
|
|
}
|
|
|
|
set(name, value) {
|
|
assert.equal(this.status, PENDING)
|
|
|
|
this.#emit('property', { name, value })
|
|
}
|
|
|
|
start() {
|
|
const startData = this.#startData
|
|
assert.notEqual(startData, undefined, 'task has already started')
|
|
|
|
this.#startData = undefined
|
|
this.#emit('start', startData)
|
|
}
|
|
|
|
success(result) {
|
|
this.#end(SUCCESS, result)
|
|
}
|
|
|
|
warning(message, data) {
|
|
assert.equal(this.status, PENDING)
|
|
|
|
this.#emit('warning', { data, message })
|
|
}
|
|
|
|
wrap(fn) {
|
|
const task = this
|
|
return function taskRun() {
|
|
return task.run(() => fn.apply(this, arguments))
|
|
}
|
|
}
|
|
|
|
wrapInside(fn) {
|
|
const task = this
|
|
return function taskRunInside() {
|
|
return task.runInside(() => fn.apply(this, arguments))
|
|
}
|
|
}
|
|
}
|
|
exports.Task = Task
|
|
|
|
// setup static aliases
|
|
for (const name of ['info', 'set', 'warning']) {
|
|
const method = Task.prototype[name]
|
|
Task[name] = function () {
|
|
const task = getTask()
|
|
if (task !== undefined) {
|
|
return method.apply(task, arguments)
|
|
}
|
|
}
|
|
}
|