Hoist retry logic
This commit is contained in:
parent
80ad81f872
commit
c7e53bff26
4 changed files with 215 additions and 189 deletions
|
|
@ -211,6 +211,7 @@ export class SyncSettingsTab extends PluginSettingTab {
|
||||||
new Notice(
|
new Notice(
|
||||||
"The changes have been applied successfully!"
|
"The changes have been applied successfully!"
|
||||||
);
|
);
|
||||||
|
await this.statusDescription.updateConnectionState();
|
||||||
} else {
|
} else {
|
||||||
new Notice("No changes to apply");
|
new Notice("No changes to apply");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,25 +39,16 @@ export class ConnectionStatus {
|
||||||
return input.url;
|
return input.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFetchImplementation(
|
|
||||||
fetch: typeof globalThis.fetch,
|
|
||||||
{ doRetries = true }: { doRetries: boolean } = { doRetries: true }
|
|
||||||
): typeof globalThis.fetch {
|
|
||||||
return doRetries ? this.retriedFetchFactory(this.logger, fetch) : fetch;
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset(): void {
|
public reset(): void {
|
||||||
this.rejectUntil(new Error("Sync was reset"));
|
this.rejectUntil(new Error("Sync was reset"));
|
||||||
[this.until, this.resolveUntil, this.rejectUntil] = createPromise();
|
[this.until, this.resolveUntil, this.rejectUntil] = createPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
private retriedFetchFactory(
|
public getFetchImplementation(
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
fetch: typeof globalThis.fetch = globalThis.fetch
|
fetch: typeof globalThis.fetch = globalThis.fetch
|
||||||
): typeof globalThis.fetch {
|
): typeof globalThis.fetch {
|
||||||
return async (input: RequestInfo | URL): Promise<Response> => {
|
return async (input: RequestInfo | URL): Promise<Response> => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
while (true) {
|
|
||||||
while (!this.canFetch) {
|
while (!this.canFetch) {
|
||||||
await this.until;
|
await this.until;
|
||||||
}
|
}
|
||||||
|
|
@ -65,8 +56,7 @@ export class ConnectionStatus {
|
||||||
try {
|
try {
|
||||||
// https://github.com/jonbern/fetch-retry/blob/8684ef4e688375f623bd76f13add76dbc1d67cfb/index.js#L67C1-L70C21
|
// https://github.com/jonbern/fetch-retry/blob/8684ef4e688375f623bd76f13add76dbc1d67cfb/index.js#L67C1-L70C21
|
||||||
const _input =
|
const _input =
|
||||||
typeof Request !== "undefined" &&
|
typeof Request !== "undefined" && input instanceof Request
|
||||||
input instanceof Request
|
|
||||||
? input.clone()
|
? input.clone()
|
||||||
: input;
|
: input;
|
||||||
|
|
||||||
|
|
@ -82,7 +72,7 @@ export class ConnectionStatus {
|
||||||
|
|
||||||
if (!fetchResult.ok) {
|
if (!fetchResult.ok) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Retrying fetch for ${ConnectionStatus.getUrlFromInput(
|
`Fetch for ${ConnectionStatus.getUrlFromInput(
|
||||||
input
|
input
|
||||||
)}, got status ${fetchResult.status}`
|
)}, got status ${fetchResult.status}`
|
||||||
);
|
);
|
||||||
|
|
@ -91,13 +81,11 @@ export class ConnectionStatus {
|
||||||
return fetchResult;
|
return fetchResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Retrying fetch for ${ConnectionStatus.getUrlFromInput(
|
`Fetch for ${ConnectionStatus.getUrlFromInput(
|
||||||
input
|
input
|
||||||
)}, got error: ${error}`
|
)}, got error: ${error}`
|
||||||
);
|
);
|
||||||
}
|
throw error;
|
||||||
|
|
||||||
await Promise.race([this.until, sleep(1000)]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
import type { Logger } from "../tracing/logger";
|
import type { Logger } from "../tracing/logger";
|
||||||
import type { Settings } from "../persistence/settings";
|
import type { Settings } from "../persistence/settings";
|
||||||
import type { ConnectionStatus } from "./connection-status";
|
import type { ConnectionStatus } from "./connection-status";
|
||||||
|
import { sleep } from "../utils/sleep";
|
||||||
|
|
||||||
export interface CheckConnectionResult {
|
export interface CheckConnectionResult {
|
||||||
isSuccessful: boolean;
|
isSuccessful: boolean;
|
||||||
|
|
@ -16,8 +17,8 @@ export interface CheckConnectionResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SyncService {
|
export class SyncService {
|
||||||
private client!: Client<paths>;
|
private client: Client<paths>;
|
||||||
private clientWithoutRetries!: Client<paths>;
|
private pingClient: Client<paths>;
|
||||||
private _fetchImplementation: typeof globalThis.fetch = globalThis.fetch;
|
private _fetchImplementation: typeof globalThis.fetch = globalThis.fetch;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
|
@ -25,20 +26,26 @@ export class SyncService {
|
||||||
private readonly settings: Settings,
|
private readonly settings: Settings,
|
||||||
private readonly logger: Logger
|
private readonly logger: Logger
|
||||||
) {
|
) {
|
||||||
this.createClient(this.settings.getSettings().remoteUri);
|
[this.client, this.pingClient] = this.createClient(
|
||||||
|
this.settings.getSettings().remoteUri
|
||||||
|
);
|
||||||
|
|
||||||
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
||||||
if (newSettings.remoteUri === oldSettings.remoteUri) {
|
if (newSettings.remoteUri === oldSettings.remoteUri) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.createClient(newSettings.remoteUri);
|
[this.client, this.pingClient] = this.createClient(
|
||||||
|
newSettings.remoteUri
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public set fetchImplementation(fetch: typeof globalThis.fetch) {
|
public set fetchImplementation(fetch: typeof globalThis.fetch) {
|
||||||
this._fetchImplementation = fetch;
|
this._fetchImplementation = fetch;
|
||||||
this.createClient(this.settings.getSettings().remoteUri);
|
[this.client, this.pingClient] = this.createClient(
|
||||||
|
this.settings.getSettings().remoteUri
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static formatError(
|
private static formatError(
|
||||||
|
|
@ -62,6 +69,7 @@ export class SyncService {
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
contentBytes: Uint8Array;
|
contentBytes: Uint8Array;
|
||||||
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
|
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
|
||||||
|
return this.withRetries(async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (documentId !== undefined) {
|
if (documentId !== undefined) {
|
||||||
formData.append("document_id", documentId);
|
formData.append("document_id", documentId);
|
||||||
|
|
@ -98,6 +106,7 @@ export class SyncService {
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async put({
|
public async put({
|
||||||
|
|
@ -111,6 +120,7 @@ export class SyncService {
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
contentBytes: Uint8Array;
|
contentBytes: Uint8Array;
|
||||||
}): Promise<components["schemas"]["DocumentUpdateResponse"]> {
|
}): Promise<components["schemas"]["DocumentUpdateResponse"]> {
|
||||||
|
return this.withRetries(async () => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Updating document ${documentId} with parent version ${parentVersionId} and relative path ${relativePath}`
|
`Updating document ${documentId} with parent version ${parentVersionId} and relative path ${relativePath}`
|
||||||
);
|
);
|
||||||
|
|
@ -149,6 +159,7 @@ export class SyncService {
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete({
|
public async delete({
|
||||||
|
|
@ -158,6 +169,7 @@ export class SyncService {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
relativePath: RelativePath;
|
relativePath: RelativePath;
|
||||||
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
|
}): Promise<components["schemas"]["DocumentVersionWithoutContent"]> {
|
||||||
|
return this.withRetries(async () => {
|
||||||
const response = await this.client.DELETE(
|
const response = await this.client.DELETE(
|
||||||
"/vaults/{vault_id}/documents/{document_id}",
|
"/vaults/{vault_id}/documents/{document_id}",
|
||||||
{
|
{
|
||||||
|
|
@ -185,6 +197,7 @@ export class SyncService {
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get({
|
public async get({
|
||||||
|
|
@ -192,6 +205,7 @@ export class SyncService {
|
||||||
}: {
|
}: {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
}): Promise<components["schemas"]["DocumentVersion"]> {
|
}): Promise<components["schemas"]["DocumentVersion"]> {
|
||||||
|
return this.withRetries(async () => {
|
||||||
const response = await this.client.GET(
|
const response = await this.client.GET(
|
||||||
"/vaults/{vault_id}/documents/{document_id}",
|
"/vaults/{vault_id}/documents/{document_id}",
|
||||||
{
|
{
|
||||||
|
|
@ -218,12 +232,16 @@ export class SyncService {
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAll(
|
public async getAll(
|
||||||
since?: VaultUpdateId
|
since?: VaultUpdateId
|
||||||
): Promise<components["schemas"]["FetchLatestDocumentsResponse"]> {
|
): Promise<components["schemas"]["FetchLatestDocumentsResponse"]> {
|
||||||
const response = await this.client.GET("/vaults/{vault_id}/documents", {
|
return this.withRetries(async () => {
|
||||||
|
const response = await this.client.GET(
|
||||||
|
"/vaults/{vault_id}/documents",
|
||||||
|
{
|
||||||
params: {
|
params: {
|
||||||
path: {
|
path: {
|
||||||
vault_id: this.settings.getSettings().vaultName
|
vault_id: this.settings.getSettings().vaultName
|
||||||
|
|
@ -235,7 +253,8 @@ export class SyncService {
|
||||||
since_update_id: since
|
since_update_id: since
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { error } = response;
|
const { error } = response;
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -249,6 +268,7 @@ export class SyncService {
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkConnection(): Promise<CheckConnectionResult> {
|
public async checkConnection(): Promise<CheckConnectionResult> {
|
||||||
|
|
@ -273,8 +293,9 @@ export class SyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No retries
|
||||||
private async ping(): Promise<components["schemas"]["PingResponse"]> {
|
private async ping(): Promise<components["schemas"]["PingResponse"]> {
|
||||||
const response = await this.clientWithoutRetries.GET("/ping", {
|
const response = await this.pingClient.GET("/ping", {
|
||||||
params: {
|
params: {
|
||||||
header: {
|
header: {
|
||||||
authorization: `Bearer ${this.settings.getSettings().token}`
|
authorization: `Bearer ${this.settings.getSettings().token}`
|
||||||
|
|
@ -293,20 +314,34 @@ export class SyncService {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createClient(remoteUri: string): void {
|
/**
|
||||||
this.client = createClient<paths>({
|
* Create a client and a ping client for the given remote URI.
|
||||||
|
*/
|
||||||
|
private createClient(remoteUri: string): [Client<paths>, Client<paths>] {
|
||||||
|
return [
|
||||||
|
createClient<paths>({
|
||||||
baseUrl: remoteUri,
|
baseUrl: remoteUri,
|
||||||
fetch: this.connectionStatus.getFetchImplementation(
|
fetch: this.connectionStatus.getFetchImplementation(
|
||||||
|
this.logger,
|
||||||
this._fetchImplementation
|
this._fetchImplementation
|
||||||
)
|
)
|
||||||
});
|
}),
|
||||||
|
createClient<paths>({
|
||||||
this.clientWithoutRetries = createClient<paths>({
|
|
||||||
baseUrl: remoteUri,
|
baseUrl: remoteUri,
|
||||||
fetch: this.connectionStatus.getFetchImplementation(
|
fetch: this._fetchImplementation
|
||||||
this._fetchImplementation,
|
})
|
||||||
{ doRetries: false }
|
];
|
||||||
)
|
}
|
||||||
});
|
|
||||||
|
private async withRetries<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`Failed network call (${e}), retrying`);
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ export class UnrestrictedSyncer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
document.metadata.parentVersionId >= response.vaultUpdateId
|
// `Syncer` creates fake local document metadata for all remote docs with invalid hashes. The parent IDs will likely match
|
||||||
|
// the latest versions so we still need to update the local versions to turn the fakes into real metadata.
|
||||||
|
document.metadata.parentVersionId > response.vaultUpdateId
|
||||||
) {
|
) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Document ${document.relativePath} is already more up to date than the fetched version`
|
`Document ${document.relativePath} is already more up to date than the fetched version`
|
||||||
|
|
@ -281,7 +283,7 @@ export class UnrestrictedSyncer {
|
||||||
remoteVersion.vaultUpdateId
|
remoteVersion.vaultUpdateId
|
||||||
) {
|
) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Document ${remoteVersion.relativePath} is already more up to date than the fetched version`
|
`Document ${remoteVersion.relativePath} is already at least as up to date as the fetched version`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue