@xen-orchestra/backups 0.44.7 → 0.45.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.
@@ -6,11 +6,12 @@ import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
6
6
  import { Task } from '../Task.mjs'
7
7
  import createStreamThrottle from './_createStreamThrottle.mjs'
8
8
  import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
9
- import { runTask } from './_runTask.mjs'
10
9
  import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
11
10
  import { FullRemote } from './_vmRunners/FullRemote.mjs'
12
11
  import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
13
12
 
13
+ const noop = Function.prototype
14
+
14
15
  const DEFAULT_REMOTE_VM_SETTINGS = {
15
16
  concurrency: 2,
16
17
  copyRetention: 0,
@@ -20,6 +21,7 @@ const DEFAULT_REMOTE_VM_SETTINGS = {
20
21
  healthCheckVmsWithTags: [],
21
22
  maxExportRate: 0,
22
23
  maxMergedDeltasPerRun: Infinity,
24
+ nRetriesVmBackupFailures: 0,
23
25
  timeout: 0,
24
26
  validateVhdStreams: false,
25
27
  vmTimeout: 0,
@@ -41,6 +43,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
41
43
  const throttleStream = createStreamThrottle(settings.maxExportRate)
42
44
 
43
45
  const config = this._config
46
+
44
47
  await Disposable.use(
45
48
  () => this._getAdapter(job.sourceRemote),
46
49
  () => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
@@ -62,8 +65,19 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
62
65
  const allSettings = this._job.settings
63
66
  const baseSettings = this._baseSettings
64
67
 
68
+ const queue = new Set(vmsUuids)
69
+ const taskByVmId = {}
70
+ const nTriesByVmId = {}
71
+
65
72
  const handleVm = vmUuid => {
73
+ if (nTriesByVmId[vmUuid] === undefined) {
74
+ nTriesByVmId[vmUuid] = 0
75
+ }
76
+ nTriesByVmId[vmUuid]++
77
+
66
78
  const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
79
+ const vmSettings = { ...settings, ...allSettings[vmUuid] }
80
+ const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
67
81
 
68
82
  const opts = {
69
83
  baseSettings,
@@ -72,7 +86,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
72
86
  healthCheckSr,
73
87
  remoteAdapters,
74
88
  schedule,
75
- settings: { ...settings, ...allSettings[vmUuid] },
89
+ settings: vmSettings,
76
90
  sourceRemoteAdapter,
77
91
  throttleStream,
78
92
  vmUuid,
@@ -90,11 +104,42 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
90
104
  .listVmBackups(vmUuid, ({ mode }) => mode === job.mode)
91
105
  .then(vmBackups => {
92
106
  // avoiding to create tasks for empty directories
93
- if (vmBackups.length > 0) return runTask(taskStart, () => vmBackup.run())
107
+ if (vmBackups.length > 0) {
108
+ if (taskByVmId[vmUuid] === undefined) {
109
+ taskByVmId[vmUuid] = new Task(taskStart)
110
+ }
111
+ const task = taskByVmId[vmUuid]
112
+ return task
113
+ .run(async () => {
114
+ try {
115
+ const result = await vmBackup.run()
116
+ task.success(result)
117
+ return result
118
+ } catch (error) {
119
+ if (isLastRun) {
120
+ throw error
121
+ } else {
122
+ Task.warning(`Retry the VM mirror backup due to an error`, {
123
+ attempt: nTriesByVmId[vmUuid],
124
+ error: error.message,
125
+ })
126
+ queue.add(vmUuid)
127
+ }
128
+ }
129
+ })
130
+ .catch(noop)
131
+ }
94
132
  })
95
133
  }
96
134
  const { concurrency } = settings
97
- await asyncMapSettled(vmsUuids, !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm))
135
+ const _handleVm = !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm)
136
+
137
+ while (queue.size > 0) {
138
+ const vmIds = Array.from(queue)
139
+ queue.clear()
140
+
141
+ await asyncMapSettled(vmIds, _handleVm)
142
+ }
98
143
  }
99
144
  )
100
145
  }
@@ -11,6 +11,8 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
11
11
  import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
12
12
  import { FullXapi } from './_vmRunners/FullXapi.mjs'
13
13
 
