feature: idiomatic table backup management implementation#761
feature: idiomatic table backup management implementation#761
Conversation
|
System and samples tests are both currently failing due to: All necessary methods are present (in master) within |
|
It seems the Factory reference usually would take a pathway to service (like const backup = instance.backup('my-backup', 'my-cluster');
backup.getMetadata();
backup.update(...); |
src/backup.ts
Outdated
| * @param {Bigtable} bigtable | ||
| * @param {google.bigtable.admin.v2.IBackup} backup A Backup or | ||
| */ | ||
| constructor(bigtable: Bigtable, backup: google.bigtable.admin.v2.IBackup) { |
There was a problem hiding this comment.
Looking at the pattern of other classes in the library, the constructor should take an instance of its parent class (seems Cluster in this case) and a string id.
User may pass just an 'my-backup' as an id or a full name as 'projects/{my-project}/instances/{my-instance}/clusters/{my-cluster}/backups/{my-backup}'. Then the pattern is to
let name;
if (id.includes('/')) {
if (id.startsWith(`${cluster.name}/backups/`)) {
name = id;
} else {
throw new Error(`Backup id '${id}' is not formatted correctly.
Please use the format 'my-backup' or '${cluster.name}/backups/my-backup'.`);
}
} else {
name = `${cluster.name}/backups/${id}`;
}
this.id = name.split('/').pop()!;
this.name = name;
src/backup.ts
Outdated
| constructor(bigtable: Bigtable, backup: google.bigtable.admin.v2.IBackup) { | ||
| Object.assign(this, backup); | ||
|
|
||
| this.metadata = backup.valueOf(); |
There was a problem hiding this comment.
As per the pattern this.metadata usually gets set/updated during getMetadata call.
src/backup.ts
Outdated
| Object.assign(this, backup); | ||
|
|
||
| this.metadata = backup.valueOf(); | ||
| this._bigtable = bigtable; |
There was a problem hiding this comment.
would be cluster.bigtable if following suggestion above.
also this.cluster = cluster is useful to delegate backup.create to.
src/backup.ts
Outdated
| sourceTable?: string | null; | ||
| expireTime?: google.protobuf.ITimestamp | null; | ||
| startTime?: google.protobuf.ITimestamp | null; | ||
| endTime?: google.protobuf.ITimestamp | null; | ||
| sizeBytes?: number | Long | string | null; | ||
| state?: |
There was a problem hiding this comment.
If follow the pattern of other classes in the library, these values are located in this.metadata?
There was a problem hiding this comment.
@AVaksman I don't quite follow this comment. Could you explain a bit? Or maybe this comment is showing in the wrong place?
There was a problem hiding this comment.
What I meant to say it usually all proto properties of a resource are located in the metadata property of the resource object.
Instance.metadata has type, labels, displayName, state
Cluster.metadata has defaultStorageType, serveNodes, location, state
Table.metadata has clusterStates, columnFamilies, granularity
Class objects usually have properties id, name, metadata, bigtable, instance(if it is not an instance of class Instance).
src/backup.ts
Outdated
| * @readonly | ||
| * @return {string} | ||
| */ | ||
| get projectId(): string { |
There was a problem hiding this comment.
projectId might not be available at the time of instantiation of the object and usually is updated during the first network call in prepareGaxRequest via getProjectId_, and subsequently updated across reqOpts.
src/backup.ts
Outdated
| */ | ||
| update( | ||
| fields: ModifiableBackupFields, | ||
| options?: UpdateBackupOptions |
There was a problem hiding this comment.
Could probably use top level gaxOptions?: CallOptions as in other setMetadata methods
src/backup.ts
Outdated
| update( | ||
| fields: ModifiableBackupFields, | ||
| options?: UpdateBackupOptions | ||
| ): Promise<UpdateBackupResponse> { |
There was a problem hiding this comment.
Would need overloads here as well
src/backup.ts
Outdated
| return this._bigtable | ||
| .instance(this.instanceId) | ||
| .cluster(this.clusterId) | ||
| .updateBackup(this.backupId, fields, options); | ||
| } |
There was a problem hiding this comment.
Usually setMetadata functions belong to the object itself.
src/backup.ts
Outdated
|
|
||
| return this._bigtable | ||
| .instance(this.instanceId) | ||
| .restoreTable(this.backupId, this.clusterId, tableId, options); |
There was a problem hiding this comment.
Please refer to comment for instance.restoreTable
src/backup.ts
Outdated
| * A plain-old-javascript-object representation of this backup. | ||
| * @return {google.bigtable.admin.v2.IBackup} | ||
| */ | ||
| valueOf(): google.bigtable.admin.v2.IBackup { |
There was a problem hiding this comment.
I do not think we utilize this method in other classes. Usually this value would be assigned to this.metadata
src/cluster.ts
Outdated
| export interface CreateBackupOptions { | ||
| gaxOptions?: CallOptions; | ||
| } |
There was a problem hiding this comment.
Perhaps we should package all but id params in one object
export interface CreateBackupOptions {
tableId: string; // tableId might be enough since we can build full table path
expireTime: BackupTimestamp;
gaxOptions?: CallOptions;
}Similar to creteInstance, createCluster, createTable functions.
src/cluster.ts
Outdated
| * minimum 6 hours from the time of the backup request and a maximum of 30 | ||
| * days. | ||
| */ | ||
| expireTime?: BackupTimestamp; |
There was a problem hiding this comment.
expireTime could be required here since it is required for creation of a backup, and is the only field that can be updated on an existing backup.
src/instance.ts
Outdated
| export type RestoreTableCallback = ( | ||
| err: ServiceError | null, | ||
| apiResponse?: LROperation< | ||
| google.bigtable.admin.v2.ITable, | ||
| google.bigtable.admin.v2.IRestoreTableMetadata | ||
| > | ||
| ) => void; |
There was a problem hiding this comment.
Long running operation functions that return a resource object usually have the return signatures as in this example
nodejs-bigtable/src/instance.ts
Lines 102 to 111 in 5e47bbc
src/cluster.ts
Outdated
| export type CreateBackupCallback = ( | ||
| err: ServiceError | null, | ||
| apiResponse?: LROperation< | ||
| google.bigtable.admin.v2.IBackup, | ||
| google.bigtable.admin.v2.ICreateBackupMetadata | ||
| > | ||
| ) => void; | ||
| export type CreateBackupResponse = [ | ||
| LROperation< | ||
| google.bigtable.admin.v2.IBackup, | ||
| google.bigtable.admin.v2.ICreateBackupMetadata | ||
| > | ||
| ]; |
src/cluster.ts
Outdated
| * described at https://aip.dev/132#ordering. | ||
| */ | ||
| orderBy?: string; | ||
|
|
There was a problem hiding this comment.
should have autoPaginate, pageSize and pageToken as well.
src/cluster.ts
Outdated
| export type ListBackupsResponse = [Backup[]]; | ||
| export type ListBackupsCallback = ( | ||
| err: ServiceError | null, | ||
| metadata?: Backup[] | ||
| ) => void; |
There was a problem hiding this comment.
Paged functions callbacks/responses usually are of the form
export interface PagedCallback {
(
err: ServiceError | null,
results?: Backup[] | null,
nextQuery?: GetBackupsOptions | null,
response?: Response | null
): void;
}
src/cluster.ts
Outdated
| * `name` should be supplied to be useful in most cases. | ||
| * @return {Backup} | ||
| */ | ||
| asBackup(backup: google.bigtable.admin.v2.IBackup): Backup { |
There was a problem hiding this comment.
Should we introduce a factory backup function similar to Instance.table or Insance.cluster?
src/cluster.ts
Outdated
| table: Table | string, | ||
| id: string, | ||
| fields: Required<ModifiableBackupFields>, | ||
| optionsOrCallback?: CreateBackupOptions | CreateBackupCallback, |
There was a problem hiding this comment.
Commented above about possibly combining params.
src/cluster.ts
Outdated
| ); | ||
| } | ||
|
|
||
| listBackups(options?: ListBackupsOptions): Promise<ListBackupsResponse>; |
There was a problem hiding this comment.
Probably should name getBackups as other similar paged functions getTables, getClusters ...
There was a problem hiding this comment.
are the other underlying rpcs similarly listTable and not getTables? Either way this may make sense for interface consistency.
There was a problem hiding this comment.
Yes, API has getInstances, getClusters, getTables, getAppProfiles, ... that wrap underlying list RPCs.
src/cluster.ts
Outdated
| * @example <caption>include:samples/document-snippets/cluster.js</caption> | ||
| * region_tag:bigtable_cluster_list_backups | ||
| */ | ||
| listBackups( |
There was a problem hiding this comment.
Suggesting to implement getBackups in the Instance class to get all backups across all clusters on the instance by specifying reqOpts.parent as projects/{project}/instances/{instance}/clusters/- and have cluster.getBackups delegate to instance.getBackupswith specific cluster name as reqOpts.parent
src/cluster.ts
Outdated
| id: string, | ||
| fields: ModifiableBackupFields, | ||
| optionsOrCallback?: UpdateBackupOptions | UpdateBackupCallback, | ||
| cb?: UpdateBackupCallback |
There was a problem hiding this comment.
Pattern-wise it seems we do not have similar methods in other classes, just object.setMetadata
Otherwise would propose to bundle up the params in one options object.
src/cluster.ts
Outdated
| * | ||
| * These methods can be auto-paginated. | ||
| */ | ||
| paginator.extend(Cluster, ['listBackups']); |
There was a problem hiding this comment.
I believe gapic supports auto-pagination now. I do not think this is needed?
There was a problem hiding this comment.
I went through pagination thoroughly and recall finding that gapic was incompatible with what was expected from the backups RPC, at least per the generated types and protobufs. The implementation as it stands using the paginator library works as expected.
src/instance.ts
Outdated
| reqOpts, | ||
| gaxOpts: options.gaxOptions, | ||
| }, | ||
| callback // TODO cast as Table |
There was a problem hiding this comment.
should splice in a table Object in the callback.
src/backup.ts
Outdated
|
|
||
| /** | ||
| * @param {Bigtable} bigtable | ||
| * @param {google.bigtable.admin.v2.IBackup} backup A Backup or |
There was a problem hiding this comment.
Seems this comment is incomplete?
|
|
||
| // Alternatively, if a backup object is available, use the `delete` helper. | ||
| const backupPartial = cluster.asBackup({ | ||
| name: backupFromTable.name, // used from another example |
There was a problem hiding this comment.
I don't understand this comment?
| cluster | ||
| .deleteBackup(backupId, { | ||
| gaxOptions: { | ||
| timeout: 50 * 1000, // this op takes a good 30-40 seconds |
There was a problem hiding this comment.
nit: time cannot itself be good or bad ;) also, probably shouldn't abbreviate op
src/backup.ts
Outdated
| sourceTable?: string | null; | ||
| expireTime?: google.protobuf.ITimestamp | null; | ||
| startTime?: google.protobuf.ITimestamp | null; | ||
| endTime?: google.protobuf.ITimestamp | null; | ||
| sizeBytes?: number | Long | string | null; | ||
| state?: |
There was a problem hiding this comment.
@AVaksman I don't quite follow this comment. Could you explain a bit? Or maybe this comment is showing in the wrong place?
src/cluster.ts
Outdated
| /** | ||
| * The ITimestamp (Date or PreciseDate will be converted) representing | ||
| * when the backup will automatically be deleted. This must be at a | ||
| * minimum 6 hours from the time of the backup request and a maximum of 30 |
There was a problem hiding this comment.
this min/max is mentioned but I didn't see anything validating this.
There was a problem hiding this comment.
from what i found, this min/max was not documented. the grpc response will respond with an error outside of these bounds. i opted to not perform client-side validation in case this changes in the future.
src/cluster.ts
Outdated
| ); | ||
| } | ||
|
|
||
| listBackups(options?: ListBackupsOptions): Promise<ListBackupsResponse>; |
There was a problem hiding this comment.
are the other underlying rpcs similarly listTable and not getTables? Either way this may make sense for interface consistency.
…lete timeouts, expireTime is required
…expire to field encapsulation for flexibility
…precise-date for backup timestamps
…e dupe proto import, import reorder
…for backup timestamps, show Backup usage in sample
…nsider promise wrapping
…le name instead of reference, orig backup as public metadata
…sting helper and update sample accordingly
4d8a372 to
1b69be9
Compare
Codecov Report
@@ Coverage Diff @@
## master #761 +/- ##
==========================================
+ Coverage 99.14% 99.18% +0.04%
==========================================
Files 17 18 +1
Lines 15880 16938 +1058
Branches 955 1039 +84
==========================================
+ Hits 15744 16800 +1056
- Misses 133 135 +2
Partials 3 3
Continue to review full report at Codecov.
|
| "api-documenter": "api-documenter yaml --input-folder=temp" | ||
| }, | ||
| "dependencies": { | ||
| "@google-cloud/paginator": "^3.0.0", |
There was a problem hiding this comment.
There was a problem hiding this comment.
Sure,
- add gapic method name (
listBackupsStream: true) togapicStreamingMethods - pipe, rather
pumpifyreceived objects through aTransformstream that will convert proto objects into this libraryBackupobjects. Similar reference below.
nodejs-bigtable/src/instance.ts
Lines 727 to 744 in 2917d17
| const uuid = require('uuid'); | ||
| const {inspect} = require('util'); | ||
|
|
||
| function generateBackupId() { |
There was a problem hiding this comment.
These almost always belong in the samples test, and are passed in as a parameter to the main function (will keep reading though)
| return `gcloud-tests-${uuid.v4()}`.substr(0, 48); | ||
| } | ||
|
|
||
| async function runBackupOperations( |
There was a problem hiding this comment.
It's important that we have an outer main function, with an inner async function due to the way these get embedded in cloudsite. Please follow the pattern here:
https://github.com/googleapis/nodejs-bigtable/blob/master/samples/quickstart.js
| }); | ||
| console.log( | ||
| 'Started a table backup operation:', | ||
| inspect(backupOperation.latestResponse.name, {depth: null, colors: true}) |
There was a problem hiding this comment.
Typically we don't drop down to util.inspect, but just drop individual properties in the logs. Since this is a name, is it just a string type?
| // [END bigtable_delete_backup] | ||
| } | ||
|
|
||
| require('yargs') |
There was a problem hiding this comment.
In our modern samples we do not use yargs, and instead use the model found in https://github.com/googleapis/nodejs-bigtable/blob/master/samples/quickstart.js
| // [END bigtable_cluster_set_meta] | ||
| }, | ||
|
|
||
| createBackup: (instanceId, clusterId, tableId, backupId) => { |
There was a problem hiding this comment.
These snippets are tech debt :/ I believe we don't have tests for many of them. For any new sample code, please create a new file, and follow https://github.com/googleapis/nodejs-bigtable/blob/master/samples/quickstart.js as a guide. One per file!
There was a problem hiding this comment.
Thanks for reviewing the samples. There were competing patterns, and I wasn't sure what to implement. I'll revise based on your feedback.
|
|
||
| describe('backups', () => { | ||
| before(async () => { | ||
| await instance.create({ |
There was a problem hiding this comment.
I seem to remember there being a fairly low cap on the number of clusters you can have at a time. I created samples/test/util.js to help do a check-in/out re-use thing to keep the number of clusters we create down if possible.
| }); | ||
|
|
||
| it('should create a backup of a table', () => { | ||
| clusterSnippets.createBackup(INSTANCE_ID, CLUSTER_ID, TABLE_ID, BACKUP_ID); |
There was a problem hiding this comment.
Are these all async functions? Do we need any assertions to make sure they ran successfully? Is there a chance these tests are all skipped in the describe block?
src/backup.ts
Outdated
| * @example <caption>include:samples/document-snippets/cluster.js</caption> | ||
| * region_tag:bigtable_cluster_delete_backup | ||
| */ | ||
| delete(options?: DeleteBackupOptions): Promise<DeleteBackupResponse> { |
There was a problem hiding this comment.
You should keep the overloads as is
delete(gaxOptions?: CallOptions): Promise<DeleteBackupResponse>;
delete(callback: DeleteBackupCallback): void;
delete(gaxOptions: CallOptions, callback: DeleteBackupCallback): void;and use async for the function implementation
async delete(
optionsOrCallback?: CallOptions | DeleteBackupCallback
): void | Promise<DeleteIBackupResponse> {Provide callback support via callbackifyAll
src/instance.ts
Outdated
| export interface RestoreTableOptions { | ||
| gaxOptions?: CallOptions; | ||
| } | ||
| export type RestoreTableCallback = ( |
There was a problem hiding this comment.
should these types be on table rather than instance?
… to interact with backups
|
@zamnuts can you take a look at the build failures? Thanks! |
Build failures are due to automated tests not being update to reflect the refactored implementation. |
| export type DeleteBackupCallback = GenericBackupCallback<IEmpty>; | ||
| export type DeleteBackupResponse = [Backup, IEmpty]; |
| export type CreateBackupCallback = ( | ||
| err: ServiceError | null, | ||
| apiResponse?: LROperation< | ||
| google.bigtable.admin.v2.IBackup, | ||
| google.bigtable.admin.v2.ICreateBackupMetadata | ||
| > | ||
| ) => void; | ||
| export type CreateBackupResponse = [ | ||
| LROperation< | ||
| google.bigtable.admin.v2.IBackup, | ||
| google.bigtable.admin.v2.ICreateBackupMetadata | ||
| > | ||
| ]; |
There was a problem hiding this comment.
Create backup callback/response should follow the following pattern
nodejs-bigtable/src/instance.ts
Lines 102 to 111 in 5e47bbc
where an Operation is imported from google-gax and IOperation is google.longrunning.IOperation
Use case
const [backup, operation] = await backup.create(...);
await operation.promise();
console.log('finished creating backup');Or
backup.create(..., (err, backup, operation) => {
if (err) {...}
operation
.on('error', (err) => {...})
.on('complete', () => console.log('finished creating backup'));
})| export type RestoreTableCallback = ( | ||
| err: ServiceError | null, | ||
| table: Table | null, | ||
| apiResponse?: LROperation< | ||
| google.bigtable.admin.v2.ITable, | ||
| google.bigtable.admin.v2.IRestoreTableMetadata | ||
| > | ||
| ) => void; | ||
| export type RestoreTableResponse = [ | ||
| Table, | ||
| LROperation< | ||
| google.bigtable.admin.v2.ITable, | ||
| google.bigtable.admin.v2.IRestoreTableMetadata | ||
| > | ||
| ]; |
There was a problem hiding this comment.
restoreTable callback/response should follow the same pattern as createBackup callback/response above.
| * described at https://aip.dev/132#ordering. | ||
| */ | ||
| orderBy?: string; | ||
|
|
There was a problem hiding this comment.
should add pageSize?: number and pageToken?:string here as well.
should respect (until pageSize and pageToken are removed from CallOptions) same values if past within gaxOptions
Top level values should take precedence
reqOpts = extend(
{},
{
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
},
reqOpts
);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;| export type GetBackupsResponse = [Backup[], IBackup[]]; | ||
| export type GetBackupsCallback = ( | ||
| err: ServiceError | null, | ||
| backups?: Backup[], | ||
| apiResponse?: IBackup[] | ||
| ) => void; |
There was a problem hiding this comment.
GetBackupCallback/Response should follow the following pattern
export type GetBackupsCallback = (
err: ServiceError | null,
backups?: Backup[],
nextQuery?: GetBackupsOptions | null,
apiResponse?: google.bigtable.admin.v2.IListBackupsResponse | null
) => void;
export type GetBackupsResponse = [
Backup[],
GetBackupsOptions;
google.bigtable.admin.v2.IListBackupsResponse;
]| backup( | ||
| idOrName: string, | ||
| cluster: string | Cluster, | ||
| fields?: ModifiableBackupFields |
There was a problem hiding this comment.
As per my comment above, I do not think fields are needed in Backup constructor
| if (!clusterId) { | ||
| throw new Error('A complete backup name (path) is required.'); | ||
| } | ||
| backupFrom = new Backup(this.cluster(clusterId), backup); |
There was a problem hiding this comment.
| backupFrom = new Backup(this.cluster(clusterId), backup); | |
| backupFrom = this.backup(this.cluster(clusterId), backup); |
| tableId: string, | ||
| backup: Backup | string, |
There was a problem hiding this comment.
Should wrap it in options object
| const tableAdminClient = this.bigtable.api[ | ||
| 'BigtableTableAdminClient' | ||
| ] as BigtableTableAdminClient; |
There was a problem hiding this comment.
this.bigtable.api may not contain a BigtableTableAdminClient at this point.
| * @example <caption>include:samples/document-snippets/table.js</caption> | ||
| * region_tag:bigtable_create_table | ||
| */ | ||
| backup( |
There was a problem hiding this comment.
Not sure about this convenience function, as it internally sends additional RPC.
This is the hand-written implementation that introduces backup support for tables.
createBackup,getBackup,listBackups,updateBackup, anddeleteBackupare inClusterrestoreTableis inInstancetable.backup()to create a backupBackupclass withdelete,get,restore, andupdatemethodsBackuporBackup[]where applicable, but Longrunning Operations (LROs) do notBackup, this is left to the usercluster.asBackupcasting helper to convert POJOs intoBackupPR checklist:
Fixes ? 🦕