Skip to content

Commit

Permalink
chore: retries improvements (#230)
Browse files Browse the repository at this point in the history
* chore: retries improvements

* chore: improvements on retries

* chore: improvements on retries

* chore: add unit test

* chore: format

* chore: format

* chore: format

* fix: remove console
  • Loading branch information
alexcastrodev committed Oct 3, 2023
1 parent 5246b67 commit 22a9bc4
Show file tree
Hide file tree
Showing 12 changed files with 1,843 additions and 51 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.vscode
.env
.idea
.lock
deno.lock
*.sh
**/.DS_Store
12 changes: 11 additions & 1 deletion deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
} from "https://deno.land/[email protected]/assert/mod.ts";
import {
assertSpyCalls,
returnsNext,
spy,
stub,
} from "https://deno.land/[email protected]/testing/mock.ts";

import { CONSTANTS } from "./src/utils.ts";
Expand All @@ -18,4 +20,12 @@ const soxa = new ServiceProvider({
baseURL,
});

export { assertEquals, assertRejects, assertSpyCalls, soxa, spy };
export {
assertEquals,
assertRejects,
assertSpyCalls,
returnsNext,
soxa,
spy,
stub,
};
Binary file removed src/.DS_Store
Binary file not shown.
20 changes: 18 additions & 2 deletions src/Helpers/Retry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ServiceError } from "../Types/index.ts";
import { Logger } from "./Logger.ts";

export type RetryCallbackProps = {
Expand All @@ -12,11 +13,17 @@ async function* createAsyncIterable<T>(
delay: number,
) {
for (let i = 0; i < retries; i++) {
const isLastAttempt = i === retries - 1;
try {
const data = await callback({ attempt: i });
yield data;
return;
} catch (e) {
if (e instanceof ServiceError && isLastAttempt) {
yield e;
return;
}

yield null;
Logger.error(e);
await new Promise((resolve) => setTimeout(resolve, delay));
Expand All @@ -29,18 +36,27 @@ export class Retry {
async fetch<T = unknown>(
callback: callbackType<T>,
) {
let lastError = null;
for await (
const callbackResult of createAsyncIterable<T>(
callback,
this.maxRetries,
this.retryDelay,
)
) {
if (callbackResult) {
const isError = callbackResult instanceof Error;

if (callbackResult && !isError) {
return callbackResult as T;
}

if (isError) {
lastError = callbackResult;
}
}

throw new Error(`Max retries (${this.maxRetries}) exceeded.`);
throw new Error(`Max retries (${this.maxRetries}) exceeded.`, {
cause: lastError,
});
}
}
60 changes: 14 additions & 46 deletions src/Services/GithubApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import {
queryUserPullRequest,
queryUserRepository,
} from "../Schemas/index.ts";
import { soxa } from "../../deps.ts";
import { Retry } from "../Helpers/Retry.ts";
import { GithubError, QueryDefaultResponse } from "../Types/index.ts";
import { CONSTANTS } from "../utils.ts";
import { EServiceKindError } from "../Types/EServiceKindError.ts";
import { ServiceError } from "../Types/ServiceError.ts";
import { Logger } from "../Helpers/Logger.ts";
import { requestGithubData } from "./request.ts";

// Need to be here - Exporting from another file makes array of null
export const TOKENS = [
Expand Down Expand Up @@ -59,6 +58,7 @@ export class GithubApiService extends GithubRepository {
async requestUserInfo(username: string): Promise<UserInfo | ServiceError> {
// Avoid to call others if one of them is null
const repository = await this.requestUserRepository(username);

if (repository instanceof ServiceError) {
Logger.error(repository);
return repository;
Expand All @@ -78,7 +78,7 @@ export class GithubApiService extends GithubRepository {

if (status.includes("rejected")) {
Logger.error(`Can not find a user with username:' ${username}'`);
return new ServiceError("not found", EServiceKindError.NOT_FOUND);
return new ServiceError("Not found", EServiceKindError.NOT_FOUND);
}

return new UserInfo(
Expand All @@ -89,27 +89,6 @@ export class GithubApiService extends GithubRepository {
);
}

private handleError(responseErrors: GithubError[]): ServiceError {
const errors = responseErrors ?? [];

const isRateLimitExceeded = errors.some((error) =>
error.type.includes(EServiceKindError.RATE_LIMIT) ||
error.message.includes("rate limit")
);

if (isRateLimitExceeded) {
throw new ServiceError(
"Rate limit exceeded",
EServiceKindError.RATE_LIMIT,
);
}

throw new ServiceError(
"unknown error",
EServiceKindError.NOT_FOUND,
);
}

async executeQuery<T = unknown>(
query: string,
variables: { [key: string]: string },
Expand All @@ -120,36 +99,25 @@ export class GithubApiService extends GithubRepository {
CONSTANTS.DEFAULT_GITHUB_RETRY_DELAY,
);
const response = await retry.fetch<Promise<T>>(async ({ attempt }) => {
const res = await soxa.post("", {}, {
data: { query: query, variables },
headers: {
Authorization: `bearer ${TOKENS[attempt]}`,
},
});
if (res?.data?.errors) {
return this.handleError(res?.data?.errors);
}
return res;
}) as QueryDefaultResponse<{ user: T }>;
return await requestGithubData(
query,
variables,
TOKENS[attempt],
);
});

return response?.data?.data?.user ??
new ServiceError("not found", EServiceKindError.NOT_FOUND);
return response;
} catch (error) {
if (error instanceof ServiceError) {
Logger.error(error);
return error;
if (error.cause instanceof ServiceError) {
Logger.error(error.cause.message);
return error.cause;
}
// TODO: Move this to a logger instance later
if (error instanceof Error && error.cause) {
Logger.error(JSON.stringify(error.cause, null, 2));
} else {
Logger.error(error);
}

return new ServiceError(
"Rate limit exceeded",
EServiceKindError.RATE_LIMIT,
);
return new ServiceError("not found", EServiceKindError.NOT_FOUND);
}
}
}
20 changes: 20 additions & 0 deletions src/Services/__mocks__/notFoundUserMock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"data": {
"user": null
},
"errors": [
{
"type": "NOT_FOUND",
"path": [
"user"
],
"locations": [
{
"line": 2,
"column": 5
}
],
"message": "Could not resolve to a User with the login of 'alekinho'."
}
]
}
18 changes: 18 additions & 0 deletions src/Services/__mocks__/rateLimitMock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"exceeded": {
"data": {
"documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#secondary-rate-limits",
"message": "You have exceeded a secondary rate limit. Please wait a few minutes before you try again. If you reach out to GitHub Support for help, please include the request ID DBD8:FB98:31801A8:3222432:65195FDB."
}
},
"rate_limit": {
"data": {
"errors": [
{
"type": "RATE_LIMITED",
"message": "API rate limit exceeded for user ID 10711649."
}
]
}
}
}
Loading

1 comment on commit 22a9bc4

@vercel
Copy link

@vercel vercel bot commented on 22a9bc4 Oct 3, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.