14
+ const noop = Function.prototype
15
+
14
16
  const DEFAULT_XAPI_VM_SETTINGS = {
15
17
  bypassVdiChainsCheck: false,
16
18
  checkpointSnapshot: false,
@@ -24,6 +26,7 @@ const DEFAULT_XAPI_VM_SETTINGS = {
24
26
  healthCheckVmsWithTags: [],
25
27
  maxExportRate: 0,
26
28
  maxMergedDeltasPerRun: Infinity,
29
+ nRetriesVmBackupFailures: 0,
27
30
  offlineBackup: false,
28
31
  offlineSnapshot: false,
29
32
  snapshotRetention: 0,
@@ -53,6 +56,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
53
56
  const throttleStream = createStreamThrottle(settings.maxExportRate)
54
57
 
55
58
  const config = this._config
59
+
56
60
  await Disposable.use(
57
61
  Disposable.all(
58
62
  extractIdsFromSimplePattern(job.srs).map(id =>
@@ -89,48 +93,98 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
89
93
  const allSettings = this._job.settings
90
94
  const baseSettings = this._baseSettings
91
95
 
96
+ const queue = new Set(vmIds)
97
+ const taskByVmId = {}
98
+ const nTriesByVmId = {}
99
+
92
100
  const handleVm = vmUuid => {
101
+ const getVmTask = () => {
102
+ if (taskByVmId[vmUuid] === undefined) {
103
+ taskByVmId[vmUuid] = new Task(taskStart)
104
+ }
105
+ return taskByVmId[vmUuid]
106
+ }
107
+ const vmBackupFailed = error => {
108
+ if (isLastRun) {
109
+ throw error
110
+ } else {
111
+ Task.warning(`Retry the VM backup due to an error`, {
112
+ attempt: nTriesByVmId[vmUuid],
113
+ error: error.message,
114
+ })
115
+ queue.add(vmUuid)
116
+ }
117
+ }
118
+
119
+ if (nTriesByVmId[vmUuid] === undefined) {
120
+ nTriesByVmId[vmUuid] = 0
121
+ }
122
+ nTriesByVmId[vmUuid]++
123
+
124
+ const vmSettings = { ...settings, ...allSettings[vmUuid] }
93
125
  const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
126
+ const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
94
127
 
95
128
  return this._getRecord('VM', vmUuid).then(
96
129
  disposableVm =>
97
- Disposable.use(disposableVm, vm => {
98
- taskStart.data.name_label = vm.name_label
99
- return runTask(taskStart, () => {
100
- const opts = {
101
- baseSettings,
102
- config,
103
- getSnapshotNameLabel,
104
- healthCheckSr,
105
- job,
106
- remoteAdapters,
107
- schedule,
108
- settings: { ...settings, ...allSettings[vm.uuid] },
109
- srs,
110
- throttleStream,
111
- vm,
112
- }
113
- let vmBackup
114
- if (job.mode === 'delta') {
115
- vmBackup = new IncrementalXapi(opts)
116
- } else {
117
- if (job.mode === 'full') {
118
- vmBackup = new FullXapi(opts)
130
+ Disposable.use(disposableVm, async vm => {
131
+ if (taskStart.data.name_label === undefined) {
132
+ taskStart.data.name_label = vm.name_label
133
+ }
134
+
135
+ const task = getVmTask()
136
+ return task
137
+ .run(async () => {
138
+ const opts = {
139
+ baseSettings,
140
+ config,
141
+ getSnapshotNameLabel,
142
+ healthCheckSr,
143
+ job,
144
+ remoteAdapters,
145
+ schedule,
146
+ settings: vmSettings,
147
+ srs,
148
+ throttleStream,
149
+ vm,
150
+ }
151
+
152
+ let vmBackup
153
+ if (job.mode === 'delta') {
154
+ vmBackup = new IncrementalXapi(opts)
119
155
  } else {
120
- throw new Error(`Job mode ${job.mode} not implemented`)
156
+ if (job.mode === 'full') {
157
+ vmBackup = new FullXapi(opts)
158
+ } else {
159
+ throw new Error(`Job mode ${job.mode} not implemented`)
160
+ }
161
+ }
162
+
163
+ try {
164
+ const result = await vmBackup.run()
165
+ task.success(result)
166
+ return result
167
+ } catch (error) {
168
+ vmBackupFailed(error)
121
169
  }
122
- }
123
- return vmBackup.run()
124
- })
170
+ })
171
+ .catch(noop) // errors are handled by logs
125
172
  }),
126
173
  error =>
127
- runTask(taskStart, () => {
128
- throw error
174
+ getVmTask().run(() => {
175
+ vmBackupFailed(error)
129
176
  })
130
177
  )
131
178
  }
132
179
  const { concurrency } = settings
133
- await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
180
+ const _handleVm = concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm)
181
+
182
+ while (queue.size > 0) {
183
+ const vmIds = Array.from(queue)
184
+ queue.clear()
185
+
186
+ await asyncMapSettled(vmIds, _handleVm)
187
+ }
134
188
  }
135
189
  )
136
190
  }
@@ -22,6 +22,7 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
22
22
  this.config = config
23
23
  this.job = job
24
24
  this.remoteAdapters = remoteAdapters
25
+ this._settings = settings
25
26
  this.scheduleId = schedule.id
26
27
  this.timestamp = undefined
27
28
 
@@ -77,11 +77,11 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
77
77
  healthCheck() {
78
78
  const sr = this._healthCheckSr
79
79
  assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
80
- assert.notStrictEqual(
81
- this._metadataFileName,
82
- undefined,
83
- 'Metadata file name should be defined before making a health check'
84
- )
80
+ if (this._metadataFileName === undefined) {
81
+ // this can happen when making a mirror backup with nothing to transfer
82
+ Task.info('no health check, since no backup have been transferred')
83
+ return
84
+ }
85
85
  return Task.run(
86
86
  {
87
87
  name: 'health check',
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/vatesfr/xen-orchestra.git"
10
10
  },
11
- "version": "0.44.7",
11
+ "version": "0.45.0",
12
12
  "engines": {
13
13
  "node": ">=14.18"
14
14
  },
@@ -56,7 +56,7 @@
56
56
  "tmp": "^0.2.1"
57
57
  },
58
58
  "peerDependencies": {
59
- "@xen-orchestra/xapi": "^4.2.1"
59
+ "@xen-orchestra/xapi": "^4.3.0"
60
60
  },
61
61
  "license": "AGPL-3.0-or-later",
62
62
  "author": {