-
Notifications
You must be signed in to change notification settings - Fork 62
feat: Add feature for copying backups #1153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 136 commits
46ce5b8
576550d
31a1d9a
90e290e
7bce08d
29a736f
edc30d3
c8a16fc
2d5320c
adabd71
89f742e
d4f5d47
ea43929
bb6e708
c96442a
701badc
448e41d
7407fa2
4eba7d3
9c21e29
c87a4d9
a5c1ce2
3ee1ec3
1d80cf4
081ddac
2315c78
0c07e7c
2330a56
aa5bab3
967bd71
0075152
c97b89f
fbdf7d4
8c685fd
38d7961
e4d2229
77d105f
a69c8c6
d3387d3
452eef0
d7c4e94
c142deb
fbe0b0c
11148fb
e055215
463666a
3318e84
c932b4c
db741f2
0b91bb6
307a8cb
ee33f43
a3a4d9c
e554f92
3a1cd27
3f594b2
4ffb7a1
8073653
e1f2dde
f66ee04
13e128d
b1af9c1
4c0310c
d61e1cc
e2a8be5
763115b
1827059
3259115
4534399
0836841
49f693f
67d5eb0
2ef42e6
86a0311
f163aba
a27f29f
80f0154
3b95726
ae0e313
df282b4
99e3d8c
3dddc0b
5a0d07b
e4d38d4
6a26bde
e7e1eb3
d859a33
b2c728f
377d5bc
1520f22
74cbffa
26289af
c5ba924
ffb9588
1fe2ef0
9c79b43
7ab1c96
842b70b
3872ff0
02078e5
5ac03be
4bd4777
7ee2f18
1a0e153
e3fdbd2
b232315
9890fd8
56c7b7f
83b7e7e
af77ce8
b6fa45a
a3c83bd
d63f892
d2de338
9e8731d
2695d96
cc8ce59
45ef624
835c827
2518d3d
40f3cc2
dc149e6
165ce36
0720df1
22e9bec
dfc03bf
8cba026
d0a0e1f
3b16183
6a46c8c
9253c9c
4e49ab7
ff3e603
fcae645
307d110
69318a0
1b334a1
781516f
9a48759
71c5bef
2089c64
4bc7a7d
661c927
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,8 +18,15 @@ import * as assert from 'assert'; | |||||||||||||||||||||||||||||||||||||
| import {beforeEach, afterEach, describe, it, before, after} from 'mocha'; | ||||||||||||||||||||||||||||||||||||||
| import Q from 'p-queue'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import {Backup, Bigtable, Instance} from '../src'; | ||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||
| Backup, | ||||||||||||||||||||||||||||||||||||||
| BackupTimestamp, | ||||||||||||||||||||||||||||||||||||||
| Bigtable, | ||||||||||||||||||||||||||||||||||||||
| Instance, | ||||||||||||||||||||||||||||||||||||||
| InstanceOptions, | ||||||||||||||||||||||||||||||||||||||
| } from '../src'; | ||||||||||||||||||||||||||||||||||||||
| import {AppProfile} from '../src/app-profile.js'; | ||||||||||||||||||||||||||||||||||||||
| import {CopyBackupConfig} from '../src/backup.js'; | ||||||||||||||||||||||||||||||||||||||
| import {Cluster} from '../src/cluster.js'; | ||||||||||||||||||||||||||||||||||||||
| import {Family} from '../src/family.js'; | ||||||||||||||||||||||||||||||||||||||
| import {Row} from '../src/row.js'; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1414,6 +1421,269 @@ describe('Bigtable', () => { | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Object.keys(policy).forEach(key => assert(key in updatedPolicy)); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| describe('copying backups', () => { | ||||||||||||||||||||||||||||||||||||||
| const sourceExpireTimeMilliseconds = | ||||||||||||||||||||||||||||||||||||||
| PreciseDate.now() + (8 + 300) * 60 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reasoning behind this arithmetic? I'm sure there is some, but to an outsider, all I think of is PEMDAS 🙂 Consider adding a comment
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My local system tests are hitting quotas so it's hard to recall exactly by testing it out. But if I recall correctly, the create backup expire time has to be sufficiently far ahead of the current time and the copied backup expire time has to be sufficiently ahead of the create backup expire time to avoid server errors specific to the copy backup feature. I added a comment for this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the comment! Makes sense as to why it needs to be a large number. My remaining confusion is why the arithmetic needs to be there rather than adding a fixed integer to PreciseDate.now()?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To your point, I think we can change this to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, thank you! I agree with the 60 * 60 * 1000 making sense. I made a suggestion to modify the comment
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestions applied |
||||||||||||||||||||||||||||||||||||||
| const sourceExpireTime = new PreciseDate(sourceExpireTimeMilliseconds); | ||||||||||||||||||||||||||||||||||||||
| const copyExpireTimeMilliseconds = | ||||||||||||||||||||||||||||||||||||||
| PreciseDate.now() + (8 + 600) * 60 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||
danieljbruce marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
| const copyExpireTime = new PreciseDate(copyExpireTimeMilliseconds); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||
| This function checks that when a backup is copied using the provided | ||||||||||||||||||||||||||||||||||||||
| config that a new backup is created on the instance. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| async function testCopyBackup( | ||||||||||||||||||||||||||||||||||||||
| backup: Backup, | ||||||||||||||||||||||||||||||||||||||
| config: CopyBackupConfig, | ||||||||||||||||||||||||||||||||||||||
| instance: Instance | ||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||
| // Get a list of backup ids before the copy | ||||||||||||||||||||||||||||||||||||||
| const [backupsBeforeCopy] = await instance.getBackups(); | ||||||||||||||||||||||||||||||||||||||
| const backupIdsBeforeCopy = backupsBeforeCopy.map(backup => backup.id); | ||||||||||||||||||||||||||||||||||||||
| // Copy the backup | ||||||||||||||||||||||||||||||||||||||
| const [newBackup, operation] = await backup.copy(config); | ||||||||||||||||||||||||||||||||||||||
| assert.strictEqual(config.id, newBackup.id); | ||||||||||||||||||||||||||||||||||||||
| await operation.promise(); | ||||||||||||||||||||||||||||||||||||||
| const id = config.id; | ||||||||||||||||||||||||||||||||||||||
| const backupPath = `${config.cluster.name}/backups/${id}`; | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| // Ensure that the backup specified by the config and id match the backup name for the operation returned by the server. | ||||||||||||||||||||||||||||||||||||||
| // the split/map/join functions replace the project name with the {{projectId}} string | ||||||||||||||||||||||||||||||||||||||
| assert(operation); | ||||||||||||||||||||||||||||||||||||||
| assert(operation.metadata); | ||||||||||||||||||||||||||||||||||||||
| assert.strictEqual( | ||||||||||||||||||||||||||||||||||||||
| operation.metadata.name | ||||||||||||||||||||||||||||||||||||||
| .split('/') | ||||||||||||||||||||||||||||||||||||||
| .map((item, index) => (index === 1 ? '{{projectId}}' : item)) | ||||||||||||||||||||||||||||||||||||||
| .join('/'), | ||||||||||||||||||||||||||||||||||||||
| backupPath | ||||||||||||||||||||||||||||||||||||||
| .split('/') | ||||||||||||||||||||||||||||||||||||||
| .map((item, index) => (index === 1 ? '{{projectId}}' : item)) | ||||||||||||||||||||||||||||||||||||||
| .join('/') | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| // Check that there is now one more backup | ||||||||||||||||||||||||||||||||||||||
| const [backupsAfterCopy] = await instance.getBackups(); | ||||||||||||||||||||||||||||||||||||||
| const newBackups = backupsAfterCopy.filter( | ||||||||||||||||||||||||||||||||||||||
| backup => !backupIdsBeforeCopy.includes(backup.id) | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| assert.strictEqual(newBackups.length, 1); | ||||||||||||||||||||||||||||||||||||||
| const [fetchedNewBackup] = newBackups; | ||||||||||||||||||||||||||||||||||||||
| // Ensure the fetched backup matches the config | ||||||||||||||||||||||||||||||||||||||
| assert.strictEqual(fetchedNewBackup.id, id); | ||||||||||||||||||||||||||||||||||||||
| assert.strictEqual(fetchedNewBackup.name, backupPath); | ||||||||||||||||||||||||||||||||||||||
| // Delete the copied backup | ||||||||||||||||||||||||||||||||||||||
| await config.cluster.backup(fetchedNewBackup.id).delete(); | ||||||||||||||||||||||||||||||||||||||
leahecole marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| describe('should create backup of a table and copy it in the same cluster', async () => { | ||||||||||||||||||||||||||||||||||||||
| async function testWithExpiryTimes( | ||||||||||||||||||||||||||||||||||||||
| sourceTestExpireTime: BackupTimestamp, | ||||||||||||||||||||||||||||||||||||||
| copyTestExpireTime: BackupTimestamp | ||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||
| const [backup, op] = await TABLE.createBackup(generateId('backup'), { | ||||||||||||||||||||||||||||||||||||||
| expireTime: sourceTestExpireTime, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| await op.promise(); | ||||||||||||||||||||||||||||||||||||||
| // Check expiry time for running operation. | ||||||||||||||||||||||||||||||||||||||
| await backup.getMetadata(); | ||||||||||||||||||||||||||||||||||||||
| assert.deepStrictEqual(backup.expireDate, sourceExpireTime); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| await testCopyBackup( | ||||||||||||||||||||||||||||||||||||||
| backup, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| cluster: backup.cluster, | ||||||||||||||||||||||||||||||||||||||
| id: generateId('backup'), | ||||||||||||||||||||||||||||||||||||||
| expireTime: copyTestExpireTime, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| INSTANCE | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| it('should copy to the same cluster with precise date expiry times', async () => { | ||||||||||||||||||||||||||||||||||||||
| await testWithExpiryTimes(sourceExpireTime, copyExpireTime); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| it('should copy to the same cluster with timestamp expiry times', async () => { | ||||||||||||||||||||||||||||||||||||||
| // Calling toStruct converts times to a timestamp object. | ||||||||||||||||||||||||||||||||||||||
| // For example: sourceExpireTime.toStruct() = {seconds: 1706659851, nanos: 981000000} | ||||||||||||||||||||||||||||||||||||||
| await testWithExpiryTimes( | ||||||||||||||||||||||||||||||||||||||
| sourceExpireTime.toStruct(), | ||||||||||||||||||||||||||||||||||||||
| copyExpireTime.toStruct() | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| it('should copy to the same cluster with date expiry times', async () => { | ||||||||||||||||||||||||||||||||||||||
| await testWithExpiryTimes( | ||||||||||||||||||||||||||||||||||||||
| new Date(sourceExpireTimeMilliseconds), | ||||||||||||||||||||||||||||||||||||||
| new Date(copyExpireTimeMilliseconds) | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| it('should create backup of a table and copy it on another cluster of another instance', async () => { | ||||||||||||||||||||||||||||||||||||||
| const [backup, op] = await TABLE.createBackup(generateId('backup'), { | ||||||||||||||||||||||||||||||||||||||
| expireTime: sourceExpireTime, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| await op.promise(); | ||||||||||||||||||||||||||||||||||||||
| // Check the expiry time. | ||||||||||||||||||||||||||||||||||||||
| await backup.getMetadata(); | ||||||||||||||||||||||||||||||||||||||
| assert.deepStrictEqual(backup.expireDate, sourceExpireTime); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| // Create another instance | ||||||||||||||||||||||||||||||||||||||
| const instance = bigtable.instance(generateId('instance')); | ||||||||||||||||||||||||||||||||||||||
| const destinationClusterId = generateId('cluster'); | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| // Create production instance with given options | ||||||||||||||||||||||||||||||||||||||
| const instanceOptions: InstanceOptions = { | ||||||||||||||||||||||||||||||||||||||
| clusters: [ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| id: destinationClusterId, | ||||||||||||||||||||||||||||||||||||||
| nodes: 3, | ||||||||||||||||||||||||||||||||||||||
| location: 'us-central1-f', | ||||||||||||||||||||||||||||||||||||||
| storage: 'ssd', | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||
| labels: {'prod-label': 'prod-label'}, | ||||||||||||||||||||||||||||||||||||||
| type: 'production', | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| const [, operation] = await instance.create(instanceOptions); | ||||||||||||||||||||||||||||||||||||||
| await operation.promise(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| // Create the copy and test the copied backup | ||||||||||||||||||||||||||||||||||||||
| await testCopyBackup( | ||||||||||||||||||||||||||||||||||||||
| backup, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| cluster: new Cluster(instance, destinationClusterId), | ||||||||||||||||||||||||||||||||||||||
| id: generateId('backup'), | ||||||||||||||||||||||||||||||||||||||
| expireTime: copyExpireTime, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| instance | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| await instance.delete(); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| it('should create backup of a table and copy it on another cluster of the same instance', async () => { | ||||||||||||||||||||||||||||||||||||||
| const [backup, op] = await TABLE.createBackup(generateId('backup'), { | ||||||||||||||||||||||||||||||||||||||
| expireTime: sourceExpireTime, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| await op.promise(); | ||||||||||||||||||||||||||||||||||||||
| // Check the expiry time. | ||||||||||||||||||||||||||||||||||||||
| await backup.getMetadata(); | ||||||||||||||||||||||||||||||||||||||
| assert.deepStrictEqual(backup.expireDate, sourceExpireTime); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| const destinationClusterId = generateId('cluster'); | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| // Create destination cluster with given options | ||||||||||||||||||||||||||||||||||||||
| const [, operation] = await INSTANCE.cluster( | ||||||||||||||||||||||||||||||||||||||
| destinationClusterId | ||||||||||||||||||||||||||||||||||||||
| ).create({ | ||||||||||||||||||||||||||||||||||||||
| location: 'us-central1-b', | ||||||||||||||||||||||||||||||||||||||
| nodes: 3, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| await operation.promise(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| // Create the copy and test the copied backup | ||||||||||||||||||||||||||||||||||||||
| await testCopyBackup( | ||||||||||||||||||||||||||||||||||||||
| backup, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| cluster: new Cluster(INSTANCE, destinationClusterId), | ||||||||||||||||||||||||||||||||||||||
| id: generateId('backup'), | ||||||||||||||||||||||||||||||||||||||
| expireTime: copyExpireTime, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| INSTANCE | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| after(async () => { | |
| const q = new Q({concurrency: 5}); | |
| const instances = [INSTANCE, DIFF_INSTANCE, CMEK_INSTANCE]; | |
| // need to delete backups first due to instance deletion precondition | |
| await Promise.all(instances.map(instance => reapBackups(instance))); | |
| await Promise.all( | |
| instances.map(instance => { | |
| q.add(async () => { | |
| try { | |
| await instance.delete(); | |
| } catch (e) { | |
| console.log(`Error deleting instance: ${instance.id}`); | |
| } | |
| }); | |
| }) | |
| ); | |
| }); |
Uh oh!
There was an error while loading. Please reload this page.