Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions src/backup/backupCreator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Connection from '../connection/index.js';
import { WeaviateInvalidInputError } from '../errors.js';
import { WeaviateInvalidInputError, WeaviateUnsupportedFeatureError } from '../errors.js';
import {
BackupConfig,
BackupCreateRequest,
BackupCreateResponse,
BackupCreateStatusResponse,
} from '../openapi/types.js';
import { DbVersionSupport } from '../utils/dbVersion.js';
import { CommandBase } from '../validation/commandBase.js';
import BackupCreateStatusGetter from './backupCreateStatusGetter.js';
import { Backend } from './index.js';
Expand All @@ -26,10 +27,17 @@ export default class BackupCreator extends CommandBase {
private statusGetter: BackupCreateStatusGetter;
private waitForCompletion!: boolean;
private config?: BackupConfig;

constructor(client: Connection, statusGetter: BackupCreateStatusGetter) {
private incrementalBaseBackupId?: string;
private dbVersionSupport?: DbVersionSupport;

constructor(
client: Connection,
statusGetter: BackupCreateStatusGetter,
dbVersionSupport?: DbVersionSupport
) {
super(client);
this.statusGetter = statusGetter;
this.dbVersionSupport = dbVersionSupport;
}

withIncludeClassNames(...classNames: string[]) {
Expand Down Expand Up @@ -70,6 +78,16 @@ export default class BackupCreator extends CommandBase {
return this;
}

/**
* The ID of an existing backup to build a file-based incremental backup on. Unchanged files are
* restored from the base, so deleting a base backup breaks every incremental built on it.
* Requires Weaviate `v1.37.0` or higher.
*/
withIncrementalBaseBackupId(backupId: string) {
Comment thread
g-despot marked this conversation as resolved.
this.incrementalBaseBackupId = backupId;
return this;
}

validate = (): void => {
this.addErrors([
...validateIncludeClassNames(this.includeClassNames),
Expand All @@ -90,12 +108,24 @@ export default class BackupCreator extends CommandBase {
config: this.config,
include: this.includeClassNames,
exclude: this.excludeClassNames,
incremental_base_backup_id: this.incrementalBaseBackupId,
} as BackupCreateRequest;

if (this.waitForCompletion) {
return this._createAndWaitForCompletion(payload);
return this.checkIncrementalSupport().then(() =>
this.waitForCompletion ? this._createAndWaitForCompletion(payload) : this._create(payload)
);
};

/** Weaviate below v1.37.0 ignores the field and silently writes a full backup, so fail loudly instead. */
private checkIncrementalSupport = (): Promise<void> => {
if (this.incrementalBaseBackupId === undefined || this.dbVersionSupport === undefined) {
return Promise.resolve();
}
return this._create(payload);
return this.dbVersionSupport.supportsIncrementalBackups().then((check) => {
if (!check.supports) {
throw new WeaviateUnsupportedFeatureError(check.message);
}
});
};

_create = (payload: BackupCreateRequest): Promise<BackupCreateResponse> => {
Expand Down
5 changes: 3 additions & 2 deletions src/backup/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Connection from '../connection/index.js';
import { DbVersionSupport } from '../utils/dbVersion.js';
import BackupCreateStatusGetter from './backupCreateStatusGetter.js';
import BackupCreator from './backupCreator.js';
import BackupRestoreStatusGetter from './backupRestoreStatusGetter.js';
Expand All @@ -22,9 +23,9 @@ export interface Backup {
restoreStatusGetter: () => BackupRestoreStatusGetter;
}

const backup = (client: Connection): Backup => {
const backup = (client: Connection, dbVersionSupport: DbVersionSupport): Backup => {
return {
creator: () => new BackupCreator(client, new BackupCreateStatusGetter(client)),
creator: () => new BackupCreator(client, new BackupCreateStatusGetter(client), dbVersionSupport),
createStatusGetter: () => new BackupCreateStatusGetter(client),
restorer: () => new BackupRestorer(client, new BackupRestoreStatusGetter(client)),
restoreStatusGetter: () => new BackupRestoreStatusGetter(client),
Expand Down
22 changes: 18 additions & 4 deletions src/collections/backup/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import {
WeaviateInvalidInputError,
WeaviateUnexpectedResponseError,
WeaviateUnexpectedStatusCodeError,
WeaviateUnsupportedFeatureError,
} from '../../errors.js';
import {
BackupCreateResponse,
BackupCreateStatusResponse,
BackupListResponse,
BackupRestoreResponse,
} from '../../openapi/types.js';
import { DbVersionSupport } from '../../utils/dbVersion.js';
import {
BackupArgs,
BackupCancelArgs,
Expand All @@ -31,8 +34,8 @@ import {
ListBackupOptions,
} from './types.js';

export const backup = (connection: Connection): Backup => {
const parseStatus = (res: BackupCreateStatusResponse | BackupRestoreResponse): BackupStatusReturn => {
export const backup = (connection: Connection, dbVersionSupport: DbVersionSupport): Backup => {
const parseStatus = (res: BackupCreateStatusResponse & BackupRestoreResponse): BackupStatusReturn => {
if (res.id === undefined) {
throw new WeaviateUnexpectedResponseError('Backup ID is undefined in response');
}
Expand All @@ -47,6 +50,8 @@ export const backup = (connection: Connection): Backup => {
error: res.error,
path: res.path,
status: res.status,
size: res.size,
incrementalBaseBackupId: res.incremental_base_backup_id,
};
};
const parseResponse = (res: BackupCreateResponse | BackupRestoreResponse): BackupReturn => {
Expand Down Expand Up @@ -110,9 +115,12 @@ export const backup = (connection: Connection): Backup => {
return true;
},
create: async (args: BackupArgs<BackupConfigCreate>): Promise<BackupReturn> => {
let builder = new BackupCreator(connection, new BackupCreateStatusGetter(connection))
let builder = new BackupCreator(connection, new BackupCreateStatusGetter(connection), dbVersionSupport)
.withBackupId(args.backupId)
.withBackend(args.backend);
if (args.config?.incrementalBaseBackupId !== undefined) {
builder = builder.withIncrementalBaseBackupId(args.config.incrementalBaseBackupId);
}
if (args.includeCollections) {
builder = builder.withIncludeClassNames(...args.includeCollections);
}
Expand All @@ -129,6 +137,7 @@ export const backup = (connection: Connection): Backup => {
try {
res = await builder.do();
} catch (err) {
if (err instanceof WeaviateUnsupportedFeatureError) throw err;
throw new WeaviateBackupFailed(`Backup creation failed: ${err}`, 'creation');
}
if (res.status === 'FAILED') {
Expand Down Expand Up @@ -213,7 +222,12 @@ export const backup = (connection: Connection): Backup => {
if (opts?.startedAtAsc) {
url += '?order=asc';
}
return connection.get<BackupReturn[]>(url);
return connection.get<BackupListResponse>(url).then((res) =>
res.map(({ incremental_base_backup_id: baseBackupId, ...rest }) => ({
...rest,
incrementalBaseBackupId: baseBackupId,
}))
) as Promise<BackupReturn[]>;
},
};
};
Expand Down
15 changes: 10 additions & 5 deletions src/collections/backup/collection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Backend } from '../../backup/index.js';
import Connection from '../../connection/index.js';
import { DbVersionSupport } from '../../utils/dbVersion.js';
import { backup } from './client.js';
import { BackupReturn, BackupStatusArgs, BackupStatusReturn } from './types.js';
import { BackupConfigCreate, BackupReturn, BackupStatusArgs, BackupStatusReturn } from './types.js';

/** The arguments required to create and restore backups. */
export type BackupCollectionArgs = {
Expand All @@ -13,10 +14,14 @@ export type BackupCollectionArgs = {
waitForCompletion?: boolean;
};

export const backupCollection = (connection: Connection, name: string) => {
const handler = backup(connection);
export const backupCollection = (
connection: Connection,
name: string,
dbVersionSupport: DbVersionSupport
) => {
const handler = backup(connection, dbVersionSupport);
return {
create: (args: BackupCollectionArgs) =>
create: (args: BackupCollectionArgs & { config?: BackupConfigCreate }) =>
handler.create({
...args,
includeCollections: [name],
Expand All @@ -41,7 +46,7 @@ export interface BackupCollection {
* @throws {WeaviateBackupFailed} If the backup creation fails.
* @throws {WeaviateBackupCanceled} If the backup creation is canceled.
*/
create(args: BackupCollectionArgs): Promise<BackupReturn>;
create(args: BackupCollectionArgs & { config?: BackupConfigCreate }): Promise<BackupReturn>;
/**
* Get the status of a backup.
*
Expand Down
4 changes: 4 additions & 0 deletions src/collections/backup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type BackupStatusReturn = {
status: BackupStatus;
/** Size of the backup in Gibs */
size?: number;
/** The ID of the base backup an incremental backup was built on. Only returned to root users, from Weaviate `v1.37.6`, and never by `create()` without `waitForCompletion`. */
incrementalBaseBackupId?: string;
};

/** The return type of a backup creation or restoration operation */
Expand All @@ -38,6 +40,8 @@ export type BackupConfigCreate = {
compressionLevel?: BackupCompressionLevel;
/** The percentage of CPU to use for the backup creation job. */
cpuPercentage?: number;
/** The ID of an existing backup to build a file-based incremental backup on. Unchanged files are restored from the base, so deleting a base backup breaks every incremental built on it. Requires Weaviate `v1.37.0` or higher. */
incrementalBaseBackupId?: string;
};

/** Configuration options available when restoring a backup */
Expand Down
2 changes: 1 addition & 1 deletion src/collections/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ const collection = <T, N, V>(
);
return {
aggregate: aggregateCollection,
backup: backupCollection(connection, capitalizedName),
backup: backupCollection(connection, capitalizedName, dbVersionSupport),
config: config<T>(connection, capitalizedName, dbVersionSupport, tenant),
data: data<T>(connection, capitalizedName, dbVersionSupport, consistencyLevel, tenant),
filter: filter<T extends undefined ? any : T>(),
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ async function client(params: ClientParams): Promise<WeaviateClient> {

const ifc: WeaviateClient = {
alias: alias(connection),
backup: backup(connection),
backup: backup(connection, dbVersionSupport),
batch: batch(connection, dbVersionSupport),
cluster: cluster(connection),
collections: collections(connection, dbVersionSupport),
Expand Down
1 change: 1 addition & 0 deletions src/openapi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type DataObject = definitions['Object'];
export type BackupCreateRequest = definitions['BackupCreateRequest'];
export type BackupCreateResponse = definitions['BackupCreateResponse'];
export type BackupCreateStatusResponse = definitions['BackupCreateStatusResponse'];
export type BackupListResponse = definitions['BackupListResponse'];
export type BackupRestoreRequest = definitions['BackupRestoreRequest'];
export type BackupRestoreResponse = definitions['BackupRestoreResponse'];
export type BackupRestoreStatusResponse = definitions['BackupRestoreStatusResponse'];
Expand Down
7 changes: 7 additions & 0 deletions src/utils/dbVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ export class DbVersionSupport {
message: this.errorMessage('Tokenize endpoint stopwords / stopwordPresets', version.show(), '1.37.2'),
}));

supportsIncrementalBackups = () =>
this.dbVersionProvider.getVersion().then((version) => ({
version,
supports: version.isAtLeast(1, 37, 0),
Comment thread
g-despot marked this conversation as resolved.
message: this.errorMessage('Incremental backups', version.show(), '1.37.0'),
}));

supportsServerSideDefaultVectorIndexType = () =>
this.dbVersionProvider.getVersion().then((version) => ({
version,
Expand Down
2 changes: 1 addition & 1 deletion src/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const app = {
batch: batch(conn, dbVersionSupport),
misc: misc(conn, dbVersionProvider),
c11y: c11y(conn),
backup: backup(conn),
backup: backup(conn, dbVersionSupport),
cluster: cluster(conn),
};

Expand Down
52 changes: 52 additions & 0 deletions test/collections/backup/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,58 @@ describe('Integration testing of backups', () => {
});
});

requireAtLeast(1, 37, 0).describe('incremental backups', () => {
it('creates and restores an incremental backup on top of a base backup', async () => {
const client = await clientPromise;
const collection = await client.collections
.create({ name: 'TestIncrementalBackup' })
.then((col) => col.data.insert().then(() => col));

const base = await client.backup.create({
backupId: randomBackupId(),
backend: 'filesystem',
includeCollections: [collection.name],
waitForCompletion: true,
});
expect(base.status).toBe('SUCCESS');

await collection.data.insert();

const incremental = await client.backup.create({
backupId: randomBackupId(),
backend: 'filesystem',
includeCollections: [collection.name],
config: { incrementalBaseBackupId: base.id },
waitForCompletion: true,
});
expect(incremental.status).toBe('SUCCESS');

await client.collections.delete(collection.name);
const restored = await client.backup.restore({
backupId: incremental.id,
backend: 'filesystem',
includeCollections: [collection.name],
waitForCompletion: true,
});
expect(restored.status).toBe('SUCCESS');
await expect(collection.length()).resolves.toBe(2);

await client.collections.delete(collection.name);
});

it('sends the base backup ID to the server', async () => {
const client = await clientPromise;
await expect(
client.backup.create({
backupId: randomBackupId(),
backend: 'filesystem',
includeCollections: ['TestBackupCollection'],
config: { incrementalBaseBackupId: 'does-not-exist' },
})
).rejects.toThrow(/could not fetch base backup/);
});
});

function randomBackupId() {
return 'backup-id-' + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
}
Expand Down
Loading