@xen-orchestra/backups 0.72.0 → 0.73.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,325 +0,0 @@
1
- // @ts-check
2
-
3
- /**
4
- * @typedef {import('./RemoteDisk.mjs').RemoteDisk} RemoteDisk
5
- * @typedef {import('@xen-orchestra/disk-transform').FileAccessor} FileAccessor
6
- */
7
-
8
- import assert from 'assert'
9
- import { createLogger } from '@xen-orchestra/log'
10
-
11
- import { basename, dirname } from 'path'
12
- import { asyncEach } from '@vates/async-each'
13
- import { relativeFromFile } from '@xen-orchestra/fs/path'
14
-
15
- // @ts-ignore
16
- const { warn } = createLogger('remote-disk:merge')
17
-
18
- /**
19
- * @typedef {Object} MergeState
20
- * @property {{ uuid: string }} child
21
- * @property {{ uuid: string }} parent
22
- * @property { string[] | undefined} chain
23
- * @property {number} currentBlock
24
- * @property {number} mergedDataSize
25
- * @property {'mergeBlocks' | 'cleanup'} step
26
- * @property {number} diskSize
27
- * @typedef {(message: string, data?: Record<string, unknown>) => void} Logger
28
- */
29
-
30
- export class MergeRemoteDisk {
31
- /**
32
- * @type {MergeState}
33
- */
34
- #state = {
35
- child: { uuid: '0' },
36
- parent: { uuid: '0' },
37
- chain: undefined,
38
- currentBlock: 0,
39
- mergedDataSize: 0,
40
- step: 'mergeBlocks',
41
- diskSize: 0,
42
- }
43
-
44
- /**
45
- * @type {string}
46
- */
47
- #statePath = ''
48
-
49
- /**
50
- * @type {boolean}
51
- */
52
- #isResuming
53
-
54
- /**
55
- * @type {Logger | Function}
56
- */
57
- #logInfo
58
-
59
- /**
60
- * @type {number}
61
- */
62
- #mergeBlockConcurrency
63
-
64
- /**
65
- * @type {Function}
66
- */
67
- #onProgress
68
-
69
- /**
70
- * @type {boolean}
71
- */
72
- #removeUnused
73
-
74
- /**
75
- * @type {number}
76
- */
77
- #writeStateDelay
78
-
79
- /**
80
- * @type {number}
81
- */
82
- #lastStateWrittenAt
83
-
84
- /**
85
- * @type {FileAccessor}
86
- */
87
- #handler
88
-
89
- /**
90
- * @param {FileAccessor} handler
91
- * @param {Object} params
92
- * @param {Function} [params.onProgress]
93
- * @param {Logger | Function} [params.logInfo]
94
- * @param {boolean} [params.removeUnused]
95
- * @param {number} [params.mergeBlockConcurrency]
96
- * @param {number} [params.writeStateDelay]
97
- */
98
- constructor(
99
- handler,
100
- {
101
- onProgress = () => {},
102
- logInfo = () => {},
103
- removeUnused = false,
104
- mergeBlockConcurrency = 2,
105
- writeStateDelay = 10e3,
106
- }
107
- ) {
108
- this.#handler = handler
109
- this.#logInfo = logInfo
110
- this.#onProgress = onProgress
111
- this.#removeUnused = removeUnused
112
- this.#mergeBlockConcurrency = mergeBlockConcurrency
113
- this.#writeStateDelay = writeStateDelay
114
-
115
- this.#isResuming = false
116
- this.#lastStateWrittenAt = 0
117
- }
118
-
119
- /**
120
- * @param {RemoteDisk} parentDisk
121
- * @returns {Promise<boolean>} isResuming
122
- */
123
- async isResuming(parentDisk) {
124
- try {
125
- await this.#handler.readFile(
126
- dirname(parentDisk.getPath()) + '/.' + basename(parentDisk.getPath()) + '.merge.json'
127
- )
128
- return true
129
- } catch (error) {
130
- return false
131
- }
132
- }
133
-
134
- /**
135
- * @param {RemoteDisk} parentDisk
136
- * @param {RemoteDisk} childDisk
137
- */
138
- async merge(parentDisk, childDisk) {
139
- this.#statePath = dirname(parentDisk.getPath()) + '/.' + basename(parentDisk.getPath()) + '.merge.json'
140
-
141
- try {
142
- const mergeStateContent = await this.#handler.readFile(this.#statePath)
143
- this.#state = JSON.parse(mergeStateContent)
144
-
145
- // work-around a bug introduce in 97d94b795
146
- //
147
- // currentBlock could be `null` due to the JSON.stringify of a `NaN` value
148
- if (this.#state?.currentBlock === null) this.#state.currentBlock = 0
149
- this.#isResuming = true
150
- } catch (error) {
151
- // @ts-ignore
152
- if (error.code !== 'ENOENT') {
153
- warn('problem while checking the merge state', { error })
154
- }
155
- }
156
-
157
- try {
158
- /* eslint-disable no-fallthrough */
159
- switch (this.#state?.step ?? 'mergeBlocks') {
160
- case 'mergeBlocks':
161
- await this.#step_mergeBlocks(parentDisk, childDisk)
162
- case 'cleanup':
163
- await this.#step_cleanup(parentDisk, childDisk)
164
- return this.#cleanup(parentDisk, childDisk, true)
165
- default:
166
- warn(`Step ${this.#state?.step} is unknown`, { state: this.#state })
167
- }
168
- /* eslint-enable no-fallthrough */
169
- } catch (error) {
170
- await this.#cleanup(parentDisk, childDisk, false)
171
- throw error
172
- }
173
- }
174
-
175
- async #writeState() {
176
- try {
177
- await this.#handler.writeFile(this.#statePath, JSON.stringify(this.#state), { flags: 'w' })
178
- } catch (err) {
179
- warn('failed to write merge state', { error: err })
180
- }
181
- }
182
-
183
- async #writeStateThrottled() {
184
- const now = Date.now()
185
- if (now - this.#lastStateWrittenAt > this.#writeStateDelay) {
186
- this.#lastStateWrittenAt = now
187
- await this.#writeState()
188
- }
189
- }
190
-
191
- /**
192
- * @param {RemoteDisk} parentDisk
193
- * @param {RemoteDisk} childDisk
194
- */
195
- async #step_mergeBlocks(parentDisk, childDisk) {
196
- const getMaxBlockCount = childDisk.getMaxBlockCount()
197
- await parentDisk.resize(getMaxBlockCount)
198
-
199
- if (this.#isResuming) {
200
- const alreadyMergedBlocks = []
201
- for (let blockId = 0; blockId < this.#state.currentBlock; blockId++) {
202
- if (childDisk.hasBlock(blockId)) {
203
- alreadyMergedBlocks.push(blockId)
204
- }
205
- }
206
-
207
- parentDisk.setAllocatedBlocks(alreadyMergedBlocks)
208
- } else {
209
- this.#state.child = { uuid: childDisk.getUuid() ?? undefined }
210
- this.#state.parent = { uuid: parentDisk.getUuid() ?? undefined }
211
- this.#state.chain = [parentDisk.getPath(), ...childDisk.getPaths()].map(path =>
212
- relativeFromFile(this.#statePath, path)
213
- )
214
-
215
- // Finds first allocated block for the 2 following loops
216
- while (this.#state.currentBlock < getMaxBlockCount && !childDisk.hasBlock(this.#state.currentBlock)) {
217
- ++this.#state.currentBlock
218
- }
219
- await this.#writeState()
220
- }
221
-
222
- await this.#mergeBlocks(parentDisk, childDisk)
223
- await parentDisk.flushMetadata(childDisk)
224
- await parentDisk.mergeMetadata(childDisk)
225
- }
226
-
227
- /**
228
- * @param {RemoteDisk} parentDisk
229
- * @param {RemoteDisk} childDisk
230
- */
231
- async #mergeBlocks(parentDisk, childDisk) {
232
- this.#mergeBlockConcurrency =
233
- (await parentDisk.canMergeConcurently()) && (await childDisk.canMergeConcurently())
234
- ? this.#mergeBlockConcurrency
235
- : 1
236
-
237
- const toMerge = []
238
- for (let block = this.#state.currentBlock; block < childDisk.getMaxBlockCount(); block++) {
239
- if (childDisk.hasBlock(block)) {
240
- toMerge.push(block)
241
- }
242
- }
243
-
244
- const nBlocks = toMerge.length
245
- this.#onProgress({ total: nBlocks, done: 0 })
246
-
247
- const merging = new Set()
248
- let counter = 0
249
- await asyncEach(
250
- toMerge,
251
- async blockId => {
252
- merging.add(blockId)
253
-
254
- const blockSize = await parentDisk.mergeBlock(childDisk, blockId, this.#isResuming)
255
- this.#state.mergedDataSize += blockSize
256
-
257
- this.#state.currentBlock = Math.min(...merging) - 1
258
- merging.delete(blockId)
259
-
260
- this.#onProgress({ total: nBlocks, done: counter + 1 })
261
- counter++
262
- await this.#writeStateThrottled()
263
- },
264
- { concurrency: this.#mergeBlockConcurrency }
265
- )
266
-
267
- await this.#writeState()
268
-
269
- this.#state.diskSize = childDisk.getSizeOnDisk()
270
-
271
- this.#onProgress({ total: nBlocks, done: nBlocks })
272
- }
273
-
274
- /**
275
- * @param {RemoteDisk} parentDisk
276
- * @param {RemoteDisk} childDisk
277
- */
278
- async #step_cleanup(parentDisk, childDisk) {
279
- assert.notEqual(this.#state, undefined)
280
- this.#state.step = 'cleanup'
281
- await this.#writeState()
282
-
283
- const mergeTargetPath = childDisk.getPath()
284
-
285
- // delete intermediate children if needed
286
- if (this.#removeUnused) {
287
- await childDisk.unlink()
288
- }
289
-
290
- try {
291
- await parentDisk.rename(mergeTargetPath)
292
- } catch (error) {
293
- // @ts-ignore
294
- if (error.code === 'ENOENT' && this.#isResuming) {
295
- // @ts-ignore
296
- this.#logInfo(`the parent disk was already renamed`, {
297
- parent: parentDisk.getPath(),
298
- mergeTarget: mergeTargetPath,
299
- })
300
- } else {
301
- throw error
302
- }
303
- }
304
- }
305
-
306
- /**
307
- * @param {RemoteDisk} parentDisk
308
- * @param {RemoteDisk} childDisk
309
- * @param {boolean} cleanStateFile
310
- *
311
- * @returns {Promise<{mergedDataSize: number, finalDiskSize: number}>} result
312
- */
313
- async #cleanup(parentDisk, childDisk, cleanStateFile) {
314
- const finalDiskSize = this.#state?.diskSize ?? 0
315
- const mergedDataSize = this.#state?.mergedDataSize ?? 0
316
- await parentDisk.close().catch(warn)
317
- await childDisk.close().catch(warn)
318
-
319
- if (cleanStateFile) {
320
- await this.#handler.unlink(this.#statePath).catch(warn)
321
- }
322
-
323
- return { mergedDataSize, finalDiskSize }
324
- }
325
- }
@@ -1,223 +0,0 @@
1
- // @ts-check
2
-
3
- /**
4
- * @typedef {import('@xen-orchestra/disk-transform').DiskBlock} DiskBlock
5
- */
6
-
7
- import { RandomAccessDisk } from '@xen-orchestra/disk-transform'
8
- export class RemoteDisk extends RandomAccessDisk {
9
- /**
10
- * Abstract
11
- * @param {Object} options
12
- * @param {boolean} options.force
13
- * @returns {Promise<void>}
14
- */
15
- async init(options = { force: false }) {
16
- throw new Error(`init must be implemented`)
17
- }
18
-
19
- /**
20
- * Abstract
21
- * @returns {Promise<void>}
22
- */
23
- async close() {
24
- throw new Error(`close must be implemented`)
25
- }
26
-
27
- /**
28
- * Abstract
29
- * @returns {number}
30
- */
31
- getVirtualSize() {
32
- throw new Error(`getVirtualSize must be implemented`)
33
- }
34
-
35
- /**
36
- * Abstract
37
- * @returns {number} size
38
- */
39
- getSizeOnDisk() {
40
- throw new Error(`getSizeOnDisk must be implemented`)
41
- }
42
-
43
- /**
44
- * @returns {number}
45
- */
46
- getBlockSize() {
47
- throw new Error(`getBlockSize must be implemented`)
48
- }
49
-
50
- /**
51
- * Abstract
52
- * @returns {string}
53
- */
54
- getPath() {
55
- throw new Error(`getPath must be implemented`)
56
- }
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
-
68
- /**
69
- * Abstract
70
- * @returns {string}
71
- */
72
- getUuid() {
73
- throw new Error(`getUuid must be implemented`)
74
- }
75
-
76
- /**
77
- * Abstract
78
- * @returns {Promise<boolean>} canMergeConcurently
79
- */
80
- async canMergeConcurently() {
81
- throw new Error(`canMergeConcurently must be implemented`)
82
- }
83
-
84
- /**
85
- * @returns {number} getMaxBlockCount
86
- */
87
- getMaxBlockCount() {
88
- return Math.ceil(this.getVirtualSize() / this.getBlockSize())
89
- }
90
-
91
- /**
92
- * Checks if the VHD contains a specific block.
93
- * @param {number} index
94
- * @returns {boolean}
95
- */
96
- hasBlock(index) {
97
- throw new Error(`hasBlock must be implemented`)
98
- }
99
-
100
- /**
101
- * Abstract
102
- * Gets the indexes of all blocks in the VHD.
103
- * @returns {Array<number>}
104
- */
105
- getBlockIndexes() {
106
- throw new Error(`getBlockIndexes must be implemented`)
107
- }
108
-
109
- /**
110
- * Abstract
111
- * Returns the parent non inizialized instance
112
- * @returns {RemoteDisk}
113
- */
114
- instantiateParent() {
115
- throw new Error(`instantiateParent must be implemented`)
116
- }
117
-
118
- /**
119
- * Abstract
120
- * Writes a full block.
121
- * @param {DiskBlock} diskBlock
122
- * @return {Promise<number>} blockSize
123
- */
124
- async writeBlock(diskBlock) {
125
- throw new Error(`writeBlock must be implemented`)
126
- }
127
-
128
- /**
129
- * Abstract
130
- * Reads a specific block from the VHD.
131
- * @param {number} index
132
- * @returns {Promise<DiskBlock>} diskBlock
133
- */
134
- async readBlock(index) {
135
- throw new Error(`readBlock must be implemented`)
136
- }
137
-
138
- /**
139
- * Abstract
140
- * Reads a specific block from the child disk to copy/move it to this disk.
141
- * @param {RemoteDisk} childDisk
142
- * @param {number} index
143
- * @param {boolean} isResumingMerge
144
- * @returns {Promise<number>} blockSize
145
- */
146
- async mergeBlock(childDisk, index, isResumingMerge) {
147
- throw new Error(`mergeBlock must be implemented`)
148
- }
149
-
150
- /**
151
- * Abstract
152
- * Manually set the disk allocated blocks.
153
- * @param {Array<number>} blockIds
154
- * @returns {Promise<void>}
155
- */
156
- async setAllocatedBlocks(blockIds) {
157
- throw new Error(`setAllocatedBlocks must be implemented`)
158
- }
159
-
160
- /**
161
- * Abstract
162
- * @param {number} blockCount
163
- * @returns {Promise<void>}
164
- */
165
- async resize(blockCount) {
166
- throw new Error(`resize must be implemented`)
167
- }
168
-
169
- /**
170
- * Abstract
171
- * @param {RemoteDisk} childDisk
172
- * @returns {Promise<void>}
173
- */
174
- async flushMetadata(childDisk) {
175
- throw new Error(`flushMetadata must be implemented`)
176
- }
177
-
178
- /**
179
- * Abstract
180
- * @param {RemoteDisk} childDisk
181
- * @returns {Promise<void>}
182
- */
183
- mergeMetadata(childDisk) {
184
- throw new Error(`mergeMetadata must be implemented`)
185
- }
186
-
187
- /**
188
- * Abstract
189
- * Checks if the VHD is a differencing disk.
190
- * @returns {boolean}
191
- */
192
- isDifferencing() {
193
- throw new Error(`isDifferencing must be implemented`)
194
- }
195
-
196
- /**
197
- * Abstract
198
- * Rename alias/disk
199
- * @param {string} newPath
200
- */
201
- async rename(newPath) {
202
- throw new Error(`rename must be implemented`)
203
- }
204
-
205
- /**
206
- * Abstract
207
- * Deletes alias/disk/disks
208
- */
209
- async unlink() {
210
- throw new Error(`unlink must be implemented`)
211
- }
212
-
213
- /**
214
- * @returns {Promise<RemoteDisk>}
215
- */
216
- async openParent() {
217
- const parent = await super.openParent()
218
- if (!(parent instanceof RemoteDisk)) {
219
- throw new Error('parent of a RemoteDisk must be also a RemoteDisk')
220
- }
221
- return parent
222
- }
223
- }