Skip to content

Sync cloud integration on Authorization problems #4324

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 1 addition & 2 deletions src/plus/integrations/models/gitHostIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,8 +665,7 @@ export abstract class GitHostIntegration<
);
return { value: pullRequests, duration: Date.now() - start };
} catch (ex) {
Logger.error(ex, scope);
return { error: ex, duration: Date.now() - start };
return this.handleProviderException(ex, scope, { error: ex, duration: Date.now() - start });
}
}

Expand Down
28 changes: 25 additions & 3 deletions src/plus/integrations/models/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,22 @@ export abstract class IntegrationBase<
void this.ensureSession({ createIfNeeded: false });
}

private static readonly requestExceptionLimit = 5;
private static readonly syncDueToRequestExceptionLimit = 1;
private syncCountDueToRequestException = 0;
private requestExceptionCount = 0;

resetRequestExceptionCount(): void {
this.requestExceptionCount = 0;
this.syncCountDueToRequestException = 0;
}

/**
* Resets request exceptions without resetting the amount of syncs
*/
smoothifyRequestExceptionCount(): void {
// On resync we reset exception count only to avoid infinitive syncs on failure
this.requestExceptionCount = 0;
}

async reset(): Promise<void> {
Expand Down Expand Up @@ -270,7 +282,17 @@ export abstract class IntegrationBase<

Logger.error(ex, scope);

if (ex instanceof AuthenticationError || ex instanceof RequestClientError) {
if (ex instanceof AuthenticationError && this._session?.cloud) {
if (this.syncCountDueToRequestException < IntegrationBase.syncDueToRequestExceptionLimit) {
this.syncCountDueToRequestException++;
this._session = {
...this._session,
expiresAt: new Date(Date.now() - 1),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a hypothetical scenario I want to check with you here.

Let's say I have a GitHub token which has the required scopes to get my current account but not the scopes to get PRs.

So, every time the Launchpad automatically fetches PRs (let's say, every 5 minutes), it first gets the current account, which succeeds (and also resets the sync count) and then it tries to get PRs and fails because of the scope issue, throws an AuthenticationError, and we force it to sync.

This would cause us to sync every time the Launchpad fetches PRs which would hit our backend pretty hard and probably is not the intention, but..is it possible? If we can confirm that I am worrying for nothing, I think we are ok to merge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@axosoft-ramint

That makes sense. It's hard to reproduce, probably should be reproduced on a self-hosted server where the integration is connected using PAT.

Anyway, I've implemented the fix here: 4e1b411 when a success in one use-case doesn't cancel failure in another case, so we avoid continuous syncing.

Also I've noticed that when GKDev keeps returning expired key we keep refreshing it in refreshSessionIfExpired(), so I added another fix: 3c06400

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@axosoft-ramint

better solution for unsuccessful syncing of expired tokens: b178a03

} else {
this.trackRequestException();
}
} else if (ex instanceof AuthenticationError || ex instanceof RequestClientError) {
this.trackRequestException();
}
return defaultValue;
Expand Down Expand Up @@ -304,7 +326,7 @@ export abstract class IntegrationBase<
trackRequestException(): void {
this.requestExceptionCount++;

if (this.requestExceptionCount >= 5 && this._session !== null) {
if (this.requestExceptionCount >= IntegrationBase.requestExceptionLimit && this._session !== null) {
void showIntegrationDisconnectedTooManyFailedRequestsWarningMessage(this.name);
void this.disconnect({ currentSessionOnly: true });
}
Expand Down Expand Up @@ -370,7 +392,7 @@ export abstract class IntegrationBase<
}

this._session = session ?? null;
this.resetRequestExceptionCount();
this.smoothifyRequestExceptionCount();

if (session != null) {
await this.container.storage.storeWorkspace(this.connectedKey, true);
Expand Down