@xen-orchestra/backups 0.71.2 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { Task } from './Task.mjs'
1
+ import { Task } from '@vates/task'
2
2
 
3
3
  export class HealthCheckVmBackup {
4
4
  #restoredVm
@@ -14,7 +14,7 @@ export class HealthCheckVmBackup {
14
14
  async run() {
15
15
  return Task.run(
16
16
  {
17
- name: 'vmstart',
17
+ properties: { name: 'vmstart' },
18
18
  },
19
19
  async () => {
20
20
  let restoredVm = this.#restoredVm
@@ -2,9 +2,9 @@ import assert from 'node:assert'
2
2
 
3
3
  import { formatFilenameDate } from './_filenameDate.mjs'
4
4
  import { importIncrementalVm } from './_incrementalVm.mjs'
5
- import { Task } from './Task.mjs'
6
5
  import { watchStreamSize } from './_watchStreamSize.mjs'
7
6
  import { decorateClass } from '@vates/decorate-with'
7
+ import { Task } from '@vates/task'
8
8
  import { createLogger } from '@xen-orchestra/log'
9
9
  import { dirname, join } from 'node:path'
10
10
  import pickBy from 'lodash/pickBy.js'
@@ -244,7 +244,7 @@ export class ImportVmBackup {
244
244
 
245
245
  return Task.run(
246
246
  {
247
- name: 'transfer',
247
+ properties: { name: 'transfer' },
248
248
  },
249
249
  async () => {
250
250
  const xapi = this._xapi
package/_backupWorker.mjs CHANGED
@@ -14,10 +14,10 @@ import { decorateMethodsWith } from '@vates/decorate-with'
14
14
  import { deduped } from '@vates/disposable/deduped.js'
15
15
  import { getHandler } from '@xen-orchestra/fs'
16
16
  import { parseDuration } from '@vates/parse-duration'
17
+ import { Task } from '@vates/task'
17
18
  import { Xapi } from '@xen-orchestra/xapi'
18
19
 
19
20
  import { RemoteAdapter } from './RemoteAdapter.mjs'
20
- import { Task } from './Task.mjs'
21
21
 
22
22
  createCachedLookup().patchGlobal()
23
23
 
@@ -178,8 +178,8 @@ process.on('message', async message => {
178
178
  const result = message.runWithLogs
179
179
  ? await Task.run(
180
180
  {
181
- name: 'backup run',
182
- onLog: data =>
181
+ properties: { name: 'backup run', ...message.data.jobData },
182
+ onProgress: data =>
183
183
  emitMessage({
184
184
  data,
185
185
  type: 'log',
package/_cleanVm.mjs CHANGED
@@ -9,8 +9,8 @@ import { RemoteVhdDisk } from './disks/RemoteVhdDisk.mjs'
9
9
  import { RemoteVhdDiskChain } from './disks/RemoteVhdDiskChain.mjs'
10
10
  import { MergeRemoteDisk } from './disks/MergeRemoteDisk.mjs'
11
11
 
12
- import { Task } from './Task.mjs'
13
12
  import { Disposable } from 'promise-toolbox'
13
+ import { Task } from '@vates/task'
14
14
  import handlerPath from '@xen-orchestra/fs/path'
15
15
 
16
16
  const { DISK_TYPES } = Constants
@@ -514,23 +514,27 @@ export async function cleanVm(
514
514
  })
515
515
  }
516
516
 
517
+ let totalMergedDataSize = 0
517
518
  const metadataWithMergedVhd = {}
518
519
  const doMerge = async () => {
519
520
  await asyncMap(toMerge, async chain => {
520
- const { finalDiskSize } = await limitedMergeVhdChain(handler, chain, {
521
+ const { finalDiskSize, mergedDataSize } = await limitedMergeVhdChain(handler, chain, {
521
522
  logInfo,
522
523
  logWarn,
523
524
  remove,
524
525
  mergeBlockConcurrency,
525
526
  })
527
+ totalMergedDataSize += mergedDataSize
526
528
  const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metadata file
527
529
  metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalDiskSize
528
530
  })
531
+
532
+ return { size: totalMergedDataSize }
529
533
  }
530
534
 
531
535
  await Promise.all([
532
536
  ...unusedVhdsDeletion,
533
- toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : () => Promise.resolve()),
537
+ toMerge.length !== 0 && (merge ? Task.run({ properties: { name: 'merge' } }, doMerge) : () => Promise.resolve()),
534
538
  asyncMap(unusedXvas, path => {
535
539
  logWarn('unused XVA', { path })
536
540
  if (remove) {
@@ -619,5 +623,6 @@ export async function cleanVm(
619
623
  return {
620
624
  // boolean whether some VHDs were merged (or should be merged)
621
625
  merge: toMerge.length !== 0,
626
+ size: totalMergedDataSize,
622
627
  }
623
628
  }
@@ -4,9 +4,9 @@ import { asyncMap } from '@xen-orchestra/async-map'
4
4
  import { CancelToken } from 'promise-toolbox'
5
5
  import { compareVersions } from 'compare-versions'
6
6
  import { defer } from 'golike-defer'
7
+ import { Task } from '@vates/task'
7
8
 
8
9
  import { cancelableMap } from './_cancelableMap.mjs'
9
- import { Task } from './Task.mjs'
10
10
  import pick from 'lodash/pick.js'
11
11
  import { BASE_DELTA_VDI, CONTENT_KEY, COPY_OF, VM_UUID } from './_otherConfig.mjs'
12
12
 
@@ -1,4 +1,5 @@
1
1
  import { asyncMap } from '@xen-orchestra/async-map'
2
+ import { Task } from '@vates/task'
2
3
  import Disposable from 'promise-toolbox/Disposable'
3
4
  import ignoreErrors from 'promise-toolbox/ignoreErrors'
4
5
 
@@ -6,9 +7,10 @@ import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
6
7
  import { PoolMetadataBackup } from './_PoolMetadataBackup.mjs'
7
8
  import { XoMetadataBackup } from './_XoMetadataBackup.mjs'
8
9
  import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
9
- import { runTask } from './_runTask.mjs'
10
10
  import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
11
11
 
12
+ const noop = Function.prototype
13
+
12
14
  const DEFAULT_METADATA_SETTINGS = {
13
15
  retentionPoolMetadata: 0,
14
16
  retentionXoMetadata: 0,
@@ -55,13 +57,16 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
55
57
  poolIds.map(id =>
56
58
  this._getRecord('pool', id).catch(error => {
57
59
  // See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
58
- runTask(
60
+ Task.run(
59
61
  {
60
- name: 'get pool record',
61
- data: { type: 'pool', id },
62
+ properties: {
63
+ id,
64
+ name: 'get pool record',
65
+ type: 'pool',
66
+ },
62
67
  },
63
68
  () => Promise.reject(error)
64
- )
69
+ ).catch(noop)
65
70
  })
66
71
  )
67
72
  ),
@@ -81,11 +86,11 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
81
86
  if (pools.length !== 0 && settings.retentionPoolMetadata !== 0) {
82
87
  promises.push(
83
88
  asyncMap(pools, async pool =>
84
- runTask(
89
+ Task.run(
85
90
  {
86
- name: `Starting metadata backup for the pool (${pool.$id}). (${job.id})`,
87
- data: {
91
+ properties: {
88
92
  id: pool.$id,
93
+ name: `Starting metadata backup for the pool (${pool.$id}). (${job.id})`,
89
94
  pool,
90
95
  poolMaster: await ignoreErrors.call(pool.$xapi.getRecord('host', pool.master)),
91
96
  type: 'pool',
@@ -100,17 +105,17 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
100
105
  schedule,
101
106
  settings,
102
107
  }).run()
103
- )
108
+ ).catch(noop)
104
109
  )
105
110
  )
106
111
  }
107
112
 
108
113
  if (job.xoMetadata !== undefined && settings.retentionXoMetadata !== 0) {
109
114
  promises.push(
110
- runTask(
115
+ Task.run(
111
116
  {
112
- name: `Starting XO metadata backup. (${job.id})`,
113
- data: {
117
+ properties: {
118
+ name: `Starting XO metadata backup. (${job.id})`,
114
119
  type: 'xo',
115
120
  },
116
121
  },
@@ -122,7 +127,7 @@ export const Metadata = class MetadataBackupRunner extends Abstract {
122
127
  schedule,
123
128
  settings,
124
129
  }).run()
125
- )
130
+ ).catch(noop)
126
131
  )
127
132
  }
128
133
  await Promise.all(promises)
@@ -1,9 +1,9 @@
1
1
  import { asyncMapSettled } from '@xen-orchestra/async-map'
2
2
  import Disposable from 'promise-toolbox/Disposable'
3
3
  import { limitConcurrency } from 'limit-concurrency-decorator'
4
+ import { Task } from '@vates/task'
4
5
 
5
6
  import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
6
- import { Task } from '../Task.mjs'
7
7
  import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
8
8
  import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
9
9
  import { FullRemote } from './_vmRunners/FullRemote.mjs'
@@ -78,7 +78,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
78
78
  }
79
79
  nTriesByVmId[vmUuid]++
80
80
 
81
- const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
81
+ const taskStart = { properties: { id: vmUuid, name: 'backup VM', type: 'VM' } }
82
82
  const vmSettings = { ...settings, ...allSettings[vmUuid] }
83
83
  const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
84
84
 
@@ -113,23 +113,27 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
113
113
  taskByVmId[vmUuid] = new Task(taskStart)
114
114
  }
115
115
  const task = taskByVmId[vmUuid]
116
+ // error has to be caught in the task to prevent its failure, but handled outside the task to execute another task.run()
117
+ let taskError
116
118
  return task
117
- .run(async () => {
118
- try {
119
- const result = await vmBackup.run()
120
- task.success(result)
121
- return result
122
- } catch (error) {
123
- if (isLastRun) {
124
- throw error
125
- } else {
126
- Task.warning(`Retry the VM mirror backup due to an error`, {
127
- attempt: nTriesByVmId[vmUuid],
128
- error: error.message,
129
- })
130
- queue.add(vmUuid)
131
- }
119
+ .runInside(async () =>
120
+ vmBackup.run().catch(error => {
121
+ taskError = error
122
+ })
123
+ )
124
+ .then(result => {
125
+ if (taskError === undefined) {
126
+ return task.success(result)
132
127
  }
128
+ if (isLastRun) {
129
+ return task.failure(taskError)
130
+ }
131
+ // don't end the task
132
+ task.warning(`Retry the VM mirror backup due to an error`, {
133
+ attempt: nTriesByVmId[vmUuid],
134
+ error: taskError.message,
135
+ })
136
+ queue.add(vmUuid)
133
137
  })
134
138
  .catch(noop)
135
139
  }
@@ -1,11 +1,10 @@
1
1
  import { asyncMapSettled } from '@xen-orchestra/async-map'
2
2
  import Disposable from 'promise-toolbox/Disposable'
3
3
  import { limitConcurrency } from 'limit-concurrency-decorator'
4
+ import { Task } from '@vates/task'
4
5
 
5
6
  import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
6
- import { Task } from '../Task.mjs'
7
7
  import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
8
- import { runTask } from './_runTask.mjs'
9
8
  import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
10
9
  import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
11
10
  import { FullXapi } from './_vmRunners/FullXapi.mjs'
@@ -65,13 +64,12 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
65
64
  Disposable.all(
66
65
  extractIdsFromSimplePattern(job.srs).map(id =>
67
66
  this._getRecord('SR', id).catch(error => {
68
- runTask(
67
+ Task.run(
69
68
  {
70
- name: 'get SR record',
71
- data: { type: 'SR', id },
69
+ properties: { id, name: 'get SR record', type: 'SR' },
72
70
  },
73
71
  () => Promise.reject(error)
74
- )
72
+ ).catch(noop)
75
73
  })
76
74
  )
77
75
  ),
@@ -108,11 +106,12 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
108
106
  }
109
107
  return taskByVmId[vmUuid]
110
108
  }
111
- const vmBackupFailed = error => {
109
+ const vmBackupFailed = async (error, task) => {
112
110
  if (isLastRun) {
113
- throw error
111
+ return task.failure(error)
114
112
  } else {
115
- Task.warning(`Retry the VM backup due to an error`, {
113
+ // don't end the task
114
+ task.warning(`Retry the VM backup due to an error`, {
116
115
  attempt: nTriesByVmId[vmUuid],
117
116
  error: error.message,
118
117
  })
@@ -126,19 +125,21 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
126
125
  nTriesByVmId[vmUuid]++
127
126
 
128
127
  const vmSettings = { ...settings, ...allSettings[vmUuid] }
129
- const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
128
+ const taskStart = { properties: { id: vmUuid, name: 'backup VM', type: 'VM' } }
130
129
  const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
131
130
 
132
131
  return this._getRecord('VM', vmUuid).then(
133
132
  disposableVm =>
134
133
  Disposable.use(disposableVm, async vm => {
135
- if (taskStart.data.name_label === undefined) {
136
- taskStart.data.name_label = vm.name_label
134
+ if (taskStart.properties.name_label === undefined) {
135
+ taskStart.properties.name_label = vm.name_label
137
136
  }
138
137
 
139
138
  const task = getVmTask()
139
+ // error has to be caught in the task to prevent its failure, but handled outside the task to execute another task.run()
140
+ let taskError
140
141
  return task
141
- .run(async () => {
142
+ .runInside(async () => {
142
143
  const opts = {
143
144
  baseSettings,
144
145
  config,
@@ -164,21 +165,21 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
164
165
  throw new Error(`Job mode ${job.mode} not implemented`)
165
166
  }
166
167
  }
167
-
168
- try {
169
- const result = await vmBackup.run()
168
+ return vmBackup.run().catch(error => {
169
+ taskError = error
170
+ })
171
+ })
172
+ .then(result => {
173
+ if (taskError === undefined) {
170
174
  task.success(result)
171
- return result
172
- } catch (error) {
173
- vmBackupFailed(error)
175
+ } else {
176
+ // ending the task with error or not ending the task
177
+ vmBackupFailed(taskError, task)
174
178
  }
175
179
  })
176
180
  .catch(noop) // errors are handled by logs
177
181
  }),
178
- error =>
179
- getVmTask().run(() => {
180
- vmBackupFailed(error)
181
- })
182
+ error => vmBackupFailed(error, getVmTask())
182
183
  )
183
184
  }
184
185
  const { concurrency } = settings
@@ -1,8 +1,10 @@
1
1
  import Disposable from 'promise-toolbox/Disposable'
2
2
  import pTimeout from 'promise-toolbox/timeout'
3
3
  import { compileTemplate } from '@xen-orchestra/template'
4
- import { runTask } from './_runTask.mjs'
5
4
  import { RemoteTimeoutError } from './_RemoteTimeoutError.mjs'
5
+ import { Task } from '@vates/task'
6
+
7
+ const noop = Function.prototype
6
8
 
7
9
  export const DEFAULT_SETTINGS = {
8
10
  getRemoteTimeout: 300e3,
@@ -36,13 +38,16 @@ export const Abstract = class AbstractRunner {
36
38
  })
37
39
  } catch (error) {
38
40
  // See https://github.com/vatesfr/xen-orchestra/commit/6aa6cfba8ec939c0288f0fa740f6dfad98c43cbb
39
- runTask(
41
+ Task.run(
40
42
  {
41
- name: 'get remote adapter',
42
- data: { type: 'remote', id: remoteId },
43
+ properties: {
44
+ id: remoteId,
45
+ name: 'get remote adapter',
46
+ type: 'remote',
47
+ },
43
48
  },
44
49
  () => Promise.reject(error)
45
- )
50
+ ).catch(noop)
46
51
  }
47
52
  }
48
53
  }
@@ -1,9 +1,9 @@
1
1
  import { asyncMap } from '@xen-orchestra/async-map'
2
+ import { Task } from '@vates/task'
2
3
 
3
4
  import { DIR_XO_POOL_METADATA_BACKUPS } from '../RemoteAdapter.mjs'
4
5
  import { forkStreamUnpipe } from './_forkStreamUnpipe.mjs'
5
6
  import { formatFilenameDate } from '../_filenameDate.mjs'
6
- import { Task } from '../Task.mjs'
7
7
 
8
8
  export const PATH_DB_DUMP = '/pool/xmldbdump'
9
9
 
@@ -54,8 +54,8 @@ export class PoolMetadataBackup {
54
54
  ([remoteId, adapter]) =>
55
55
  Task.run(
56
56
  {
57
- name: `Starting metadata backup for the pool (${pool.$id}) for the remote (${remoteId}). (${job.id})`,
58
- data: {
57
+ properties: {
58
+ name: `Starting metadata backup for the pool (${pool.$id}) for the remote (${remoteId}). (${job.id})`,
59
59
  id: remoteId,
60
60
  type: 'remote',
61
61
  },
@@ -1,9 +1,9 @@
1
1
  import { asyncMap } from '@xen-orchestra/async-map'
2
2
  import { join } from '@xen-orchestra/fs/path'
3
+ import { Task } from '@vates/task'
3
4
 
4
5
  import { DIR_XO_CONFIG_BACKUPS } from '../RemoteAdapter.mjs'
5
6
  import { formatFilenameDate } from '../_filenameDate.mjs'
6
- import { Task } from '../Task.mjs'
7
7
 
8
8
  export class XoMetadataBackup {
9
9
  constructor({ config, job, remoteAdapters, schedule, settings }) {
@@ -51,8 +51,8 @@ export class XoMetadataBackup {
51
51
  ([remoteId, adapter]) =>
52
52
  Task.run(
53
53
  {
54
- name: `Starting XO metadata backup for the remote (${remoteId}). (${job.id})`,
55
- data: {
54
+ properties: {
55
+ name: `Starting XO metadata backup for the remote (${remoteId}). (${job.id})`,
56
56
  id: remoteId,
57
57
  type: 'remote',
58
58
  },
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from '@xen-orchestra/log'
2
+ import { Task } from '@vates/task'
2
3
  import keyBy from 'lodash/keyBy.js'
3
4
 
4
5
  import { AbstractXapi } from './_AbstractXapi.mjs'
@@ -6,7 +7,6 @@ import { forkDeltaExport } from './_forkDeltaExport.mjs'
6
7
  import { exportIncrementalVm } from '../../_incrementalVm.mjs'
7
8
  import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
8
9
  import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
9
- import { Task } from '../../Task.mjs'
10
10
  import {
11
11
  DATETIME,
12
12
  DELTA_CHAIN_LENGTH,
@@ -1,6 +1,6 @@
1
1
  import { asyncMap } from '@xen-orchestra/async-map'
2
2
  import { createLogger } from '@xen-orchestra/log'
3
- import { Task } from '../../Task.mjs'
3
+ import { Task } from '@vates/task'
4
4
 
5
5
  const { debug, warn } = createLogger('xo:backups:AbstractVmRunner')
6
6
 
@@ -83,7 +83,7 @@ export const Abstract = class AbstractVmBackupRunner {
83
83
  // create a task to have an info in the logs and reports
84
84
  return Task.run(
85
85
  {
86
- name: 'health check',
86
+ properties: { name: 'health check' },
87
87
  },
88
88
  () => {
89
89
  Task.info(`This VM doesn't match the health check's tags for this schedule`)
@@ -4,12 +4,12 @@ import { decorateMethodsWith } from '@vates/decorate-with'
4
4
  import { defer } from 'golike-defer'
5
5
  import { Disposable } from 'promise-toolbox'
6
6
  import { createPredicate } from 'value-matcher'
7
+ import { Task } from '@vates/task'
7
8
 
8
9
  import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
9
10
 
10
11
  import { Abstract } from './_Abstract.mjs'
11
12
  import { extractIdsFromSimplePattern } from '../../extractIdsFromSimplePattern.mjs'
12
- import { Task } from '../../Task.mjs'
13
13
 
14
14
  export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
15
15
  _filterPredicate
@@ -6,9 +6,9 @@ import { asyncMap } from '@xen-orchestra/async-map'
6
6
  import { asyncEach } from '@vates/async-each'
7
7
  import { decorateMethodsWith } from '@vates/decorate-with'
8
8
  import { defer } from 'golike-defer'
9
+ import { Task } from '@vates/task'
9
10
 
10
11
  import { getOldEntries } from '../../_getOldEntries.mjs'
11
- import { Task } from '../../Task.mjs'
12
12
  import { Abstract } from './_Abstract.mjs'
13
13
  import {
14
14
  COPY_OF,
@@ -186,7 +186,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
186
186
  const settings = this._settings
187
187
 
188
188
  if (await this._mustDoSnapshot()) {
189
- await Task.run({ name: 'snapshot' }, async () => {
189
+ await Task.run({ properties: { name: 'snapshot' } }, async () => {
190
190
  if (!settings.bypassVdiChainsCheck) {
191
191
  await vm.$assertHealthyVdiChains()
192
192
  }
@@ -1,6 +1,7 @@
1
+ import { Task } from '@vates/task'
2
+
1
3
  import { formatFilenameDate } from '../../_filenameDate.mjs'
2
4
  import { getOldEntries } from '../../_getOldEntries.mjs'
3
- import { Task } from '../../Task.mjs'
4
5
 
5
6
  import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
6
7
  import { AbstractFullWriter } from './_AbstractFullWriter.mjs'
@@ -9,11 +10,11 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
9
10
  constructor(props) {
10
11
  super(props)
11
12
 
12
- this.run = Task.wrapFn(
13
+ this.run = Task.wrap(
13
14
  {
14
- name: 'export',
15
- data: {
15
+ properties: {
16
16
  id: props.remoteId,
17
+ name: 'export',
17
18
  type: 'remote',
18
19
 
19
20
  // necessary?
@@ -64,7 +65,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
64
65
  await deleteOldBackups()
65
66
  }
66
67
 
67
- await Task.run({ name: 'transfer' }, async () => {
68
+ await Task.run({ properties: { name: 'transfer' } }, async () => {
68
69
  await adapter.outputStream(dataFilename, stream, {
69
70
  maxStreamLength,
70
71
  streamLength,
@@ -1,9 +1,9 @@
1
1
  import ignoreErrors from 'promise-toolbox/ignoreErrors'
2
2
  import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
3
+ import { Task } from '@vates/task'
3
4
 
4
5
  import { formatFilenameDate } from '../../_filenameDate.mjs'
5
6
  import { getOldEntries } from '../../_getOldEntries.mjs'
6
- import { Task } from '../../Task.mjs'
7
7
 
8
8
  import { AbstractFullWriter } from './_AbstractFullWriter.mjs'
9
9
  import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
@@ -14,11 +14,11 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
14
14
  constructor(props) {
15
15
  super(props)
16
16
 
17
- this.run = Task.wrapFn(
17
+ this.run = Task.wrap(
18
18
  {
19
- name: 'export',
20
- data: {
19
+ properties: {
21
20
  id: props.sr.uuid,
21
+ name: 'export',
22
22
  name_label: this._sr.name_label,
23
23
  type: 'SR',
24
24
 
@@ -52,7 +52,7 @@ export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
52
52
  }
53
53
 
54
54
  let targetVmRef
55
- await Task.run({ name: 'transfer' }, async () => {
55
+ await Task.run({ properties: { name: 'transfer' } }, async () => {
56
56
  targetVmRef = await xapi.VM_import(stream, sr.$ref, vm =>
57
57
  Promise.all([
58
58
  !_warmMigration && vm.add_tags('Disaster Recovery'),
@@ -7,10 +7,10 @@ import { createLogger } from '@xen-orchestra/log'
7
7
  import { decorateClass } from '@vates/decorate-with'
8
8
  import { defer } from 'golike-defer'
9
9
  import { dirname, basename } from 'node:path'
10
+ import { Task } from '@vates/task'
10
11
 
11
12
  import { formatFilenameDate } from '../../_filenameDate.mjs'
12
13
  import { getOldEntries } from '../../_getOldEntries.mjs'
13
- import { Task } from '../../Task.mjs'
14
14
 
15
15
  import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
16
16
  import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
@@ -72,19 +72,20 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
72
72
  prepare({ isFull }) {
73
73
  // create the task related to this export and ensure all methods are called in this context
74
74
  const task = new Task({
75
- name: 'export',
76
- data: {
75
+ properties: {
77
76
  id: this._remoteId,
78
77
  isFull,
78
+ name: 'export',
79
79
  type: 'remote',
80
80
  },
81
81
  })
82
- this.transfer = task.wrapFn(this.transfer)
83
- this.healthCheck = task.wrapFn(this.healthCheck)
84
- this.cleanup = task.wrapFn(this.cleanup)
85
- this.afterBackup = task.wrapFn(this.afterBackup, true)
82
+ this._prepare = task.wrapInside(this._prepare)
83
+ this.transfer = task.wrapInside(this.transfer)
84
+ this.healthCheck = task.wrapInside(this.healthCheck)
85
+ this.cleanup = task.wrapInside(this.cleanup)
86
+ this.afterBackup = task.wrap(this.afterBackup)
86
87
 
87
- return task.run(() => this._prepare())
88
+ return this._prepare()
88
89
  }
89
90
 
90
91
  async _prepare() {
@@ -223,7 +224,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
223
224
  vtpms: deltaExport.vtpms,
224
225
  }
225
226
  let size = 0
226
- await Task.run({ name: 'transfer' }, async () => {
227
+ await Task.run({ properties: { name: 'transfer' } }, async () => {
227
228
  await asyncEach(
228
229
  Object.entries(deltaExport.disks),
229
230
  async ([diskRef, disk]) => {
@@ -1,11 +1,11 @@
1
1
  import humanFormat from 'human-format'
2
2
 
3
3
  import { asyncMapSettled } from '@xen-orchestra/async-map'
4
+ import { Task } from '@vates/task'
4
5
  import ignoreErrors from 'promise-toolbox/ignoreErrors'
5
6
 
6
7
  import { getOldEntries } from '../../_getOldEntries.mjs'
7
8
  import { importIncrementalVm } from '../../_incrementalVm.mjs'
8
- import { Task } from '../../Task.mjs'
9
9
 
10
10
  import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
11
11
  import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
@@ -65,8 +65,9 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
65
65
  snapshotCandidates,
66
66
  async snapshot => {
67
67
  let diffDisk
68
+ let activeVdi
68
69
  try {
69
- const activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
70
+ activeVdi = sr.$xapi.getObject(snapshot.$snapshot_of)
70
71
  const userVbds = activeVdi.$VBDs?.filter(vbd => vbd.$VM && !vbd.$VM.is_control_domain) ?? []
71
72
  if (userVbds.length !== 1) {
72
73
  debug('checkBaseVdis, share vbd ', { ref: snapshot.$ref, userVbds })
@@ -81,7 +82,12 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
81
82
  // but it indicates the users played with the blocked operations
82
83
  return
83
84
  }
84
- diffDisk = new XapiDiskSource({ xapi: sr.$xapi, vdiRef: activeVdi.$ref, baseRef: snapshot.$ref })
85
+ diffDisk = new XapiDiskSource({
86
+ xapi: sr.$xapi,
87
+ vdiRef: activeVdi.$ref,
88
+ baseRef: snapshot.$ref,
89
+ onlyListChangedBlocks: true,
90
+ })
85
91
  await diffDisk.init()
86
92
  if (diffDisk.getBlockIndexes().length === 0) {
87
93
  const sourceUuid = snapshot.other_config?.[COPY_OF]
@@ -103,6 +109,10 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
103
109
  return
104
110
  } finally {
105
111
  await diffDisk?.close().catch(error => debug('checkBaseVdis, error closing', error))
112
+ await sr.$xapi.VDI_disconnectFromControlDomain(snapshot.$ref)
113
+ if (activeVdi !== undefined) {
114
+ await sr.$xapi.VDI_disconnectFromControlDomain(activeVdi.$ref)
115
+ }
106
116
  }
107
117
  },
108
118
  {
@@ -141,20 +151,24 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
141
151
  prepare({ isFull }) {
142
152
  // create the task related to this export and ensure all methods are called in this context
143
153
  const task = new Task({
144
- name: 'export',
145
- data: {
154
+ properties: {
146
155
  id: this._sr.uuid,
147
156
  isFull,
157
+ name: 'export',
148
158
  name_label: this._sr.name_label,
149
159
  type: 'SR',
150
160
  },
151
161
  })
152
- const hasHealthCheckSr = this._healthCheckSr !== undefined
153
- this.transfer = task.wrapFn(this.transfer)
154
- this.cleanup = task.wrapFn(this.cleanup, !hasHealthCheckSr)
155
- this.healthCheck = task.wrapFn(this.healthCheck, hasHealthCheckSr)
162
+ this._prepare = task.wrapInside(this._prepare)
163
+ this.transfer = task.wrapInside(this.transfer)
164
+ if (this._healthCheckSr !== undefined) {
165
+ this.cleanup = task.wrapInside(this.cleanup)
166
+ this.healthCheck = task.wrap(this.healthCheck)
167
+ } else {
168
+ this.cleanup = task.wrap(this.cleanup)
169
+ }
156
170
 
157
- return task.run(() => this._prepare(isFull))
171
+ return this._prepare(isFull)
158
172
  }
159
173
 
160
174
  async _prepare(isFull) {
@@ -255,7 +269,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
255
269
  const { uuid: srUuid, $xapi: xapi } = sr
256
270
 
257
271
  let targetVmRef
258
- await Task.run({ name: 'transfer' }, async () => {
272
+ await Task.run({ properties: { name: 'transfer' } }, async () => {
259
273
  targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport, timestamp), sr, {
260
274
  targetRef: this._targetVmRef,
261
275
  })
@@ -279,7 +293,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
279
293
  ` -- last replication: ${formatFilenameDate(timestamp)} ${humanFormat.bytes(size)} read`
280
294
  )
281
295
  // take a snapshot to ensure these data are not modified until next snapshot
282
- await Task.run({ name: 'target snapshot' }, async () => {
296
+ await Task.run({ properties: { name: 'target snapshot' } }, async () => {
283
297
  await xapi.VM_snapshot(targetVmRef, {
284
298
  name_label: `${vm.name_label} - ${job.name} / ${schedule.name} ${formatFilenameDate(timestamp)}`,
285
299
  })
@@ -1,12 +1,12 @@
1
1
  import { createLogger } from '@xen-orchestra/log'
2
2
  import { join } from 'node:path'
3
+ import { Task } from '@vates/task'
3
4
  import assert from 'node:assert'
4
5
 
5
6
  import { formatFilenameDate } from '../../_filenameDate.mjs'
6
7
  import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
7
8
  import { HealthCheckVmBackup } from '../../HealthCheckVmBackup.mjs'
8
9
  import { ImportVmBackup } from '../../ImportVmBackup.mjs'
9
- import { Task } from '../../Task.mjs'
10
10
  import * as MergeWorker from '../../merge-worker/index.mjs'
11
11
  import ms from 'ms'
12
12
  import { getEntryStatus } from '../../_getOldEntries.mjs'
@@ -28,7 +28,7 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
28
28
 
29
29
  async _cleanVm(options) {
30
30
  try {
31
- return await Task.run({ name: 'clean-vm' }, () => {
31
+ return await Task.run({ properties: { name: 'clean-vm' } }, () => {
32
32
  return this._adapter.cleanVm(this._vmBackupDir, {
33
33
  ...options,
34
34
  fixMetadata: true,
@@ -100,7 +100,7 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
100
100
  }
101
101
  return Task.run(
102
102
  {
103
- name: 'health check',
103
+ properties: { name: 'health check' },
104
104
  },
105
105
  async () => {
106
106
  const xapi = sr.$xapi
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict'
2
+ import { Task } from '@vates/task'
2
3
 
3
4
  import { HealthCheckVmBackup } from '../../HealthCheckVmBackup.mjs'
4
- import { Task } from '../../Task.mjs'
5
5
  import ms from 'ms'
6
6
 
7
7
  export const MixinXapiWriter = (BaseClass = Object) =>
@@ -33,7 +33,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
33
33
  // copy VM
34
34
  return Task.run(
35
35
  {
36
- name: 'health check',
36
+ properties: { name: 'health check' },
37
37
  },
38
38
  async () => {
39
39
  const { $xapi: xapi } = sr
@@ -47,12 +47,12 @@ export const MixinXapiWriter = (BaseClass = Object) =>
47
47
  }
48
48
  if (await this.#isAlreadyOnHealthCheckSr(baseVm)) {
49
49
  healthCheckVmRef = await Task.run(
50
- { name: 'cloning-vm' },
50
+ { properties: { name: 'cloning-vm' } },
51
51
  async () => await xapi.callAsync('VM.clone', this._targetVmRef, `Health Check - ${baseVm.name_label}`)
52
52
  )
53
53
  } else {
54
54
  healthCheckVmRef = await Task.run(
55
- { name: 'copying-vm' },
55
+ { properties: { name: 'copying-vm' } },
56
56
  async () =>
57
57
  await xapi.callAsync(
58
58
  'VM.copy',
@@ -10,6 +10,7 @@ import { createLogger } from '@xen-orchestra/log'
10
10
 
11
11
  import { basename, dirname } from 'path'
12
12
  import { asyncEach } from '@vates/async-each'
13
+ import { relativeFromFile } from '@xen-orchestra/fs/path'
13
14
 
14
15
  // @ts-ignore
15
16
  const { warn } = createLogger('remote-disk:merge')
@@ -18,6 +19,7 @@ const { warn } = createLogger('remote-disk:merge')
18
19
  * @typedef {Object} MergeState
19
20
  * @property {{ uuid: string }} child
20
21
  * @property {{ uuid: string }} parent
22
+ * @property { string[] | undefined} chain
21
23
  * @property {number} currentBlock
22
24
  * @property {number} mergedDataSize
23
25
  * @property {'mergeBlocks' | 'cleanup'} step
@@ -32,6 +34,7 @@ export class MergeRemoteDisk {
32
34
  #state = {
33
35
  child: { uuid: '0' },
34
36
  parent: { uuid: '0' },
37
+ chain: undefined,
35
38
  currentBlock: 0,
36
39
  mergedDataSize: 0,
37
40
  step: 'mergeBlocks',
@@ -205,6 +208,9 @@ export class MergeRemoteDisk {
205
208
  } else {
206
209
  this.#state.child = { uuid: childDisk.getUuid() ?? undefined }
207
210
  this.#state.parent = { uuid: parentDisk.getUuid() ?? undefined }
211
+ this.#state.chain = [parentDisk.getPath(), ...childDisk.getPaths()].map(path =>
212
+ relativeFromFile(this.#statePath, path)
213
+ )
208
214
 
209
215
  // Finds first allocated block for the 2 following loops
210
216
  while (this.#state.currentBlock < getMaxBlockCount && !childDisk.hasBlock(this.#state.currentBlock)) {
@@ -55,6 +55,16 @@ export class RemoteDisk extends RandomAccessDisk {
55
55
  throw new Error(`getPath must be implemented`)
56
56
  }
57
57
 
58
+ /**
59
+ * Abstract
60
+ * Returns an array of disk paths.
61
+ *
62
+ * @returns {string[]}
63
+ */
64
+ getPaths() {
65
+ throw new Error(`getPaths must be implemented`)
66
+ }
67
+
58
68
  /**
59
69
  * Abstract
60
70
  * @returns {string}
@@ -16,6 +16,7 @@ import { DISK_TYPES } from 'vhd-lib/_constants.js'
16
16
  import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
17
17
  import { stringify } from 'uuid'
18
18
  import { dirname, join } from 'node:path'
19
+ import { RemoteVhdDiskChain } from './RemoteVhdDiskChain.mjs'
19
20
 
20
21
  export class RemoteVhdDisk extends RemoteDisk {
21
22
  /**
@@ -140,6 +141,15 @@ export class RemoteVhdDisk extends RemoteDisk {
140
141
  return this.#path
141
142
  }
142
143
 
144
+ /**
145
+ * Returns the disk path in an array.
146
+ *
147
+ * @returns {string[]}
148
+ */
149
+ getPaths() {
150
+ return [this.getPath()]
151
+ }
152
+
143
153
  /**
144
154
  * @returns {string}
145
155
  */
@@ -259,7 +269,11 @@ export class RemoteVhdDisk extends RemoteDisk {
259
269
  * @returns {Promise<number>} blockSize
260
270
  */
261
271
  async mergeBlock(childDisk, index, isResumingMerge) {
262
- if ((await this.isDirectory()) && childDisk instanceof RemoteVhdDisk && (await childDisk.isDirectory())) {
272
+ if (
273
+ (childDisk instanceof RemoteVhdDisk || childDisk instanceof RemoteVhdDiskChain) &&
274
+ (await this.isDirectory()) &&
275
+ (await childDisk.isDirectory())
276
+ ) {
263
277
  try {
264
278
  await this.#handler.rename(childDisk.getBlockPath(index), this.getBlockPath(index))
265
279
 
@@ -123,6 +123,15 @@ export class RemoteVhdDiskChain extends RemoteDisk {
123
123
  return this.#disks[this.#disks.length - 1].getPath()
124
124
  }
125
125
 
126
+ /**
127
+ * Disk chains return an array of disk paths.
128
+ *
129
+ * @returns {string[]}
130
+ */
131
+ getPaths() {
132
+ return this.#disks.map(disk => disk.getPath())
133
+ }
134
+
126
135
  /**
127
136
  * @returns {string}
128
137
  */
@@ -134,12 +143,7 @@ export class RemoteVhdDiskChain extends RemoteDisk {
134
143
  * @returns {Promise<boolean>} canMergeConcurently
135
144
  */
136
145
  async canMergeConcurently() {
137
- for (const disk of this.#disks) {
138
- if (!(await disk.isDirectory())) {
139
- return true
140
- }
141
- }
142
- return false
146
+ return this.isDirectory()
143
147
  }
144
148
 
145
149
  /**
@@ -221,6 +225,21 @@ export class RemoteVhdDiskChain extends RemoteDisk {
221
225
  throw new Error(`Can't merge block into a disk chain`)
222
226
  }
223
227
 
228
+ /**
229
+ * Gets a specific block path from the VHD directory disk.
230
+ * @param {number} index
231
+ * @returns {string} blockPath
232
+ */
233
+ getBlockPath(index) {
234
+ for (const disk of [...this.#disks].reverse()) {
235
+ if (disk.hasBlock(index)) {
236
+ return disk.getBlockPath(index)
237
+ }
238
+ }
239
+
240
+ throw new Error(`Block ${index} not found in chain`)
241
+ }
242
+
224
243
  /**
225
244
  * @returns {VhdFooter}
226
245
  */
@@ -269,4 +288,17 @@ export class RemoteVhdDiskChain extends RemoteDisk {
269
288
  await disk.unlink()
270
289
  }
271
290
  }
291
+
292
+ /**
293
+ * Check if all the disks in the chain are VHD directories.
294
+ * @returns {Promise<boolean>}
295
+ */
296
+ async isDirectory() {
297
+ for (const disk of this.#disks) {
298
+ if (!(await disk.isDirectory())) {
299
+ return false
300
+ }
301
+ }
302
+ return true
303
+ }
272
304
  }
package/package.json CHANGED
@@ -8,32 +8,34 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/vatesfr/xen-orchestra.git"
10
10
  },
11
- "version": "0.71.2",
11
+ "version": "0.72.0",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
15
15
  "scripts": {
16
16
  "postversion": "npm publish --access public",
17
+ "test": "node --test",
17
18
  "test-integration": "node --test *.integ.mjs"
18
19
  },
19
20
  "dependencies": {
20
21
  "@iarna/toml": "^2.2.5",
21
22
  "@kldzj/stream-throttle": "^1.1.1",
22
- "@vates/async-each": "^1.0.2",
23
+ "@vates/async-each": "^1.0.3",
23
24
  "@vates/cached-dns.lookup": "^1.0.0",
24
25
  "@vates/compose": "^2.1.0",
25
26
  "@vates/decorate-with": "^2.1.0",
26
27
  "@vates/disposable": "^0.1.6",
27
28
  "@vates/fuse-vhd": "^2.1.2",
28
- "@vates/generator-toolbox": "^1.1.1",
29
- "@vates/nbd-client": "^3.3.0",
29
+ "@vates/generator-toolbox": "^1.1.2",
30
+ "@vates/nbd-client": "^3.4.0",
30
31
  "@vates/parse-duration": "^0.1.1",
31
- "@xen-orchestra/async-map": "^0.1.2",
32
- "@xen-orchestra/disk-transform": "^1.2.2",
33
- "@xen-orchestra/fs": "^4.7.0",
34
- "@xen-orchestra/log": "^0.7.1",
35
- "@xen-orchestra/qcow2": "^1.2.0",
36
- "@xen-orchestra/template": "^0.1.0",
32
+ "@vates/task": "^0.7.0",
33
+ "@xen-orchestra/async-map": "^0.1.3",
34
+ "@xen-orchestra/disk-transform": "^1.2.3",
35
+ "@xen-orchestra/fs": "^4.8.0",
36
+ "@xen-orchestra/log": "^0.7.2",
37
+ "@xen-orchestra/qcow2": "^1.3.0",
38
+ "@xen-orchestra/template": "^0.1.1",
37
39
  "app-conf": "^3.0.0",
38
40
  "compare-versions": "^6.0.0",
39
41
  "d3-time-format": "^4.1.0",
@@ -41,7 +43,7 @@
41
43
  "golike-defer": "^0.5.1",
42
44
  "human-format": "^1.2.0",
43
45
  "limit-concurrency-decorator": "^0.6.0",
44
- "lodash": "^4.17.20",
46
+ "lodash": "^4.18.0",
45
47
  "moment-timezone": "^0.5.46",
46
48
  "ms": "^2.1.3",
47
49
  "node-zone": "^0.4.0",
@@ -51,8 +53,8 @@
51
53
  "tar": "^7.5.3",
52
54
  "uuid": "^9.0.0",
53
55
  "value-matcher": "^0.2.0",
54
- "vhd-lib": "^4.15.0",
55
- "xen-api": "^4.7.6",
56
+ "vhd-lib": "^4.16.0",
57
+ "xen-api": "^4.7.7",
56
58
  "yazl": "^2.5.1"
57
59
  },
58
60
  "devDependencies": {
@@ -62,7 +64,7 @@
62
64
  "tmp": "^0.2.1"
63
65
  },
64
66
  "peerDependencies": {
65
- "@xen-orchestra/xapi": "^8.7.0"
67
+ "@xen-orchestra/xapi": "^8.7.2"
66
68
  },
67
69
  "license": "AGPL-3.0-or-later",
68
70
  "author": {
package/Task.mjs DELETED
@@ -1,155 +0,0 @@
1
- import CancelToken from 'promise-toolbox/CancelToken'
2
- import Zone from 'node-zone'
3
-
4
- const logAfterEnd = log => {
5
- const error = new Error('task has already ended')
6
- error.log = log
7
- throw error
8
- }
9
-
10
- const noop = Function.prototype
11
-
12
- const serializeErrors = errors => (Array.isArray(errors) ? errors.map(serializeError) : errors)
13
-
14
- // Create a serializable object from an error.
15
- //
16
- // Otherwise some fields might be non-enumerable and missing from logs.
17
- const serializeError = error =>
18
- error instanceof Error
19
- ? {
20
- ...error, // Copy enumerable properties.
21
- code: error.code,
22
- errors: serializeErrors(error.errors), // supports AggregateError
23
- message: error.message,
24
- name: error.name,
25
- stack: error.stack,
26
- }
27
- : error
28
-
29
- const $$task = Symbol('@xen-orchestra/backups/Task')
30
-
31
- export class Task {
32
- static get cancelToken() {
33
- const task = Zone.current.data[$$task]
34
- return task !== undefined ? task.#cancelToken : CancelToken.none
35
- }
36
-
37
- static run(opts, fn) {
38
- return new this(opts).run(fn, true)
39
- }
40
-
41
- static wrapFn(opts, fn) {
42
- // compatibility with @decorateWith
43
- if (typeof fn !== 'function') {
44
- ;[fn, opts] = [opts, fn]
45
- }
46
-
47
- return function () {
48
- return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
49
- }
50
- }
51
-
52
- #cancelToken
53
- #id = Math.random().toString(36).slice(2)
54
- #onLog
55
- #zone
56
-
57
- constructor({ name, data, onLog }) {
58
- let parentCancelToken, parentId
59
- if (onLog === undefined) {
60
- const parent = Zone.current.data[$$task]
61
- if (parent === undefined) {
62
- onLog = noop
63
- } else {
64
- onLog = log => parent.#onLog(log)
65
- parentCancelToken = parent.#cancelToken
66
- parentId = parent.#id
67
- }
68
- }
69
-
70
- const zone = Zone.current.fork('@xen-orchestra/backups/Task')
71
- zone.data[$$task] = this
72
- this.#zone = zone
73
-
74
- const { cancel, token } = CancelToken.source(parentCancelToken && [parentCancelToken])
75
- this.#cancelToken = token
76
- this.cancel = cancel
77
-
78
- this.#onLog = onLog
79
-
80
- this.#log('start', {
81
- data,
82
- message: name,
83
- parentId,
84
- })
85
- }
86
-
87
- failure(error) {
88
- this.#end('failure', serializeError(error))
89
- }
90
-
91
- info(message, data) {
92
- this.#log('info', { data, message })
93
- }
94
-
95
- /**
96
- * Run a function in the context of this task
97
- *
98
- * In case of error, the task will be failed.
99
- *
100
- * @typedef Result
101
- * @param {() => Result} fn
102
- * @param {boolean} last - Whether the task should succeed if there is no error
103
- * @returns Result
104
- */
105
- run(fn, last = false) {
106
- return this.#zone.run(() => {
107
- try {
108
- const result = fn()
109
- let then
110
- if (result != null && typeof (then = result.then) === 'function') {
111
- then.call(result, last && (value => this.success(value)), error => this.failure(error))
112
- } else if (last) {
113
- this.success(result)
114
- }
115
- return result
116
- } catch (error) {
117
- this.failure(error)
118
- throw error
119
- }
120
- })
121
- }
122
-
123
- success(value) {
124
- this.#end('success', value)
125
- }
126
-
127
- warning(message, data) {
128
- this.#log('warning', { data, message })
129
- }
130
-
131
- wrapFn(fn, last) {
132
- const task = this
133
- return function () {
134
- return task.run(() => fn.apply(this, arguments), last)
135
- }
136
- }
137
-
138
- #end(status, result) {
139
- this.#log('end', { result, status })
140
- this.#onLog = logAfterEnd
141
- }
142
-
143
- #log(event, props) {
144
- this.#onLog({
145
- ...props,
146
- event,
147
- taskId: this.#id,
148
- timestamp: Date.now(),
149
- })
150
- }
151
- }
152
-
153
- for (const method of ['info', 'warning']) {
154
- Task[method] = (...args) => Zone.current.data[$$task]?.[method](...args)
155
- }
@@ -1,5 +0,0 @@
1
- import { Task } from '../Task.mjs'
2
-
3
- const noop = Function.prototype
4
-
5
- export const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs